mono-pilot 0.2.10 → 0.2.13

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.
Files changed (155) hide show
  1. package/README.md +260 -2
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +1 -2
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +70 -1
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/memory/build-memory.js +103 -0
  77. package/dist/src/memory/config/defaults.js +55 -0
  78. package/dist/src/memory/config/loader.js +29 -0
  79. package/dist/src/memory/config/paths.js +9 -0
  80. package/dist/src/memory/config/resolve.js +90 -0
  81. package/dist/src/memory/config/types.js +1 -0
  82. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  83. package/dist/src/memory/embeddings/cache.js +47 -0
  84. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  85. package/dist/src/memory/embeddings/input-limits.js +48 -0
  86. package/dist/src/memory/embeddings/local.js +108 -0
  87. package/dist/src/memory/embeddings/types.js +1 -0
  88. package/dist/src/memory/index-manager.js +552 -0
  89. package/dist/src/memory/indexing/embeddings.js +67 -0
  90. package/dist/src/memory/indexing/files.js +180 -0
  91. package/dist/src/memory/indexing/index-file.js +105 -0
  92. package/dist/src/memory/log.js +38 -0
  93. package/dist/src/memory/paths.js +15 -0
  94. package/dist/src/memory/runtime/index.js +299 -0
  95. package/dist/src/memory/runtime/thread.js +116 -0
  96. package/dist/src/memory/search/fts.js +57 -0
  97. package/dist/src/memory/search/hybrid.js +50 -0
  98. package/dist/src/memory/search/text.js +30 -0
  99. package/dist/src/memory/search/vector.js +43 -0
  100. package/dist/src/memory/session/content-hash.js +7 -0
  101. package/dist/src/memory/session/entry.js +33 -0
  102. package/dist/src/memory/session/flush-policy.js +34 -0
  103. package/dist/src/memory/session/hook.js +191 -0
  104. package/dist/src/memory/session/paths.js +15 -0
  105. package/dist/src/memory/session/session-reader.js +88 -0
  106. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  107. package/dist/src/memory/session/transcript/entry.js +28 -0
  108. package/dist/src/memory/session/transcript/flush.js +56 -0
  109. package/dist/src/memory/session/transcript/paths.js +28 -0
  110. package/dist/src/memory/session/transcript/reader.js +112 -0
  111. package/dist/src/memory/session/transcript/state.js +31 -0
  112. package/dist/src/memory/store/schema.js +89 -0
  113. package/dist/src/memory/store/sqlite.js +89 -0
  114. package/dist/src/memory/types.js +1 -0
  115. package/dist/src/memory/warm.js +25 -0
  116. package/dist/{tools → src/tools}/README.md +28 -2
  117. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  118. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  119. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  120. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  121. package/dist/src/tools/ast-grep.js +357 -0
  122. package/dist/src/tools/brief-write.js +122 -0
  123. package/dist/src/tools/bus-send.js +100 -0
  124. package/dist/{tools → src/tools}/call-mcp-tool.js +20 -24
  125. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  126. package/dist/src/tools/codex-apply-patch.js +540 -0
  127. package/dist/{tools → src/tools}/delete.js +24 -0
  128. package/dist/src/tools/exit-plan-mode.js +83 -0
  129. package/dist/{tools → src/tools}/fetch-mcp-resource.js +31 -3
  130. package/dist/src/tools/generate-image.js +567 -0
  131. package/dist/{tools → src/tools}/glob.js +55 -1
  132. package/dist/{tools → src/tools}/list-mcp-resources.js +32 -3
  133. package/dist/{tools → src/tools}/list-mcp-tools.js +38 -3
  134. package/dist/src/tools/ls.js +48 -0
  135. package/dist/src/tools/lsp-diagnostics.js +67 -0
  136. package/dist/src/tools/lsp-symbols.js +54 -0
  137. package/dist/src/tools/mailbox.js +85 -0
  138. package/dist/src/tools/memory-get.js +90 -0
  139. package/dist/src/tools/memory-search.js +180 -0
  140. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  141. package/dist/{tools → src/tools}/read-file.js +8 -19
  142. package/dist/{tools → src/tools}/rg.js +10 -20
  143. package/dist/{tools → src/tools}/shell.js +19 -42
  144. package/dist/{tools → src/tools}/subagent.js +255 -6
  145. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  146. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  147. package/dist/{tools → src/tools}/web-search.js +29 -1
  148. package/package.json +21 -9
  149. package/dist/src/utils/mcp-client.js +0 -282
  150. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  151. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  152. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  153. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  154. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  155. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
package/README.md CHANGED
@@ -27,9 +27,25 @@ mono-pilot
27
27
  # Interactive
28
28
  mono-pilot
29
29
 
30
+ # Interactive game profile (murder mystery, role-play)
31
+ mono-pilot --mono-mode game
32
+
30
33
  # One-shot prompt
