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
- if (!existsSync(rulesDirPath))
142
- return undefined;
143
- let directoryEntries;
144
- try {
145
- directoryEntries = await readdir(rulesDirPath, { withFileTypes: true, encoding: "utf8" });
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
- const content = await readFile(ruleFilePath, "utf-8");
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
- // Ignore unreadable rule files.
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 (rules.length === 0)
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, hasMessageEntries, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, parseModeStateEntry, } from "../src/extensions/mode-runtime.js";
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-pilot",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Cursor-compatible coding agent powered by pi-mono",
5
5
  "type": "module",
6
6
  "license": "MIT",