@femtomc/mu-agent 26.2.33
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/README.md +28 -0
- package/dist/command_context.d.ts +31 -0
- package/dist/command_context.d.ts.map +1 -0
- package/dist/command_context.js +221 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/meta_agent.d.ts +147 -0
- package/dist/meta_agent.d.ts.map +1 -0
- package/dist/meta_agent.js +424 -0
- package/dist/mu_roles.d.ts +15 -0
- package/dist/mu_roles.d.ts.map +1 -0
- package/dist/mu_roles.js +165 -0
- package/dist/pi_backend.d.ts +20 -0
- package/dist/pi_backend.d.ts.map +1 -0
- package/dist/pi_backend.js +27 -0
- package/dist/pi_sdk_backend.d.ts +20 -0
- package/dist/pi_sdk_backend.d.ts.map +1 -0
- package/dist/pi_sdk_backend.js +152 -0
- package/dist/prompt.d.ts +17 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +153 -0
- package/package.json +23 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, open } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { getModels, getProviders } from "@mariozechner/pi-ai";
|
|
5
|
+
import { AuthStorage, createAgentSession, createBashTool, createEditTool, createReadTool, createWriteTool, DefaultResourceLoader, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { piStreamHasError } from "./pi_backend.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a bare model ID (e.g. "gpt-5.3-codex") to a pi-ai Model object.
|
|
9
|
+
*
|
|
10
|
+
* When multiple providers offer the same model ID, prefer providers that
|
|
11
|
+
* have auth configured (env var, OAuth, or stored API key).
|
|
12
|
+
*/
|
|
13
|
+
function resolveModel(modelId, authStorage, providerConstraint) {
|
|
14
|
+
if (providerConstraint) {
|
|
15
|
+
const providers = getProviders();
|
|
16
|
+
if (!providers.includes(providerConstraint)) {
|
|
17
|
+
throw new Error(`Unknown provider "${providerConstraint}". Available: ${providers.join(", ")}`);
|
|
18
|
+
}
|
|
19
|
+
const models = getModels(providerConstraint);
|
|
20
|
+
return models.find((m) => m.id === modelId);
|
|
21
|
+
}
|
|
22
|
+
let fallback;
|
|
23
|
+
for (const provider of getProviders()) {
|
|
24
|
+
const models = getModels(provider);
|
|
25
|
+
const match = models.find((m) => m.id === modelId);
|
|
26
|
+
if (!match)
|
|
27
|
+
continue;
|
|
28
|
+
// Prefer providers that have auth configured.
|
|
29
|
+
if (authStorage.hasAuth(provider)) {
|
|
30
|
+
return match;
|
|
31
|
+
}
|
|
32
|
+
// Keep first match as fallback.
|
|
33
|
+
if (!fallback) {
|
|
34
|
+
fallback = match;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* In-process backend using the pi SDK.
|
|
41
|
+
*
|
|
42
|
+
* Replaces subprocess spawning of the `pi` CLI with direct use of
|
|
43
|
+
* `createAgentSession` from `@mariozechner/pi-coding-agent`.
|
|
44
|
+
*/
|
|
45
|
+
export class PiSdkBackend {
|
|
46
|
+
async run(opts) {
|
|
47
|
+
const authStorage = new AuthStorage();
|
|
48
|
+
const model = resolveModel(opts.model, authStorage, opts.provider);
|
|
49
|
+
if (!model) {
|
|
50
|
+
const scope = opts.provider ? ` in provider "${opts.provider}"` : "";
|
|
51
|
+
throw new Error(`Model "${opts.model}" not found${scope} in pi-ai registry.`);
|
|
52
|
+
}
|
|
53
|
+
const settingsManager = SettingsManager.inMemory();
|
|
54
|
+
const resourceLoader = createMuResourceLoader({
|
|
55
|
+
cwd: opts.cwd,
|
|
56
|
+
systemPrompt: opts.systemPrompt,
|
|
57
|
+
settingsManager,
|
|
58
|
+
});
|
|
59
|
+
await resourceLoader.reload();
|
|
60
|
+
const tools = [
|
|
61
|
+
createBashTool(opts.cwd),
|
|
62
|
+
createReadTool(opts.cwd),
|
|
63
|
+
createWriteTool(opts.cwd),
|
|
64
|
+
createEditTool(opts.cwd),
|
|
65
|
+
];
|
|
66
|
+
const sessionOpts = {
|
|
67
|
+
cwd: opts.cwd,
|
|
68
|
+
model,
|
|
69
|
+
thinkingLevel: opts.thinking,
|
|
70
|
+
tools,
|
|
71
|
+
sessionManager: SessionManager.inMemory(opts.cwd),
|
|
72
|
+
settingsManager,
|
|
73
|
+
resourceLoader,
|
|
74
|
+
authStorage,
|
|
75
|
+
};
|
|
76
|
+
const { session } = await createAgentSession(sessionOpts);
|
|
77
|
+
let teeFh = null;
|
|
78
|
+
try {
|
|
79
|
+
if (opts.teePath) {
|
|
80
|
+
await mkdir(dirname(opts.teePath), { recursive: true });
|
|
81
|
+
teeFh = await open(opts.teePath, "w");
|
|
82
|
+
}
|
|
83
|
+
// Bind extensions (required for tools to work in print mode).
|
|
84
|
+
await session.bindExtensions({
|
|
85
|
+
commandContextActions: {
|
|
86
|
+
waitForIdle: () => session.agent.waitForIdle(),
|
|
87
|
+
newSession: async () => ({ cancelled: true }),
|
|
88
|
+
fork: async () => ({ cancelled: true }),
|
|
89
|
+
navigateTree: async () => ({ cancelled: true }),
|
|
90
|
+
switchSession: async () => ({ cancelled: true }),
|
|
91
|
+
reload: async () => { },
|
|
92
|
+
},
|
|
93
|
+
onError: () => { },
|
|
94
|
+
});
|
|
95
|
+
let sawError = false;
|
|
96
|
+
const DELTA_TYPES = new Set(["thinking_delta", "toolcall_delta", "text_delta"]);
|
|
97
|
+
// Subscribe to events — serialize to JSONL for tee and error detection.
|
|
98
|
+
const unsub = session.subscribe((event) => {
|
|
99
|
+
const line = JSON.stringify(event);
|
|
100
|
+
if (piStreamHasError(line)) {
|
|
101
|
+
sawError = true;
|
|
102
|
+
}
|
|
103
|
+
// onLine gets everything (CLI needs deltas for live rendering).
|
|
104
|
+
opts.onLine?.(line);
|
|
105
|
+
// Tee file: skip streaming deltas (they carry the full accumulated
|
|
106
|
+
// message state on every token, causing quadratic log growth).
|
|
107
|
+
// Structural events (message_start/end, turn_start/end, tool_execution_*,
|
|
108
|
+
// thinking_start/end, toolcall_start/end) are kept.
|
|
109
|
+
if (teeFh) {
|
|
110
|
+
const aType = event?.assistantMessageEvent?.type;
|
|
111
|
+
if (!DELTA_TYPES.has(aType)) {
|
|
112
|
+
teeFh.write(`${line}\n`).catch(() => { });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
await session.prompt(opts.prompt, { expandPromptTemplates: false });
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
unsub();
|
|
121
|
+
}
|
|
122
|
+
return sawError ? 1 : 0;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
session.dispose();
|
|
126
|
+
if (teeFh) {
|
|
127
|
+
await teeFh.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function createMuResourceLoader(opts) {
|
|
133
|
+
const skillPaths = new Set();
|
|
134
|
+
for (const p of opts.additionalSkillPaths ?? []) {
|
|
135
|
+
skillPaths.add(p);
|
|
136
|
+
}
|
|
137
|
+
// If a repo has a top-level `skills/` dir (like workshop/), load it.
|
|
138
|
+
const repoSkills = join(opts.cwd, "skills");
|
|
139
|
+
if (existsSync(repoSkills)) {
|
|
140
|
+
skillPaths.add(repoSkills);
|
|
141
|
+
}
|
|
142
|
+
return new DefaultResourceLoader({
|
|
143
|
+
cwd: opts.cwd,
|
|
144
|
+
agentDir: opts.agentDir,
|
|
145
|
+
settingsManager: opts.settingsManager ?? SettingsManager.inMemory(),
|
|
146
|
+
additionalSkillPaths: [...skillPaths],
|
|
147
|
+
systemPromptOverride: (_base) => opts.systemPrompt,
|
|
148
|
+
agentsFilesOverride: (base) => ({
|
|
149
|
+
agentsFiles: base.agentsFiles.filter((f) => basename(f.path) === "AGENTS.md"),
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Issue } from "@femtomc/mu-core";
|
|
2
|
+
export type PromptMeta = Record<string, unknown>;
|
|
3
|
+
export declare function splitFrontmatter(text: string): {
|
|
4
|
+
meta: PromptMeta;
|
|
5
|
+
body: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function extractDescription(meta: PromptMeta, body: string): {
|
|
8
|
+
description: string;
|
|
9
|
+
source: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function readPromptMeta(path: string): Promise<PromptMeta>;
|
|
12
|
+
export declare function buildRoleCatalog(repoRoot: string): Promise<string>;
|
|
13
|
+
export declare function renderPromptTemplate(path: string, issue: Pick<Issue, "id" | "title" | "body">, opts?: {
|
|
14
|
+
repoRoot?: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
export declare function resolvePromptPath(repoRoot: string, promptPath: string): string;
|
|
17
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAyCjD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA6BjF;AAYD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAW1G;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAItE;AAMD,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwCxE;AAED,wBAAsB,oBAAoB,CACzC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,OAAO,GAAG,MAAM,CAAC,EAC3C,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9B,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAK9E"}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
3
|
+
function stripQuotes(s) {
|
|
4
|
+
const trimmed = s.trim();
|
|
5
|
+
if (trimmed.length >= 2) {
|
|
6
|
+
const first = trimmed[0];
|
|
7
|
+
const last = trimmed[trimmed.length - 1];
|
|
8
|
+
if ((first === `"` && last === `"`) || (first === `'` && last === `'`)) {
|
|
9
|
+
return trimmed.slice(1, -1);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return trimmed;
|
|
13
|
+
}
|
|
14
|
+
function parseSimpleYamlFrontmatter(text) {
|
|
15
|
+
// We only need a small subset: flat `key: value` mappings.
|
|
16
|
+
// If parsing fails, we return {} (mirrors Python behavior).
|
|
17
|
+
const out = {};
|
|
18
|
+
const lines = text.split(/\r?\n/);
|
|
19
|
+
for (const rawLine of lines) {
|
|
20
|
+
const line = rawLine.trim();
|
|
21
|
+
if (line.length === 0) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (line.startsWith("#")) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const idx = line.indexOf(":");
|
|
28
|
+
if (idx <= 0) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const key = line.slice(0, idx).trim();
|
|
32
|
+
if (!key) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const value = stripQuotes(line.slice(idx + 1));
|
|
36
|
+
out[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
export function splitFrontmatter(text) {
|
|
41
|
+
const lines = text.split(/\r?\n/);
|
|
42
|
+
if (lines.length === 0 || lines[0]?.trim() !== "---") {
|
|
43
|
+
return { meta: {}, body: text };
|
|
44
|
+
}
|
|
45
|
+
// Find the terminating `---` line.
|
|
46
|
+
let endIdx = -1;
|
|
47
|
+
for (let i = 1; i < lines.length; i++) {
|
|
48
|
+
if (lines[i]?.trim() === "---") {
|
|
49
|
+
endIdx = i;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (endIdx < 0) {
|
|
54
|
+
return { meta: {}, body: text };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const metaText = lines.slice(1, endIdx).join("\n");
|
|
58
|
+
const body = lines
|
|
59
|
+
.slice(endIdx + 1)
|
|
60
|
+
.join("\n")
|
|
61
|
+
.replace(/^\n+/, "");
|
|
62
|
+
const meta = parseSimpleYamlFrontmatter(metaText);
|
|
63
|
+
return { meta, body };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { meta: {}, body: text };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function firstNonEmptyLine(text) {
|
|
70
|
+
for (const line of text.split(/\r?\n/)) {
|
|
71
|
+
const stripped = line.trim();
|
|
72
|
+
if (stripped) {
|
|
73
|
+
return stripped;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
export function extractDescription(meta, body) {
|
|
79
|
+
const raw = meta.description;
|
|
80
|
+
const desc = typeof raw === "string" ? raw.trim() : "";
|
|
81
|
+
if (desc) {
|
|
82
|
+
return { description: desc, source: "frontmatter" };
|
|
83
|
+
}
|
|
84
|
+
const bodyDesc = firstNonEmptyLine(body);
|
|
85
|
+
if (bodyDesc) {
|
|
86
|
+
return { description: bodyDesc, source: "body" };
|
|
87
|
+
}
|
|
88
|
+
return { description: "", source: "none" };
|
|
89
|
+
}
|
|
90
|
+
export async function readPromptMeta(path) {
|
|
91
|
+
const text = await readFile(path, "utf8");
|
|
92
|
+
const { meta } = splitFrontmatter(text);
|
|
93
|
+
return meta;
|
|
94
|
+
}
|
|
95
|
+
function toPosixPath(path) {
|
|
96
|
+
return path.replaceAll("\\", "/");
|
|
97
|
+
}
|
|
98
|
+
export async function buildRoleCatalog(repoRoot) {
|
|
99
|
+
const rolesDir = join(repoRoot, ".mu", "roles");
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = await readdir(rolesDir);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
const roleFiles = entries.filter((e) => e.endsWith(".md")).sort();
|
|
108
|
+
const sections = [];
|
|
109
|
+
for (const file of roleFiles) {
|
|
110
|
+
const abs = join(rolesDir, file);
|
|
111
|
+
const text = await readFile(abs, "utf8");
|
|
112
|
+
const { meta, body } = splitFrontmatter(text);
|
|
113
|
+
const name = file.replace(/\.md$/, "");
|
|
114
|
+
const promptPath = toPosixPath(relative(repoRoot, abs));
|
|
115
|
+
const { description, source } = extractDescription(meta, body);
|
|
116
|
+
const parts = [];
|
|
117
|
+
for (const key of ["cli", "model", "reasoning"]) {
|
|
118
|
+
if (key in meta) {
|
|
119
|
+
parts.push(`${key}: ${String(meta[key])}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const configLine = parts.length > 0 ? parts.join(" | ") : "default config";
|
|
123
|
+
const catalogDesc = description || "No description provided.";
|
|
124
|
+
sections.push(`### ${name}\n` +
|
|
125
|
+
`description: ${catalogDesc}\n` +
|
|
126
|
+
`description_source: ${source}\n` +
|
|
127
|
+
`prompt: ${promptPath}\n` +
|
|
128
|
+
`config: ${configLine}`);
|
|
129
|
+
}
|
|
130
|
+
return sections.join("\n\n");
|
|
131
|
+
}
|
|
132
|
+
export async function renderPromptTemplate(path, issue, opts = {}) {
|
|
133
|
+
const text = await readFile(path, "utf8");
|
|
134
|
+
const { body } = splitFrontmatter(text);
|
|
135
|
+
let promptText = issue.title ?? "";
|
|
136
|
+
if (issue.body) {
|
|
137
|
+
promptText += `\n\n${issue.body}`;
|
|
138
|
+
}
|
|
139
|
+
let rendered = body;
|
|
140
|
+
rendered = rendered.replaceAll("{{PROMPT}}", promptText);
|
|
141
|
+
rendered = rendered.replaceAll("{{ISSUE_ID}}", issue.id ?? "");
|
|
142
|
+
if (rendered.includes("{{ROLES}}")) {
|
|
143
|
+
const catalog = opts.repoRoot ? await buildRoleCatalog(opts.repoRoot) : "";
|
|
144
|
+
rendered = rendered.replaceAll("{{ROLES}}", catalog);
|
|
145
|
+
}
|
|
146
|
+
return rendered;
|
|
147
|
+
}
|
|
148
|
+
export function resolvePromptPath(repoRoot, promptPath) {
|
|
149
|
+
if (isAbsolute(promptPath)) {
|
|
150
|
+
return promptPath;
|
|
151
|
+
}
|
|
152
|
+
return join(repoRoot, promptPath);
|
|
153
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@femtomc/mu-agent",
|
|
3
|
+
"version": "26.2.33",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@femtomc/mu-core": "26.2.33",
|
|
18
|
+
"@mariozechner/pi-agent-core": "^0.52.12",
|
|
19
|
+
"@mariozechner/pi-ai": "^0.52.12",
|
|
20
|
+
"@mariozechner/pi-coding-agent": "^0.52.12",
|
|
21
|
+
"zod": "^4.1.9"
|
|
22
|
+
}
|
|
23
|
+
}
|