31
34
  mono-pilot -p "Refactor this module"
32
35
 
36
+ # One-shot game prompt
37
+ mono-pilot --mono-mode game -p "你现在是侦探,先做自我介绍"
38
+
39
+ # Game mode with explicit channel override
40
+ mono-pilot --mono-mode game --game-channel game:office-004
41
+
42
+ # Game mode with identity auto-load from workspace
43
+ # expects .mono-game/profile.json and optional .mono-game/identity.md
44
+ mono-pilot --mono-mode game
45
+
46
+ # Optional: limit tools per role
47
+ # profile.json example: { "displayName": "侦探", "tools": ["BusSend", "MailBox", "ReadFile"] }
48
+
33
49
  # Continue previous session
34
50
  mono-pilot --continue
35
51
  ```
@@ -46,11 +62,14 @@ If you pass `--tools`, MonoPilot removes built-in `edit`, `write`, `read`, `grep
46
62
 
47
63
  - `src/cli.ts` – launcher that wraps `pi`
48
64
  - `src/extensions/mono-pilot.ts` – extension entrypoint (tool wiring)
65
+ - `src/extensions/mono-game.ts` – game-oriented extension entrypoint (story + bus tools)
49
66
  - `src/extensions/system-prompt.ts` – provider-agnostic prompt stack
50
67
  - `src/extensions/user-message.ts` – user message envelope assembly
