mono-pilot 0.2.9 → 0.2.12

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 (158) hide show
  1. package/README.md +270 -7
  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 +60 -35
  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 +94 -50
  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/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /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,9 +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
51
- - `tools/` – tool implementations and descriptions (see `tools/README.md`)
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)
70
+ - `src/mcp/` – config loading, JSON-RPC transport, server resolution
71
+ - `src/rules/` – rule file discovery (shared by envelope and session hints)
72
+ - `src/tools/` – tool implementations and descriptions (see `src/tools/README.md`)
52
73
 
53
74
  ## Cursor-styled tools
54
75
 
@@ -69,11 +90,15 @@ The full Cursor-styled tool list exposed by the extension:
69
90
  - `Shell` – execute shell commands in the workspace
70
91
  - `Glob` – find paths by glob pattern
71
92
  - `rg` – search file content with ripgrep
93
+ - `AstGrep` – search code structure with ast-grep (`run`/`scan`, read-only)
72
94
  - `ReadFile` – read file content with pagination
73
95
  - `Delete` – delete files or directories
74
96
  - `SemanticSearch` – semantic search by intent
97
+ - `memory_search` – search indexed memory snippets
98
+ - `memory_get` – read a snippet from memory files
75
99
  - `WebSearch` – search the web with snippets
76
100
  - `WebFetch` – fetch and render web content
101
+ - `GenerateImage` – generate images via Gemini API or OpenRouter
77
102
  - `AskQuestion` – collect structured multiple-choice answers
78
103
  - `Subagent` – launch delegated subprocesses
79
104
  - `ListMcpResources` – list MCP resources from config
@@ -81,21 +106,257 @@ The full Cursor-styled tool list exposed by the extension:
81
106
  - `ListMcpTools` – discover MCP tools and schemas
82
107
  - `CallMcpTool` – invoke MCP tools by name
83
108
  - `SwitchMode` – switch interaction mode (`option + m`, cycles Plan → Ask → Agent)
84
- - `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)
85
113
 
86
114
  ## User rules
87
115
 
88
- MonoPilot can inject workspace user rules into the runtime envelope on each input (handled by `src/extensions/user-message.ts`).
116
+ MonoPilot injects user rules into the runtime envelope on each turn (handled by `src/extensions/user-message.ts`).
117
+
118
+ Rules are loaded from two locations:
89
119
 
90
- - Rules live in `.pi/rules/*.rule.txt` under the workspace root
91
- - Each file becomes one `<user_rule>` block wrapped by a `<rules>` envelope
92
- - Files are read in filename order; empty files are ignored
93
- - If no rules are present, the `<rules>` section is omitted
120
+ - `~/.pi/rules/*.rule.txt` user-level rules (applies to all projects)
121
+ - `.pi/rules/*.rule.txt` project-level rules (workspace root)
122
+
123
+ When the same filename exists in both, the project rule wins. Each file becomes one `<user_rule>` block inside a `<rules>` envelope, sorted by filename. Empty files are ignored; the envelope is omitted if no rules are found.
94
124
 
95
125
  ## MCP
96
126
 
97
127
  - The user message envelope issues a lightweight MCP server `initialize` request to collect server instructions.
98
128
  - MCP tools then progressively load and surface resources, schemas, and execution only when needed.
129
+ - MCP configs are loaded from `.pi/mcp.json` (project) and `~/.pi/mcp.json` (user); project entries take precedence on name conflicts.
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.
99
360
 
100
361
  ## Local development
101
362
 
@@ -122,6 +383,8 @@ npm run dev:continue
122
383
  npm run dev:watch:continue
123
384
  ```
124
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
+
125
388
  ## Prompt inspection
126
389
 
127
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
+ }