mono-pilot 0.2.0 → 0.2.1

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 CHANGED
@@ -80,7 +80,7 @@ The full Cursor-styled tool list exposed by the extension:
80
80
  - `FetchMcpResource` – fetch a specific MCP resource
81
81
  - `ListMcpTools` – discover MCP tools and schemas
82
82
  - `CallMcpTool` – invoke MCP tools by name
83
- - `SwitchMode` – switch interaction mode (`/plan`)
83
+ - `SwitchMode` – switch interaction mode (`option + m`, cycles Plan → Ask → Agent)
84
84
  - `ApplyPatch` – apply single-file patches
85
85
 
86
86
  ## User rules
@@ -143,10 +143,6 @@ The report shows:
143
143
  - the runtime envelope built from `<rules>`, `<mcp_instructions>`, `<system_reminder>`, and `<user_query>`
144
144
 
145
145
 
146
- ## Roadmap
146
+ ## License
147
147
 
148
- Project status: MVP (core capabilities are in place and actively evolving).
149
-
150
- - Gradually migrate additional Cursor-style tools from `tools/`.
151
- - Keep compatibility behavior focused and testable, one tool at a time.
152
- - Expand docs and examples for customization.
148
+ MIT
@@ -5,6 +5,12 @@ Plan mode is still active. Continue with the task in the current mode.
5
5
  export const AGENT_MODE_SWITCH_REMINDER = `<system_reminder>
6
6
  You are now in Agent mode. Continue with the task in the new mode.
7
7
  </system_reminder>`;