68
+ - `src/brief/` – persistent agent memory ("brief" system), inspired by [letta-ai/letta-code](https://github.com/letta-ai/letta-code.git)'s memory architecture, renamed from "memory" to "brief" to distinguish condensed knowledge from conversation history
69
+ - `src/memory/` – memory search indexing + retrieval (builtin, SQLite + FTS)
51
70
  - `src/mcp/` – config loading, JSON-RPC transport, server resolution
52
71
  - `src/rules/` – rule file discovery (shared by envelope and session hints)
53
- - `tools/` – tool implementations and descriptions (see `tools/README.md`)
72
+ - `src/tools/` – tool implementations and descriptions (see `src/tools/README.md`)
54
73
 
55
74
  ## Cursor-styled tools
56
75
 
@@ -71,11 +90,15 @@ The full Cursor-styled tool list exposed by the extension:
71
90
  - `Shell` – execute shell commands in the workspace
72
91
  - `Glob` – find paths by glob pattern
73
92
  - `rg` – search file content with ripgrep
93
+ - `AstGrep` – search code structure with ast-grep (`run`/`scan`, read-only)
74
94
  - `ReadFile` – read file content with pagination
75
95
  - `Delete` – delete files or directories
76
96
  - `SemanticSearch` – semantic search by intent
97
+ - `memory_search` – search indexed memory snippets
98
+ - `memory_get` – read a snippet from memory files
77
99
  - `WebSearch` – search the web with snippets
78
100
  - `WebFetch` – fetch and render web content
101
+ - `GenerateImage` – generate images via Gemini API or OpenRouter
79
102
  - `AskQuestion` – collect structured multiple-choice answers
80
103
  - `Subagent` – launch delegated subprocesses
81
104
  - `ListMcpResources` – list MCP resources from config
@@ -83,7 +106,10 @@ The full Cursor-styled tool list exposed by the extension:
83
106
  - `ListMcpTools` – discover MCP tools and schemas
84
107
  - `CallMcpTool` – invoke MCP tools by name
85
108
  - `SwitchMode` – switch interaction mode (`option + m`, cycles Plan → Ask → Agent)
86
- - `ApplyPatch` – apply single-file patches
109
+ - `ExitPlanMode` – exit Plan mode and switch back to Agent mode
110
+ - `ApplyPatch` – apply single-file patches (supports diff-style @@ line hints)
111
+ - `CodexApplyPatch` – apply codex-style multi-file patches with `Add/Delete/Update/Move` operations
112
+ - `MailBox` – read queued bus messages (game mailbox)
87
113
 
88
114
  ## User rules
89
115
 
@@ -102,6 +128,236 @@ When the same filename exists in both, the project rule wins. Each file becomes
102
128
  - MCP tools then progressively load and surface resources, schemas, and execution only when needed.
103
129
  - MCP configs are loaded from `.pi/mcp.json` (project) and `~/.pi/mcp.json` (user); project entries take precedence on name conflicts.
104
130
 
131
+ ## Memory search
132
+
133
+ - Builtin memory search reads `~/.mono-pilot/config.json` (`memorySearch` field). If missing, defaults are used.
134
+ - Local embeddings use `node-llama-cpp`; configure `memorySearch.local.modelPath` (and optional `modelCacheDir`).
135
+ - Session flush triggers are configured via `memorySearch.flush` (`onSessionSwitch`, `onSessionCompact`, `deltaBytes`, `deltaMessages`).
136
+ - Use `/build-memory --mode full|dirty` to rebuild or incrementally sync the current agent's memory index partition.
137
+
138
+ ## Cluster v2 logging
139
+
140
+ - Enable cluster_v2 with `MONO_PILOT_CLUSTER_V2=1` or `MONO_PILOT_CLUSTER_VERSION=2`.
141
+ - cluster_v2 logs are persisted to `~/.mono-pilot/logs/cluster_v2.YYYY-MM-DD.log`.
142
+ - Default behavior is file-only logging (keeps TUI clean). Set `MONO_PILOT_CLUSTER_V2_LOG_STDIO=1` to also mirror logs to stdout/stderr.
143
+ - `/cluster` operational subcommands include `status`, `services`, `reelect`, `stepdown`, and `close`.
144
+
145
+ ## System events (TUI)
146
+
147
+ - MonoPilot keeps an in-session system event queue for low-disruption operational signals (for example SFTP, memory warmup/build/session flush, cluster failover, Discord collector progress, and Twitter pull status/failures).
148
+ - In interactive mode, info/warning/error system events show a bottom-right overlay toast (non-capturing), with latest-only replacement.
149
+ - Overlay can be dismissed with `Esc` or clicking header `[×]`; a 2-minute timeout remains as a fallback.
150
+ - Use `/events` to inspect recent events, `/events <N>` for more lines, and `/events clear` to reset.
151
+
152
+ ## Discord intelligence collector (leader-local)
153
+
154
+ When `cluster_v2` is enabled, leader can run a local Discord IPC collector that subscribes to
155
+ message events and persists them as JSONL for intelligence gathering.
156
+
157
+ - No follower push
158
+ - No Discord channel interaction
159
+ - Local persistence only
160
+
161
+ Config in `~/.mono-pilot/config.json`:
162
+
163
+ ```json
164
+ {
165
+ "discord": {
166
+ "enabled": true,
167
+ "clientId": "YOUR_DISCORD_APP_ID",
168
+ "channels": [
169
+ {
170
+ "id": "123456789012345678",
171
+ "alias": "My Server / general"
172
+ }
173
+ ],
174
+ "events": ["MESSAGE_CREATE", "MESSAGE_UPDATE", "MESSAGE_DELETE"],
175
+ "scopes": ["rpc", "messages.read", "identify"],
176
+ "outputPath": "~/.mono-pilot/discord/messages.jsonl",
177
+ "includeRawPayload": false,
178
+ "systemEventBatchSize": 20
179
+ }
180
+ }
181
+ ```
182
+
183
+ Notes:
184
+
185
+ - Requires a locally running Discord desktop client.
186
+ - If `accessToken` is omitted, collector first tries cached auth from `~/.mono-pilot/auth.json`.
187
+ - If cache is missing/invalid, collector runs RPC `AUTHORIZE` + OAuth token exchange, then stores tokens back to `~/.mono-pilot/auth.json`.
188
+ - For first-time OAuth token exchange, many Discord apps require `clientSecret` (and sometimes `redirectUri`) in config.
189
+ - `accessToken` and cached tokens are never printed in logs.
190
+ - `channels[].alias` is persisted as `channelAlias` in each JSONL event record.
191
+ - Collector enriches records by resolving `channelId -> channelName` and `guildId -> guildName` via RPC (`GET_CHANNEL` / `GET_GUILD`) when available.
192
+ - Collector writes with daily rotation: `outputPath` is treated as base name and actual files are `<base>.YYYY-MM-DD.jsonl`.
193
+ - Collector emits system-events when a channel accumulates `systemEventBatchSize` MESSAGE_CREATE events, then resets the channel counter and continues.
194
+ - Collector service is registered as `discord_intel` in `/cluster services` when active.
195
+
196
+ ## Twitter intelligence collector (leader-local)
197
+
198
+ When `cluster_v2` is enabled, leader can run a local Twitter/X collector via `bird` CLI.
199
+ The collector verifies browser profile/cookie access at startup, then periodically pulls the
200
+ "For You" timeline and persists JSONL records locally.
201
+
202
+ Config in `~/.mono-pilot/config.json`:
203
+
204
+ ```json
205
+ {
206
+ "twitter": {
207
+ "enabled": true,
208
+ "outputPath": "~/.mono-pilot/twitter/home.jsonl",
209
+ "pullIntervalMinutes": 10,
210
+ "pullCount": 10,
211
+ "commandTimeoutMs": 30000,
212
+ "requestTimeoutMs": 15000,
213
+ "chromeProfile": "Profile 4",
214
+ "chromeProfileDir": "~/Library/Application Support/Google/Chrome",
215
+ "cookieSource": ["chrome"],
216
+ "cookieTimeoutMs": 5000,
217
+ "includeRawPayload": false
218
+ }
219
+ }
220
+ ```
221
+
222
+ Notes:
223
+
224
+ - Startup runs `bird check`; if browser profile/cookies are not readable, collector startup fails and is skipped (no in-process retry loop).
225
+ - On successful startup, collector runs one timeline pull immediately and then every `pullIntervalMinutes`; it treats `bird home` and `bird home --following` as two channels and switches between them when the current channel returns 0 tweets.
226
+ - Persisted `tweets[]` keeps bird top-level tweet objects as-is, including nested quote structure (not flattened).
227
+ - Collector uses depth-1 enrichment (`tweet` + `quotedTweet`) and short-link resolution: it records `shortLinkMappings[]` (`shortUrl -> resolvedUrl -> tweetId?`) for all detected t.co links, and when a mapping resolves to a tweet id it attaches `shortLinkMappings[].tweetFull` (deduped by tweet id within a pull cycle, excluding self-reference where `tweetId` equals the host tweet id).
228
+ - Before each pull, collector loads archived `tweetId` snapshots from the recent two-day window (`today` + `yesterday`) and skips re-archiving duplicates that already have `tweetFull`; duplicate ids with missing historical `tweetFull` are kept for backfill attempts.
229
+ - Each persisted batch includes a unique `snapshotId` to avoid downstream overwrite collisions.
230
+ - Persisted `feed` reflects the actual source timeline for the batch (`for_you` or `following`).
231
+ - Per-cycle pull errors are reported as system-events/log warnings and skipped until next scheduled cycle (no immediate retry).
232
+ - Collector writes with daily rotation: `outputPath` is treated as base name and actual files are `<base>.YYYY-MM-DD.jsonl`.
233
+ - Collector service is registered as `twitter_intel` in `/cluster services` when active.
234
+
235
+ ## Digest draft command
236
+
237
+ Use `/digest draft` to generate the digest draft in TUI.
238
+ Use `/digest backfill` to serially backfill missing archive fields in-place (`tweetFull`, `quotedTweetFull`, `shortLinkMappings`).
239
+
240
+ `/digest draft` reads the daily JSONL file (`<outputBase>.YYYY-MM-DD.jsonl`) and classifies tweets with TUI progress events.
241
+ `/digest backfill` scans archive files and writes updates back to the same JSONL lines (serial mode to keep upstream pressure low).
242
+
243
+ Config in `~/.mono-pilot/config.json`:
244
+
245
+ ```json
246
+ {
247
+ "twitter": {
248
+ "digest": {
249
+ "classifier": {
250
+ "provider": "openai",
251
+ "model": "gpt-4o-mini",
252
+ "temperature": 0,
253
+ "maxTokens": 300,
254
+ "concurrency": 4
255
+ }
256
+ }
257
+ }
258
+ }
259
+ ```
260
+
261
+ Command examples:
262
+
263
+ ```text
264
+ /digest draft
265
+ /digest draft --date 2026-03-09
266
+ /digest draft --file ~/.mono-pilot/twitter/home.2026-03-09.jsonl --concurrency 8 --sample 6
267
+ /digest backfill
268
+ /digest backfill --date 2026-03-09
269
+ /digest backfill --file ~/.mono-pilot/twitter/home.2026-03-09.jsonl
270
+ ```
271
+
272
+ Notes:
273
+
274
+ - Classification categories are fixed to 7 classes: 技术 / 产品 / 融资并购 / 开源生态 / 组织动态 / 政策监管 / 学术研究.
275
+ - Depth-1 enriched fields (`tweetFull`, `quotedTweetFull`) are used when present.
276
+ - Draft rendering rewrites `t.co` short links to `shortLinkMappings[].resolvedUrl`; if the resolved link points to the host tweet itself, that short link is removed from draft text.
277
+ - Draft mode now starts in background and rejects duplicate runs while active; check progress via `/events`.
278
+ - Draft artifacts are written to `~/.mono-pilot/twitter/draft.md` (primary draft) and `~/.mono-pilot/twitter/draft-debug.md` (classification debug details).
279
+ - Backfill mode is serial (non-concurrent), starts in background, and reuses per-run tweetId cache to avoid repeated `bird read` calls.
280
+ - `/events` includes backfill start/file progress/end summary plus low-frequency item-level progress updates.
281
+
282
+ ## Image generation config
283
+
284
+ `GenerateImage` can read defaults from `~/.mono-pilot/config.json`:
285
+
286
+ ```json
287
+ {
288
+ "imageGen": {
289
+ "provider": "openrouter",
290
+ "model": "google/gemini-3.1-flash-image-preview"
291
+ },
292
+ "imageGenProviders": {
293
+ "openrouter": {
294
+ "baseUrl": "https://openrouter.ai/api/v1",
295
+ "apiKey": "",
296
+ "authHeader": true,
297
+ "models": [
298
+ { "id": "google/gemini-3.1-flash-image-preview", "name": "Nano Banana 2" }
299
+ ]
300
+ }
301
+ }
302
+ }
303
+ ```
304
+
305
+ Use `/image-model` to switch the active provider/model stored in config:
306
+
307
+ ```text
308
+ /image-model
309
+ /image-model list
310
+ /image-model use openrouter google/gemini-3.1-flash-image-preview
311
+ ```
312
+
313
+ ## SFTP sync on ApplyPatch
314
+
315
+ If `.vscode/sftp.json` exists and a profile has `uploadOnSave: true`, patch tools can trigger uploads after successful writes:
316
+
317
+ - `ApplyPatch`: uploads the affected file.
318
+ - `CodexApplyPatch`: uploads files in `added[]` and `modified[]` (`deleted[]` is intentionally not auto-deleted remotely).
319
+
320
+ Set `interactiveAuth: true` to enable OTP prompts for `/sftp` commands and patch-triggered sync when needed.
321
+
322
+ Manual commands:
323
+
324
+ ```text
325
+ /sftp
326
+ /sftp upload path/to/file-or-dir
327
+ /sftp download path/to/file-or-dir
328
+ /sftp target <targetName>
329
+ ```
330
+
331
+ `/sftp` (without args) opens an interactive target selector (up/down to choose, enter to confirm).
332
+
333
+ ```json
334
+ [
335
+ {
336
+ "name": "prod",
337
+ "protocol": "sftp",
338
+ "host": "10.0.0.130",
339
+ "port": 22,
340
+ "username": "wanqian",
341
+ "privateKeyPath": "~/.ssh/id_rsa",
342
+ "passphrase": "...",
343
+ "remotePath": "/home/wanqian/project",
344
+ "uploadOnSave": true,
345
+ "interactiveAuth": true,
346
+ "hop": {
347
+ "host": "36.151.163.132",
348
+ "port": 22,
349
+ "username": "wanqian",
350
+ "privateKeyPath": "/Users/wanqian/.ssh/id_rsa",
351
+ "passphrase": "...",
352
+ "interactiveAuth": true
353
+ }
354
+ }
355
+ ]
356
+ ```
357
+
358
+ `hop` enables single-jump SFTP tunneling (`local -> hop -> target`).
359
+ Both target and hop authentication are performed by the local client, so `privateKeyPath` values must point to local files.
360
+
105
361
  ## Local development
106
362
 
107
363
  ```bash
@@ -127,6 +383,8 @@ npm run dev:continue
127
383
  npm run dev:watch:continue
128
384
  ```
129
385
 
386
+ When running in interactive mode, `option+o` opens the current workspace directory in `nvim` (falls back to `vim` when `nvim` is unavailable).
387
+
130
388
  ## Prompt inspection
131
389
 
132
390
  ```bash
@@ -0,0 +1,36 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ /**
5
+ * Derive a stable, traceable agent ID from a project path.
6
+ * Uses the same convention as pi-coding-agent session directories:
7
+ * `/Users/wanqian/foo` → `--Users-wanqian-foo--`
8
+ */
9
+ export function deriveAgentId(cwd) {
10
+ return `-${resolve(cwd).replaceAll("/", "-")}--`;
11
+ }
12
+ export function getAllAgentsDir() {
13
+ return join(homedir(), ".mono-pilot", "agents");
14
+ }
15
+ export function getAgentDir(agentId) {
16
+ return join(getAllAgentsDir(), agentId);
17
+ }
18
+ export function getAgentMemoryDir(agentId) {
19
+ return join(getAgentDir(agentId), "memory");
20
+ }
21
+ export function getAgentTerminalsDir(agentId, sessionId) {
22
+ return join(getAgentDir(agentId), "terminals", sessionId);
23
+ }
24
+ export async function listAgentIds() {
25
+ const baseDir = getAllAgentsDir();
26
+ try {
27
+ const entries = await readdir(baseDir, { withFileTypes: true });
28
+ return entries
29
+ .filter((entry) => entry.isDirectory())
30
+ .map((entry) => entry.name)
31
+ .sort((a, b) => a.localeCompare(b));
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
@@ -0,0 +1,83 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { deriveAgentId, getAgentBriefDir } from "./paths.js";
4
+ import { parseFrontmatter } from "./frontmatter.js";
5
+ import { ensureDefaults } from "./defaults.js";
6
+ /** Recursively collect .md files under a directory, returning parsed body content. */
7
+ function collectMarkdownFiles(dir) {
8
+ if (!existsSync(dir))
9
+ return [];
10
+ const results = [];
11
+ function walk(currentDir) {
12
+ let entries;
13
+ try {
14
+ entries = readdirSync(currentDir, { withFileTypes: true });
15
+ }
16
+ catch {
17
+ return;
18
+ }
19
+ for (const entry of entries) {
20
+ const fullPath = join(currentDir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ walk(fullPath);
23
+ }
24
+ else if (entry.name.endsWith(".md")) {
25
+ try {
26
+ const raw = readFileSync(fullPath, "utf-8");
27
+ const { body } = parseFrontmatter(raw);
28
+ const trimmed = body.trim();
29
+ if (trimmed.length > 0) {
30
+ results.push({ relativePath: relative(dir, fullPath), body: trimmed });
31
+ }
32
+ }
33
+ catch {
34
+ // skip unreadable files
35
+ }
36
+ }
37
+ }
38
+ }
39
+ walk(dir);
40
+ return results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
41
+ }
42
+ function buildSection(heading, pathHint, files) {
43
+ if (files.length === 0)
44
+ return "";
45
+ const lines = [`## ${heading} [${pathHint}]`];
46
+ for (const f of files) {
47
+ lines.push(`### ${f.relativePath}`);
48
+ lines.push(f.body);
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ const BRIEF_GUIDELINE = "When applying this brief in responses, integrate it naturally — like a colleague who recalls shared context. " +
53
+ 'Never draw attention to the brief system itself or use phrases like "I remember that..." — just use what you know seamlessly.';
54
+ const BRIEF_CHECK = "Review this conversation for information worth storing in your brief. " +
55
+ "Update silently (no confirmation needed) via brief_write if you learned:\n" +
56
+ "- **User info**: Name, role, preferences, working style, current goals\n" +
57
+ "- **Project details**: Architecture, patterns, gotchas, dependencies, conventions\n" +
58
+ "- **Corrections**: User corrected you or clarified something important\n" +
59
+ "- **Preferences**: How they want you to behave, communicate, or approach tasks\n\n" +
60
+ 'Ask yourself: "If I started a new session tomorrow, what from this conversation would I want to remember?" ' +
61
+ "If the answer is meaningful, update the appropriate brief file(s) now.";
62
+ /**
63
+ * Read all core brief files and build the <brief> block for system prompt injection.
64
+ * Creates default template files if they don't exist yet.
65
+ */
66
+ export function buildBriefBlock(cwd) {
67
+ const agentId = deriveAgentId(cwd);
68
+ ensureDefaults(agentId);
69
+ const briefDir = getAgentBriefDir(agentId);
70
+ const sections = [];
71
+ const humanSection = buildSection("User Context", `~/.mono-pilot/agents/${agentId}/brief/human/`, collectMarkdownFiles(join(briefDir, "human")));
72
+ if (humanSection)
73
+ sections.push(humanSection);
74
+ const projectSection = buildSection("Project Context", `~/.mono-pilot/agents/${agentId}/brief/project/`, collectMarkdownFiles(join(briefDir, "project")));
75
+ if (projectSection)
76
+ sections.push(projectSection);
77
+ const tasksSection = buildSection("Current Tasks", `~/.mono-pilot/agents/${agentId}/brief/tasks/`, collectMarkdownFiles(join(briefDir, "tasks")));
78
+ if (tasksSection)
79
+ sections.push(tasksSection);
80
+ if (sections.length === 0)
81
+ return "";
82
+ return `<brief>\n${sections.join("\n\n")}\n\n${BRIEF_GUIDELINE}\n\n${BRIEF_CHECK}\n</brief>`;
83
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getAgentBriefDir } from "./paths.js";
4
+ const DEFAULTS = {
5
+ "human/identity.md": {
6
+ description: "What I know about the person I'm working with. Update when learning about their background, role, or identity.",
7
+ limit: 40,
8
+ content: "I haven't gotten to know this person yet.",
9
+ },
10
+ "human/prefs/communication.md": {
11
+ description: "How this person prefers to communicate. Update when they express language, verbosity, or style preferences.",
12
+ limit: 30,
13
+ content: "No communication preferences learned yet.",
14
+ },
15
+ "human/prefs/coding-style.md": {
16
+ description: "Coding conventions and style preferences. Update when they show patterns in naming, formatting, or tooling choices.",
17
+ limit: 30,
18
+ content: "No coding style preferences learned yet.",
19
+ },
20
+ "project/overview.md": {
21
+ description: "High-level understanding of this project. Update after exploring the codebase structure, tech stack, and architecture.",
22
+ limit: 50,
23
+ content: "I'm still getting to know this codebase.\nIf there's an AGENTS.md, CLAUDE.md, or README, I should read it early.",
24
+ },
25
+ "project/commands.md": {
26
+ description: "Build, test, lint, and run commands. Update when discovering or confirming project commands.",
27
+ limit: 30,
28
+ content: "No commands discovered yet.",
29
+ },
30
+ "project/conventions.md": {
31
+ description: "Code conventions, commit style, and recurring patterns. Update when observing how this project does things.",
32
+ limit: 40,
33
+ content: "No conventions learned yet.",
34
+ },
35
+ "project/gotchas.md": {
36
+ description: "Known pitfalls, warnings, and surprising behavior. Update when hitting unexpected issues.",
37
+ limit: 40,
38
+ content: "No gotchas discovered yet.",
39
+ },
40
+ "tasks/current.md": {
41
+ description: "Current task progress and status. Update when starting, progressing, or completing tasks.",
42
+ limit: 50,
43
+ content: "No active tasks.",
44
+ },
45
+ };
46
+ function writeDefaultFile(baseDir, relativePath, template) {
47
+ const filePath = join(baseDir, relativePath);
48
+ if (existsSync(filePath))
49
+ return;
50
+ const dir = dirname(filePath);
51
+ mkdirSync(dir, { recursive: true });
52
+ const content = `---\ndescription: ${template.description}\nlimit: ${template.limit}\n---\n${template.content}\n`;
53
+ writeFileSync(filePath, content, "utf-8");
54
+ }
55
+ export function ensureDefaults(agentId) {
56
+ const dir = getAgentBriefDir(agentId);
57
+ for (const [path, template] of Object.entries(DEFAULTS)) {
58
+ writeDefaultFile(dir, path, template);
59
+ }
60
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Parse YAML frontmatter from a brief file.
3
+ * Supports only `description` (string) and `limit` (positive integer).
4
+ */
5
+ export function parseFrontmatter(content) {
6
+ const trimmed = content.trimStart();
7
+ if (!trimmed.startsWith("---")) {
8
+ return { frontmatter: {}, body: content };
9
+ }
10
+ const endIndex = trimmed.indexOf("\n---", 3);
11
+ if (endIndex === -1) {
12
+ return { frontmatter: {}, body: content };
13
+ }
14
+ const yamlBlock = trimmed.slice(4, endIndex).trim();
15
+ const body = trimmed.slice(endIndex + 4).trimStart();
16
+ const frontmatter = {};
17
+ for (const line of yamlBlock.split("\n")) {
18
+ const colonIndex = line.indexOf(":");
19
+ if (colonIndex === -1)
20
+ continue;
21
+ const key = line.slice(0, colonIndex).trim();
22
+ const value = line.slice(colonIndex + 1).trim();
23
+ if (key === "limit") {
24
+ const num = parseInt(value, 10);
25
+ if (!isNaN(num) && num > 0)
26
+ frontmatter.limit = num;
27
+ }
28
+ else if (key === "description") {
29
+ frontmatter.description = value;
30
+ }
31
+ }
32
+ return { frontmatter, body };
33
+ }
34
+ export function serializeWithFrontmatter(frontmatter, body) {
35
+ const lines = ["---"];
36
+ if (frontmatter.description) {
37
+ lines.push(`description: ${frontmatter.description}`);
38
+ }
39
+ if (frontmatter.limit !== undefined) {
40
+ lines.push(`limit: ${frontmatter.limit}`);
41
+ }
42
+ lines.push("---");
43
+ if (body.length > 0) {
44
+ lines.push(body);
45
+ }
46
+ return lines.join("\n") + "\n";
47
+ }
48
+ export function countBodyLines(body) {
49
+ const trimmed = body.trim();
50
+ if (trimmed.length === 0)
51
+ return 0;
52
+ return trimmed.split("\n").length;
53
+ }
@@ -0,0 +1,10 @@
1
+ import { join } from "node:path";
2
+ import { getAgentDir } from "../agents-paths.js";
3
+ // Re-export agent-level paths for backward compatibility during migration
4
+ export { deriveAgentId, getAgentDir, getAllAgentsDir } from "../agents-paths.js";
5
+ export function getAgentBriefDir(agentId) {
6
+ return join(getAgentDir(agentId), "brief");
7
+ }
8
+ export function resolveBriefPath(relativePath, agentId) {
9
+ return join(getAgentBriefDir(agentId), relativePath);
10
+ }
@@ -0,0 +1,27 @@
1
+ const REFLECTION_INTERVAL = 25;
2
+ let turnCount = 0;
3
+ let pendingCompactionReflection = false;
4
+ /** Flag that a compaction just occurred — next turn will get a reflection reminder. */
5
+ export function onCompaction() {
6
+ pendingCompactionReflection = true;
7
+ }
8
+ /**
9
+ * Return a reflection reminder if triggered by compaction event
10
+ * or periodic turn interval. Compaction trigger takes priority.
11
+ */
12
+ export function getBriefReflectionReminder() {
13
+ turnCount++;
14
+ const triggered = pendingCompactionReflection || turnCount % REFLECTION_INTERVAL === 0;
15
+ if (!triggered)
16
+ return undefined;
17
+ const source = pendingCompactionReflection ? "post-compaction" : `turn ${turnCount}`;
18
+ pendingCompactionReflection = false;
19
+ return `<brief_reminder>
20
+ [${source}] Review this conversation for information worth remembering across sessions. If you learned anything important, use brief_write to update the relevant file:
21
+ - User info / preferences -> human/identity.md or human/prefs/
22
+ - Project context / architecture -> project/overview.md, project/conventions.md, etc.
23
+ - Task progress -> tasks/current.md
24
+ Ask yourself: "If I started a new session tomorrow, what from this conversation would I want to remember?"
25
+ Keep notes factual and concise. Append new info rather than overwriting existing knowledge.
26
+ </brief_reminder>`;
27
+ }
package/dist/src/cli.js CHANGED
@@ -5,6 +5,10 @@ import { dirname, resolve } from "node:path";
5
5
  import process from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  const DEFAULT_TOOLS = "ls";
8
+ const MODE_GAME = "game";
9
+ const MODE_CODING = "coding";
10
+ const MODE_FLAG = "--mono-mode";
11
+ const GAME_CHANNEL_FLAG = "--game-channel";
8
12
  const TOOL_BLACKLIST = new Set(["edit", "write", "grep", "read", "glob", "bash"]);
9
13
  function hasFlag(args, names) {
10
14
  for (let i = 0; i < args.length; i++) {
@@ -48,10 +52,11 @@ function sanitizeToolsArgs(args) {
48
52
  }
49
53
  return sanitized;
50
54
  }
51
- function resolveExtensionPath(here) {
55
+ function resolveExtensionPath(here, mode) {
56
+ const extensionFile = mode === MODE_GAME ? "mono-game" : "mono-pilot";
52
57
  const candidates = [
53
- resolve(here, "extensions", "mono-pilot.js"),
54
- resolve(here, "extensions", "mono-pilot.ts"),
58
+ resolve(here, "extensions", `${extensionFile}.js`),
59
+ resolve(here, "extensions", `${extensionFile}.ts`),
55
60
  ];
56
61
  for (const candidate of candidates) {
57
62
  if (existsSync(candidate)) {
@@ -61,10 +66,62 @@ function resolveExtensionPath(here) {
61
66
  // Fallback keeps previous behavior even if file is unexpectedly missing.
62
67
  return candidates[0];
63
68
  }
69
+ function extractMonoMode(args) {
70
+ const sanitized = [];
71
+ let mode = MODE_CODING;
72
+ for (let i = 0; i < args.length; i++) {
73
+ const arg = args[i];
74
+ if (arg === MODE_FLAG) {
75
+ const value = (args[i + 1] ?? "").trim().toLowerCase();
76
+ if (value === MODE_GAME)
77
+ mode = MODE_GAME;
78
+ i++;
79
+ continue;
80
+ }
81
+ if (arg.startsWith(`${MODE_FLAG}=`)) {
82
+ const value = arg.slice(`${MODE_FLAG}=`.length).trim().toLowerCase();
83
+ if (value === MODE_GAME)
84
+ mode = MODE_GAME;
85
+ continue;
86
+ }
87
+ sanitized.push(arg);
88
+ }
89
+ return { mode, args: sanitized };
90
+ }
91
+ function normalizeGameChannelArgs(args) {
92
+ const normalized = [];
93
+ for (let i = 0; i < args.length; i++) {
94
+ const arg = args[i];
95
+ if (arg === GAME_CHANNEL_FLAG) {
96
+ const value = args[i + 1];
97
+ if (value !== undefined) {
98
+ normalized.push(arg, value);
99
+ i++;
100
+ continue;
101
+ }
102
+ normalized.push(arg);
103
+ continue;
104
+ }
105
+ if (arg.startsWith(`${GAME_CHANNEL_FLAG}=`)) {
106
+ const value = arg.slice(`${GAME_CHANNEL_FLAG}=`.length).trim();
107
+ if (value) {
108
+ normalized.push(GAME_CHANNEL_FLAG, value);
109
+ }
110
+ else {
111
+ normalized.push(GAME_CHANNEL_FLAG);
112
+ }
113
+ continue;
114
+ }
115
+ normalized.push(arg);
116
+ }
117
+ return normalized;
118
+ }
64
119
  function buildPiArgs(userArgs) {
65
120
  const here = dirname(fileURLToPath(import.meta.url));
66
- const extensionPath = resolveExtensionPath(here);
67
- const sanitizedUserArgs = sanitizeToolsArgs(userArgs);
121
+ const normalizedArgs = normalizeGameChannelArgs(userArgs);
122
+ const modeResult = extractMonoMode(normalizedArgs);
123
+ const extensionPath = resolveExtensionPath(here, modeResult.mode);
124
+ const sanitizedUserArgs = sanitizeToolsArgs(modeResult.args);
68
125
  const args = ["--no-extensions", "--extension", extensionPath];
69
126
  if (!hasFlag(sanitizedUserArgs, ["--tools", "--no-tools"])) {
70
127
  args.push("--tools", DEFAULT_TOOLS);