mono-pilot 0.2.4 → 0.2.6
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.
|
@@ -16,6 +16,7 @@ import switchModeExtension from "../../tools/switch-mode.js";
|
|
|
16
16
|
import applyPatchExtension from "../../tools/apply-patch.js";
|
|
17
17
|
import userMessageExtension from "./user-message.js";
|
|
18
18
|
import systemPromptExtension from "./system-prompt.js";
|
|
19
|
+
import sessionHintsExtension from "./session-hints.js";
|
|
19
20
|
const toolExtensions = [
|
|
20
21
|
shellExtension,
|
|
21
22
|
globExtension,
|
|
@@ -35,6 +36,7 @@ const toolExtensions = [
|
|
|
35
36
|
applyPatchExtension,
|
|
36
37
|
userMessageExtension,
|
|
37
38
|
systemPromptExtension,
|
|
39
|
+
sessionHintsExtension,
|
|
38
40
|
];
|
|
39
41
|
export default function monoPilotExtension(pi) {
|
|
40
42
|
for (const register of toolExtensions) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
6
|
+
import { hasMessageEntries } from "./mode-runtime.js";
|
|
7
|
+
const SESSION_HINTS_MESSAGE_TYPE = "SessionHints";
|
|
8
|
+
const RULES_RELATIVE_DIR = join(".pi", "rules");
|
|
9
|
+
/** List *.rule.txt full paths from a directory (empty array if dir missing). */
|
|
10
|
+
async function listRuleFiles(dirPath) {
|
|
11
|
+
if (!existsSync(dirPath))
|
|
12
|
+
return [];
|
|
13
|
+
try {
|
|
14
|
+
const entries = await readdir(dirPath, { withFileTypes: true, encoding: "utf8" });
|
|
15
|
+
return entries
|
|
16
|
+
.filter((e) => e.isFile() && e.name.endsWith(".rule.txt"))
|
|
17
|
+
.map((e) => resolve(dirPath, e.name))
|
|
18
|
+
.sort((a, b) => a.localeCompare(b));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Discover rules files grouped by scope. */
|
|
25
|
+
async function discoverRules(cwd) {
|
|
26
|
+
const workspaceRulesDir = resolve(cwd, RULES_RELATIVE_DIR);
|
|
27
|
+
const userRulesDir = resolve(homedir(), RULES_RELATIVE_DIR);
|
|
28
|
+
const [workspaceRules, userRules] = await Promise.all([
|
|
29
|
+
listRuleFiles(workspaceRulesDir),
|
|
30
|
+
listRuleFiles(userRulesDir),
|
|
31
|
+
]);
|
|
32
|
+
return { userRules, projectRules: workspaceRules };
|
|
33
|
+
}
|
|
34
|
+
function shortenHome(filePath) {
|
|
35
|
+
const home = homedir();
|
|
36
|
+
if (filePath.startsWith(home)) {
|
|
37
|
+
return `~${filePath.slice(home.length)}`;
|
|
38
|
+
}
|
|
39
|
+
return filePath;
|
|
40
|
+
}
|
|
41
|
+
export default function sessionHintsExtension(pi) {
|
|
42
|
+
// Render hints matching pi's native section style (same colors as [Context], [Skills], etc.)
|
|
43
|
+
pi.registerMessageRenderer(SESSION_HINTS_MESSAGE_TYPE, (message, _options, theme) => {
|
|
44
|
+
const details = message.details;
|
|
45
|
+
const lines = [];
|
|
46
|
+
const userRules = details?.userRules ?? [];
|
|
47
|
+
const projectRules = details?.projectRules ?? [];
|
|
48
|
+
if (userRules.length > 0 || projectRules.length > 0) {
|
|
49
|
+
lines.push(theme.fg("mdHeading", "[Rules]"));
|
|
50
|
+
if (userRules.length > 0) {
|
|
51
|
+
lines.push(` ${theme.fg("accent", "user")}`);
|
|
52
|
+
for (const filePath of userRules) {
|
|
53
|
+
lines.push(theme.fg("dim", ` ${shortenHome(filePath)}`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (projectRules.length > 0) {
|
|
57
|
+
lines.push(` ${theme.fg("accent", "project")}`);
|
|
58
|
+
for (const filePath of projectRules) {
|
|
59
|
+
lines.push(theme.fg("dim", ` ${shortenHome(filePath)}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (lines.length > 0)
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push(theme.fg("muted", "Mode switch: use ") + theme.fg("dim", "option+m") + theme.fg("muted", " to toggle Plan/Ask/Agent mode."));
|
|
66
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
67
|
+
});
|
|
68
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
69
|
+
if (!ctx.hasUI)
|
|
70
|
+
return;
|
|
71
|
+
const entries = ctx.sessionManager.getEntries();
|
|
72
|
+
if (hasMessageEntries(entries))
|
|
73
|
+
return;
|
|
74
|
+
const details = await discoverRules(ctx.cwd);
|
|
75
|
+
pi.sendMessage({
|
|
76
|
+
customType: SESSION_HINTS_MESSAGE_TYPE,
|
|
77
|
+
content: "",
|
|
78
|
+
display: true,
|
|
79
|
+
details,
|
|
80
|
+
}, { triggerTurn: false });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -138,37 +139,48 @@ async function buildMcpInstructionsEnvelope(workspaceCwd) {
|
|
|
138
139
|
}
|
|
139
140
|
async function buildRulesEnvelope(workspaceCwd) {
|
|
140
141
|
const rulesDirPath = resolve(workspaceCwd, RULES_RELATIVE_DIR);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
directoryEntries
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
const ruleFileNames = directoryEntries
|
|
151
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".rule.txt"))
|
|
152
|
-
.map((entry) => entry.name)
|
|
153
|
-
.sort((a, b) => a.localeCompare(b));
|
|
154
|
-
if (ruleFileNames.length === 0)
|
|
155
|
-
return undefined;
|
|
156
|
-
const rules = [];
|
|
157
|
-
for (const ruleFileName of ruleFileNames) {
|
|
158
|
-
const ruleFilePath = resolve(rulesDirPath, ruleFileName);
|
|
142
|
+
const userRulesDirPath = resolve(homedir(), RULES_RELATIVE_DIR);
|
|
143
|
+
const loadRulesFromDir = async (dirPath) => {
|
|
144
|
+
if (!existsSync(dirPath))
|
|
145
|
+
return new Map();
|
|
146
|
+
let directoryEntries;
|
|
159
147
|
try {
|
|
160
|
-
|
|
161
|
-
const normalized = content.trim();
|
|
162
|
-
if (normalized.length > 0) {
|
|
163
|
-
rules.push(normalized);
|
|
164
|
-
}
|
|
148
|
+
directoryEntries = await readdir(dirPath, { withFileTypes: true, encoding: "utf8" });
|
|
165
149
|
}
|
|
166
150
|
catch {
|
|
167
|
-
|
|
151
|
+
return new Map();
|
|
152
|
+
}
|
|
153
|
+
const ruleFileNames = directoryEntries
|
|
154
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".rule.txt"))
|
|
155
|
+
.map((entry) => entry.name)
|
|
156
|
+
.sort((a, b) => a.localeCompare(b));
|
|
157
|
+
const rules = new Map();
|
|
158
|
+
for (const ruleFileName of ruleFileNames) {
|
|
159
|
+
const ruleFilePath = resolve(dirPath, ruleFileName);
|
|
160
|
+
try {
|
|
161
|
+
const content = await readFile(ruleFilePath, "utf-8");
|
|
162
|
+
const normalized = content.trim();
|
|
163
|
+
if (normalized.length > 0) {
|
|
164
|
+
rules.set(ruleFileName, normalized);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Ignore unreadable rule files.
|
|
169
|
+
}
|
|
168
170
|
}
|
|
171
|
+
return rules;
|
|
172
|
+
};
|
|
173
|
+
const userRules = await loadRulesFromDir(userRulesDirPath);
|
|
174
|
+
const workspaceRules = await loadRulesFromDir(rulesDirPath);
|
|
175
|
+
const mergedRules = new Map(userRules);
|
|
176
|
+
for (const [fileName, content] of workspaceRules) {
|
|
177
|
+
mergedRules.set(fileName, content);
|
|
169
178
|
}
|
|
170
|
-
if (
|
|
179
|
+
if (mergedRules.size === 0)
|
|
171
180
|
return undefined;
|
|
181
|
+
const rules = Array.from(mergedRules.entries())
|
|
182
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
183
|
+
.map(([, content]) => content);
|
|
172
184
|
const lines = ["<rules>"];
|
|
173
185
|
for (const rule of rules) {
|
|
174
186
|
lines.push("<user_rule>");
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { createModeStateData, deriveInitialModeState,
|
|
4
|
-
const MODE_HINT_MESSAGE_TYPE = "Hints";
|
|
3
|
+
import { createModeStateData, deriveInitialModeState, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, parseModeStateEntry, } from "../src/extensions/mode-runtime.js";
|
|
5
4
|
const MODE_STATUS_KEY = "mono-pilot-mode";
|
|
6
5
|
const DESCRIPTION = `Switch the interaction mode to better match the current task. Each mode is optimized for a specific type of work.
|
|
7
6
|
|
|
@@ -311,13 +310,6 @@ export default function switchModeExtension(pi) {
|
|
|
311
310
|
}
|
|
312
311
|
installModeFooter(ctx);
|
|
313
312
|
updateModeStatus(ctx);
|
|
314
|
-
if (ctx.hasUI && !hasMessageEntries(entries)) {
|
|
315
|
-
pi.sendMessage({
|
|
316
|
-
customType: MODE_HINT_MESSAGE_TYPE,
|
|
317
|
-
content: "Mode switch: use `option+m` to toggle Plan/Ask/Agent mode.",
|
|
318
|
-
display: true,
|
|
319
|
-
}, { triggerTurn: false });
|
|
320
|
-
}
|
|
321
313
|
});
|
|
322
314
|
// System prompt injection is handled centrally by system-prompt extension.
|
|
323
315
|
pi.registerTool({
|