8
+ export const ASK_MODE_SWITCH_REMINDER = `<system_reminder>
9
+ You are now in Ask mode. Continue with the task in the new mode.
10
+ </system_reminder>`;
11
+ export const ASK_MODE_STILL_ACTIVE_REMINDER = `<system_reminder>
12
+ Ask mode is still active. Continue with the task in the current mode.
13
+ </system_reminder>`;
8
14
  export function parseModeStateEntry(entry) {
9
15
  if (typeof entry !== "object" || entry === null)
10
16
  return undefined;
@@ -17,10 +23,12 @@ export function parseModeStateEntry(entry) {
17
23
  if (typeof data !== "object" || data === null)
18
24
  return undefined;
19
25
  const state = data;
20
- if (state.activeMode === "plan" || state.activeMode === "agent") {
26
+ if (state.activeMode === "plan" || state.activeMode === "agent" || state.activeMode === "ask") {
21
27
  return {
22
28
  activeMode: state.activeMode,
23
- pendingReminder: state.pendingReminder === "plan-entry" || state.pendingReminder === "agent-entry"
29
+ pendingReminder: state.pendingReminder === "plan-entry" ||
30
+ state.pendingReminder === "agent-entry" ||
31
+ state.pendingReminder === "ask-entry"
24
32
  ? state.pendingReminder
25
33
  : undefined,
26
34
  };
@@ -83,14 +91,37 @@ class ModeRuntimeStore {
83
91
  return { changed: false, snapshot: this.getSnapshot() };
84
92
  }
85
93
  this.state.activeMode = nextMode;
86
- this.state.pendingReminder = nextMode === "plan" ? "plan-entry" : "agent-entry";
94
+ switch (nextMode) {
95
+ case "plan":
96
+ this.state.pendingReminder = "plan-entry";
97
+ break;
98
+ case "ask":
99
+ this.state.pendingReminder = "ask-entry";
100
+ break;
101
+ case "agent":
102
+ default:
103
+ this.state.pendingReminder = "agent-entry";
104
+ break;
105
+ }
87
106
  return { changed: true, snapshot: this.getSnapshot() };
88
107
  }
89
108
  toggleMode() {
90
- const nextMode = this.state.activeMode === "plan" ? "agent" : "plan";
109
+ let nextMode;
110
+ switch (this.state.activeMode) {
111
+ case "agent":
112
+ nextMode = "plan";
113
+ break;
114
+ case "plan":
115
+ nextMode = "ask";
116
+ break;
117
+ case "ask":
118
+ default:
119
+ nextMode = "agent";
120
+ break;
121
+ }
91
122
  return this.setMode(nextMode);
92
123
  }
93
- consumeReminder(planEntryReminder) {
124
+ consumeReminder(planEntryReminder, askEntryReminder) {
94
125
  if (this.state.activeMode === "plan") {
95
126
  if (this.state.pendingReminder === "plan-entry") {
96
127
  this.state.pendingReminder = undefined;
@@ -106,6 +137,21 @@ class ModeRuntimeStore {
106
137
  snapshot: this.getSnapshot(),
107
138
  };
108
139
  }
140
+ if (this.state.activeMode === "ask") {
141
+ if (this.state.pendingReminder === "ask-entry") {
142
+ this.state.pendingReminder = undefined;
143
+ return {
144
+ reminder: askEntryReminder,
145
+ changed: true,
146
+ snapshot: this.getSnapshot(),
147
+ };
148
+ }
149
+ return {
150
+ reminder: ASK_MODE_STILL_ACTIVE_REMINDER,
151
+ changed: false,
152
+ snapshot: this.getSnapshot(),
153
+ };
154
+ }
109
155
  if (this.state.pendingReminder === "agent-entry") {
110
156
  this.state.pendingReminder = undefined;
111
157
  return {
@@ -3,9 +3,10 @@ import { readdir, readFile } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import process from "node:process";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { buildRuntimeEnvelope, createModeStateData, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, PLAN_MODE_STILL_ACTIVE_REMINDER, } from "./mode-runtime.js";
6
+ import { buildRuntimeEnvelope, createModeStateData, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, ASK_MODE_SWITCH_REMINDER, PLAN_MODE_STILL_ACTIVE_REMINDER, } from "./mode-runtime.js";
7
7
  import { createRpcRequestId, extractStringHeaders, isRecord, isServerEnabled, parseMcpConfig, postJsonRpcRequest, resolveMcpConfigPath, toNonEmptyString, MCP_PROTOCOL_VERSION, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, } from "../utils/mcp-client.js";
8
8
  const PLAN_MODE_REMINDER_PATH = fileURLToPath(new URL("../../tools/plan-mode-reminder.md", import.meta.url));
9
+ const ASK_MODE_REMINDER_PATH = fileURLToPath(new URL("../../tools/ask-mode-reminder.md", import.meta.url));
9
10
  const RULES_RELATIVE_DIR = join(".pi", "rules");
10
11
  const MCP_INSTRUCTIONS_DESCRIPTION = "Instructions provided by MCP servers to help use them properly";
11
12
  const USER_QUERY_RENDER_PATCH_FLAG = "__monoPilotUserQueryRenderPatched__";
@@ -180,6 +181,7 @@ async function buildRulesEnvelope(workspaceCwd) {
180
181
  export default function runtimeEnvelopeExtension(pi) {
181
182
  void patchInteractiveModeUserMessageDisplay();
182
183
  let planModeReminderCache;
184
+ let askModeReminderCache;
183
185
  let mcpInstructionsCache;
184
186
  let rulesEnvelopeCache;
185
187
  let mcpInstructionsPending;
@@ -199,6 +201,20 @@ export default function runtimeEnvelopeExtension(pi) {
199
201
  return PLAN_MODE_STILL_ACTIVE_REMINDER;
200
202
  }
201
203
  };
204
+ const getAskEntryReminder = async () => {
205
+ if (askModeReminderCache !== undefined) {
206
+ return askModeReminderCache ?? ASK_MODE_SWITCH_REMINDER;
207
+ }
208
+ try {
209
+ const content = await readFile(ASK_MODE_REMINDER_PATH, "utf-8");
210
+ askModeReminderCache = content.trim();
211
+ return askModeReminderCache || ASK_MODE_SWITCH_REMINDER;
212
+ }
213
+ catch {
214
+ askModeReminderCache = null;
215
+ return ASK_MODE_SWITCH_REMINDER;
216
+ }
217
+ };
202
218
  const getMcpInstructions = async () => {
203
219
  if (mcpInstructionsCache !== undefined) {
204
220
  return mcpInstructionsCache ?? undefined;
@@ -242,15 +258,17 @@ export default function runtimeEnvelopeExtension(pi) {
242
258
  // Eagerly pre-fetch rules and MCP instructions in the background so the first input isn't delayed
243
259
  getRulesEnvelope().catch(() => { });
244
260
  getMcpInstructions().catch(() => { });
261
+ getAskEntryReminder().catch(() => { });
245
262
  pi.on("input", async (event) => {
246
263
  if (event.source === "extension")
247
264
  return;
248
- const [planEntryReminder, mcpInstructions, rulesEnvelope] = await Promise.all([
265
+ const [planEntryReminder, askEntryReminder, mcpInstructions, rulesEnvelope] = await Promise.all([
249
266
  getPlanEntryReminder(),
267
+ getAskEntryReminder(),
250
268
  getMcpInstructions(),
251
269
  getRulesEnvelope(),
252
270
  ]);
253
- const { reminder, changed, snapshot } = modeRuntimeStore.consumeReminder(planEntryReminder);
271
+ const { reminder, changed, snapshot } = modeRuntimeStore.consumeReminder(planEntryReminder, askEntryReminder);
254
272
  if (changed) {
255
273
  pi.appendEntry(MODE_STATE_ENTRY_TYPE, createModeStateData(snapshot));
256
274
  }
@@ -7,7 +7,9 @@ This directory stores the tool layer for `mono-pilot`.
7
7
  - `*.ts`: tool implementation (registration, validation, execution)
8
8
  - `*.test.ts`: tool unit tests
9
9
  - `*-description.md`: shared tool description text consumed by the corresponding tool
10
- - `plan-mode-reminder.md`: user message prompt fragment used by `/plan`
10
+ - mode reminders
11
+ - `plan-mode-reminder.md`: system reminder injected when entering Plan mode
12
+ - `ask-mode-reminder.md`: system reminder injected when entering Ask mode
11
13
 
12
14
  Tool descriptions are now loaded by the tool implementation and exposed via the system-prompt extension.
13
15
 
@@ -0,0 +1,30 @@
1
+ <system_reminder>
2
+ You are now in Ask mode. Continue with the task in the new mode.
3
+ </system_reminder>
4
+
5
+ <system_reminder>
6
+ Ask mode is active. The user wants you to answer questions about their codebase or coding in general. You MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).
7
+
8
+ Your role in Ask mode:
9
+
10
+ 1. Answer the user's questions comprehensively and accurately. Focus on providing clear, detailed explanations.
11
+
12
+ 2. Use readonly tools to explore the codebase and gather information needed to answer the user's questions. You can:
13
+ - Read files to understand code structure and implementation
14
+ - Search the codebase to find relevant code
15
+ - Use grep to find patterns and usages
16
+ - List directory contents to understand project structure
17
+ - Read lints/diagnostics to understand code quality issues
18
+
19
+ 3. Provide code examples and references when helpful, citing specific file paths and line numbers.
20
+
21
+ 4. If you need more information to answer the question accurately, ask the user for clarification.
22
+
23
+ 5. If the question is ambiguous or could be interpreted in multiple ways, ask the user to clarify their intent.
24
+
25
+ 6. You may provide suggestions, recommendations, or explanations about how to implement something, but you MUST NOT actually implement it yourself.
26
+
27
+ 7. Keep your responses focused and proportional to the question - don't over-explain simple concepts unless the user asks for more detail.
28
+
29
+ 8. If the user asks you to make changes or implement something, politely remind them that you're in Ask mode and can only provide information and guidance. Suggest they switch to Agent mode if they want you to make changes.
30
+ </system_reminder>
@@ -57,7 +57,7 @@ Available subagent_types and a quick description of what they do:
57
57
  - browser-use: Perform browser-based testing and web automation. This subagent can navigate web pages, interact with elements, fill forms, and take screenshots. Use this for testing web applications, verifying UI changes, or any browser-based tasks. Use this browser subagent when you need to either: (1) parallelize browser tasks alongside other work, or (2) execute a longer sequence of browser actions that benefit from dedicated context. This subagent_type is stateful; if a browserUse subagent already exists, the previously created subagent will be resumed if you reuse the Task tool with subagent_type set to browserUse. (Auto-resumes most recent agent of this type; `resume` arg is ignored)
58
58
 
59
59
  Available models:
60
- - fast (cost: 1/10, intelligence: 5/10): Extremely fast, moderately intelligent model that is effective for tightly scoped changes. Not well-suited for long-horizon tasks or deep investigations.
60
+ - fast: Uses the parent model with thinking effort set to low (same model, lower thinking).
61
61
 
62
62
  When speaking to the USER about which model you selected for a Task/subagent, do NOT reveal these internal model alias names (e.g., fast, alpha, beta, gamma). Instead, use natural language such as "a faster model", "a more capable model", or "the default model".
63
63
 
@@ -12,10 +12,9 @@ const SUBAGENTS_DIRNAME = ".pi/subagents";
12
12
  const SUBAGENT_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
13
13
  const MAX_PARALLEL_TASKS = 8;
14
14
  const MAX_CONCURRENCY = 4;
15
- const FAST_MODEL_HINTS = ["haiku", "flash", "mini", "small", "nano", "lite", "fast", "instant", "spark"];
16
15
  const BUILTIN_SUBAGENT_TYPES = ["generalPurpose", "explore", "shell", "browser-use"];
17
16
  const modelAliasSchema = Type.Union([Type.Literal("fast")], {
18
- description: 'Optional model alias. If not provided, inherits from parent/model profile. Prefer fast for quick, straightforward tasks to minimize cost and latency; use default/inherited model for deeper reasoning tasks.',
17
+ description: 'Optional model alias. If not provided, inherits from parent/model profile. "fast" runs the parent model with thinking effort set to low.',
19
18
  });
20
19
  const subagentTypeSchema = Type.Union([
21
20
  Type.Literal("generalPurpose"),
@@ -240,39 +239,16 @@ function listModelCandidates(ctx) {
240
239
  return [...availableModels];
241
240
  return [...ctx.modelRegistry.getAll()];
242
241
  }
243
- function scoreFastCandidate(model, preferredProvider) {
244
- const haystack = `${model.id} ${model.name ?? ""}`.toLowerCase();
245
- let score = 0;
246
- if (preferredProvider && model.provider === preferredProvider)
247
- score += 20;
248
- if (FAST_MODEL_HINTS.some((hint) => haystack.includes(hint)))
249
- score += 100;
250
- if (haystack.includes("opus") || haystack.includes("sonnet") || haystack.includes("pro"))
251
- score -= 15;
252
- if (haystack.includes("gpt-4") || haystack.includes("gpt-5"))
253
- score -= 10;
254
- return score;
255
- }
256
242
  function pickInheritedModel(ctx) {
257
243
  if (!ctx.model)
258
244
  return undefined;
259
245
  return { provider: ctx.model.provider, modelId: ctx.model.id };
260
246
  }
261
- function pickFastModel(ctx) {
262
- const candidates = listModelCandidates(ctx);
263
- if (candidates.length === 0)
247
+ function pickLowThinkingModel(ctx) {
248
+ const inherited = pickInheritedModel(ctx);
249
+ if (!inherited)
264
250
  return undefined;
265
- const preferredProvider = ctx.model?.provider;
266
- candidates.sort((a, b) => {
267
- const scoreDiff = scoreFastCandidate(b, preferredProvider) - scoreFastCandidate(a, preferredProvider);
268
- if (scoreDiff !== 0)
269
- return scoreDiff;
270
- if (a.provider !== b.provider)
271
- return a.provider.localeCompare(b.provider);
272
- return a.id.localeCompare(b.id);
273
- });
274
- const picked = candidates[0];
275
- return { provider: picked.provider, modelId: picked.id };
251
+ return { ...inherited, thinking: "low" };
276
252
  }
277
253
  function pickModelBySpec(ctx, spec) {
278
254
  const normalized = spec.trim();
@@ -282,7 +258,7 @@ function pickModelBySpec(ctx, spec) {
282
258
  if (lowered === "inherit")
283
259
  return pickInheritedModel(ctx);
284
260
  if (lowered === "fast")
285
- return pickFastModel(ctx) ?? pickInheritedModel(ctx);
261
+ return pickLowThinkingModel(ctx) ?? pickInheritedModel(ctx);
286
262
  const slashIndex = normalized.indexOf("/");
287
263
  if (slashIndex > 0) {
288
264
  const provider = normalized.slice(0, slashIndex);
@@ -303,7 +279,7 @@ function pickModelBySpec(ctx, spec) {
303
279
  }
304
280
  function selectModelForTask(ctx, explicitAlias, profileModelSpec) {
305
281
  if (explicitAlias === "fast") {
306
- return pickFastModel(ctx) ?? pickInheritedModel(ctx);
282
+ return pickLowThinkingModel(ctx) ?? pickInheritedModel(ctx);
307
283
  }
308
284
  if (profileModelSpec) {
309
285
  return pickModelBySpec(ctx, profileModelSpec) ?? pickInheritedModel(ctx);
@@ -748,6 +724,9 @@ async function executeTask(task, index, options) {
748
724
  ];
749
725
  if (selectedModel) {
750
726
  args.push("--provider", selectedModel.provider, "--model", selectedModel.modelId);
727
+ if (selectedModel.thinking === "low") {
728
+ args.push("--thinking", "low");
729
+ }
751
730
  }
752
731
  for (const attachment of attachments) {
753
732
  args.push(`@${attachment}`);
@@ -1,7 +1,7 @@
1
1
  import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { createModeStateData, deriveInitialModeState, hasMessageEntries, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, parseModeStateEntry, } from "../src/extensions/mode-runtime.js";
4
- const MODE_HINT_MESSAGE_TYPE = "switch-mode-hint";
4
+ const MODE_HINT_MESSAGE_TYPE = "Hints";
5
5
  const MODE_STATUS_KEY = "mono-pilot-mode";
6
6
  const DESCRIPTION = `Switch the interaction mode to better match the current task. Each mode is optimized for a specific type of work.
7
7
 
@@ -252,7 +252,11 @@ function updateModeStatus(ctx) {
252
252
  if (!ctx.hasUI)
253
253
  return;
254
254
  const { activeMode } = modeRuntimeStore.getSnapshot();
255
- const statusText = activeMode === "plan" ? ctx.ui.theme.fg("warning", "mode:plan") : ctx.ui.theme.fg("muted", "mode:agent");
255
+ const statusText = activeMode === "plan"
256
+ ? ctx.ui.theme.fg("warning", "mode:plan")
257
+ : activeMode === "ask"
258
+ ? ctx.ui.theme.fg("borderAccent", "mode:ask")
259
+ : ctx.ui.theme.fg("muted", "mode:agent");
256
260
  ctx.ui.setStatus(MODE_STATUS_KEY, statusText);
257
261
  }
258
262
  function persistModeState(pi) {
@@ -273,7 +277,8 @@ function togglePlanMode(pi, ctx) {
273
277
  persistModeState(pi);
274
278
  updateModeStatus(ctx);
275
279
  if (ctx.hasUI) {
276
- ctx.ui.notify(`Switched to ${snapshot.activeMode === "plan" ? "Plan" : "Agent"} mode.`);
280
+ const label = snapshot.activeMode === "plan" ? "Plan" : snapshot.activeMode === "ask" ? "Ask" : "Agent";
281
+ ctx.ui.notify(`Switched to ${label} mode.`);
277
282
  }
278
283
  }
279
284
  export default function switchModeExtension(pi) {
@@ -282,9 +287,11 @@ export default function switchModeExtension(pi) {
282
287
  type: "boolean",
283
288
  default: false,
284
289
  });
285
- pi.registerCommand("plan", {
286
- description: "Toggle between Plan mode and Agent mode",
287
- handler: async (_args, ctx) => togglePlanMode(pi, ctx),
290
+ pi.registerShortcut("alt+m", {
291
+ description: "Cycle between Plan, Ask, and Agent modes",
292
+ handler: async (ctx) => {
293
+ togglePlanMode(pi, ctx);
294
+ }
288
295
  });
289
296
  pi.on("session_start", async (_event, ctx) => {
290
297
  const fromFlag = pi.getFlag("plan") === true;
@@ -307,7 +314,7 @@ export default function switchModeExtension(pi) {
307
314
  if (ctx.hasUI && !hasMessageEntries(entries)) {
308
315
  pi.sendMessage({
309
316
  customType: MODE_HINT_MESSAGE_TYPE,
310
- content: "Mode switch: use `/plan` to toggle Plan/Agent mode.",
317
+ content: "Mode switch: use `option+m` to toggle Plan/Ask/Agent mode.",
311
318
  display: true,
312
319
  }, { triggerTurn: false });
313
320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-pilot",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Cursor-compatible coding agent powered by pi-mono",
5
5
  "type": "module",
6
6
  "license": "MIT",