multi-agent-prompt 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ """multi-agent-prompt: a crash-proof, autosaving prompt editor for AI coding agent CLIs."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,91 @@
1
+ """Archive-on-clear: never actually lose a draft, just retire it.
2
+
3
+ Clearing the scratch file — via ``map clear`` or the in-editor keymap — first
4
+ copies the current content into a rolling, per-project archive, then prunes
5
+ that archive down to the most recent ``keep`` entries (by count, not age:
6
+ "keep the last N drafts" is simpler to reason about and configure than a
7
+ retention window, and doesn't depend on the clock).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import time
14
+ from pathlib import Path
15
+
16
+ from multi_agent_prompt.paths import ARCHIVE_DIRNAME
17
+
18
+ DEFAULT_ARCHIVE_KEEP = 10
19
+ KEEP_ENV_VAR = "MAP_ARCHIVE_KEEP"
20
+
21
+
22
+ def resolve_archive_keep(override: int | None = None) -> int:
23
+ """CLI flag > MAP_ARCHIVE_KEEP env var > default (10)."""
24
+ if override is not None:
25
+ return override
26
+ raw = os.environ.get(KEEP_ENV_VAR)
27
+ if raw:
28
+ try:
29
+ return int(raw)
30
+ except ValueError:
31
+ pass
32
+ return DEFAULT_ARCHIVE_KEEP
33
+
34
+
35
+ def _next_archive_path(directory: Path) -> Path:
36
+ """A path in ``directory``, guaranteed to sort after every existing entry.
37
+
38
+ The human-readable timestamp prefix is cosmetic; the nanosecond epoch
39
+ suffix is what actually guarantees both uniqueness and correct
40
+ chronological ordering. It must not be a "does this filename already
41
+ exist" counter starting from 0/1/2... — pruning deletes old, *low*
42
+ sequence numbers, so a fresh counter starting from 0 would reuse a freed
43
+ slot for a brand-new file, making a just-created draft sort as the
44
+ *oldest* entry and get immediately pruned by mistake. `time.time_ns()`
45
+ is (for this purpose) always increasing, so a collision is only ever
46
+ possible when the clock genuinely hasn't ticked between two calls —
47
+ handled by incrementing, never by restarting from a fresh, reusable base.
48
+ """
49
+ stamp = time.strftime("%Y%m%dT%H%M%S")
50
+ ns = time.time_ns()
51
+ candidate = directory / f"{stamp}-{ns}.md"
52
+ while candidate.exists():
53
+ ns += 1
54
+ candidate = directory / f"{stamp}-{ns}.md"
55
+ return candidate
56
+
57
+
58
+ def prune_archive(directory: Path, keep: int) -> list[Path]:
59
+ """Delete all but the ``keep`` most recent archived drafts. Returns what was removed."""
60
+ if not directory.is_dir() or keep < 0:
61
+ return []
62
+ files = sorted(directory.glob("*.md"))
63
+ excess = files if keep == 0 else files[:-keep]
64
+ for f in excess:
65
+ f.unlink()
66
+ return excess
67
+
68
+
69
+ def archive_and_clear(
70
+ file: Path,
71
+ keep: int = DEFAULT_ARCHIVE_KEEP,
72
+ archive: bool = True,
73
+ ) -> Path | None:
74
+ """Archive ``file``'s current content (unless empty or ``archive=False``), then truncate it.
75
+
76
+ Returns the path it archived to, or None if there was nothing to archive
77
+ (empty/missing file, or ``archive=False``).
78
+ """
79
+ content = file.read_text(encoding="utf-8") if file.exists() else ""
80
+ archived_to = None
81
+
82
+ if archive and content.strip():
83
+ adir = file.parent / ARCHIVE_DIRNAME
84
+ adir.mkdir(parents=True, exist_ok=True)
85
+ archived_to = _next_archive_path(adir)
86
+ archived_to.write_text(content, encoding="utf-8")
87
+ prune_archive(adir, keep)
88
+
89
+ file.parent.mkdir(parents=True, exist_ok=True)
90
+ file.write_text("", encoding="utf-8")
91
+ return archived_to
@@ -0,0 +1,338 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ import verkit
7
+ from rich.console import Console
8
+
9
+ from multi_agent_prompt import archive, paths
10
+ from multi_agent_prompt.editor import (
11
+ DEFAULT_CLEAR_KEY,
12
+ DEFAULT_DEBOUNCE_MS,
13
+ DEFAULT_FOLD_THRESHOLD,
14
+ DEFAULT_HELP_KEY,
15
+ DEFAULT_HISTORY_KEY,
16
+ nvim_available,
17
+ open_editor,
18
+ )
19
+
20
+ PACKAGE_NAME = "multi-agent-prompt"
21
+
22
+
23
+ def cmd_edit(args: argparse.Namespace) -> int:
24
+ file = paths.prompt_file()
25
+ if not nvim_available(args.nvim_bin):
26
+ print(
27
+ f"error: '{args.nvim_bin}' not found on PATH. "
28
+ "multi-agent-prompt's autosave relies on Neovim's Lua API "
29
+ "(https://neovim.io/) — plain Vim isn't supported.",
30
+ file=sys.stderr,
31
+ )
32
+ return 1
33
+
34
+ if paths.ensure_gitignored():
35
+ print(f"Added {paths.GITIGNORE_PATTERN} to .gitignore", file=sys.stderr)
36
+
37
+ return open_editor(
38
+ file,
39
+ debounce_ms=args.debounce,
40
+ nvim_bin=args.nvim_bin,
41
+ clean=args.clean,
42
+ clear_key=None if args.no_clear_key else args.clear_key,
43
+ history_key=None if args.no_history_key else args.history_key,
44
+ help_key=None if args.no_help_key else args.help_key,
45
+ fold_threshold=None if args.no_fold_paste else args.fold_threshold,
46
+ insert=not args.no_insert,
47
+ )
48
+
49
+
50
+ def cmd_where(args: argparse.Namespace) -> int:
51
+ print(paths.prompt_file())
52
+ return 0
53
+
54
+
55
+ def cmd_show(args: argparse.Namespace) -> int:
56
+ file = paths.prompt_file()
57
+ if not file.exists():
58
+ return 0
59
+ sys.stdout.write(file.read_text(encoding="utf-8"))
60
+ return 0
61
+
62
+
63
+ def cmd_clear(args: argparse.Namespace) -> int:
64
+ file = paths.prompt_file()
65
+ keep = archive.resolve_archive_keep(args.keep)
66
+ archived_to = archive.archive_and_clear(file, keep=keep, archive=not args.no_archive)
67
+
68
+ if archived_to is not None:
69
+ print(f"Archived previous draft to {archived_to}", file=sys.stderr)
70
+ print(f"Cleared {file}", file=sys.stderr)
71
+ return 0
72
+
73
+
74
+ def cmd_pop(args: argparse.Namespace) -> int:
75
+ """Read the draft, archive it, clear the file, and print what was read — one operation.
76
+
77
+ This is what `/prompt` uses: hand the draft off and reset for the next
78
+ one in a single step, so there's no separate "now go clear it" the user
79
+ has to remember to do.
80
+ """
81
+ file = paths.prompt_file()
82
+ content = file.read_text(encoding="utf-8") if file.exists() else ""
83
+ keep = archive.resolve_archive_keep(args.keep)
84
+ archive.archive_and_clear(file, keep=keep, archive=not args.no_archive)
85
+ sys.stdout.write(content)
86
+ return 0
87
+
88
+
89
+ def cmd_version(args: argparse.Namespace) -> int:
90
+ """Show version, promote it, tag it, or run a full release — via verkit.
91
+
92
+ Mirrors task-agent's `ta version` surface (verkit is the shared
93
+ version-management library across this developer's projects): bare
94
+ `map version` shows installed vs. latest-on-PyPI; `promote` bumps
95
+ semver and commits; `tag` tags HEAD from the committed version and
96
+ pushes; `release` does both atomically.
97
+ """
98
+ console = Console()
99
+ verkit.display_version_info(console, PACKAGE_NAME)
100
+
101
+ try:
102
+ if args.version_command == "release":
103
+ new_v = verkit.promote_version(args.part, console=console)
104
+ console.print(f"[blue]Release {new_v}: tagging and pushing...[/blue]")
105
+ verkit.tag_version(console=console, push=args.push, push_branch=args.push_branch)
106
+ console.print(f"[bold green]Release v{new_v} complete.[/bold green]")
107
+ elif args.version_command == "promote":
108
+ new_v = verkit.promote_version(args.part, console=console)
109
+ console.print(
110
+ "[dim]Next: map version tag[/dim] "
111
+ "[dim](or use map version release for one-shot)[/dim]"
112
+ )
113
+ console.print(f"[dim]HEAD is now v{new_v}; tag when ready to publish.[/dim]")
114
+ elif args.version_command == "tag":
115
+ verkit.tag_version(console=console, push=args.push, push_branch=args.push_branch)
116
+ except SystemExit:
117
+ raise
118
+ except Exception as e: # noqa: BLE001 - deliberate top-level boundary for verkit's git/subprocess errors
119
+ console.print(f"[red]Error during version operation: {e}[/red]")
120
+ return 1
121
+
122
+ return 0
123
+
124
+
125
+ def build_parser() -> argparse.ArgumentParser:
126
+ parser = argparse.ArgumentParser(
127
+ prog="map",
128
+ description=(
129
+ "A crash-proof, autosaving prompt editor for AI coding agent CLIs. "
130
+ "Compose in your real Neovim in a split pane; hand off to the chat "
131
+ "with /prompt or @prompt.md."
132
+ ),
133
+ )
134
+ parser.add_argument(
135
+ "-V", "--version", action="store_true", help="Show version and exit"
136
+ )
137
+ subparsers = parser.add_subparsers(dest="command")
138
+
139
+ edit_parser = subparsers.add_parser(
140
+ "edit", help="Open the project's scratch prompt file (default command)"
141
+ )
142
+ edit_parser.add_argument(
143
+ "--debounce",
144
+ type=int,
145
+ default=DEFAULT_DEBOUNCE_MS,
146
+ help=f"Autosave debounce in milliseconds (default: {DEFAULT_DEBOUNCE_MS})",
147
+ )
148
+ edit_parser.add_argument(
149
+ "--nvim-bin",
150
+ default="nvim",
151
+ help="Neovim binary to launch (default: nvim)",
152
+ )
153
+ edit_parser.add_argument(
154
+ "--clean",
155
+ action="store_true",
156
+ help="Skip the user's init.lua entirely (-u NONE) for faster startup",
157
+ )
158
+ edit_parser.add_argument(
159
+ "--clear-key",
160
+ default=DEFAULT_CLEAR_KEY,
161
+ help=f"Buffer-local normal-mode keymap that archives and clears (default: {DEFAULT_CLEAR_KEY})",
162
+ )
163
+ edit_parser.add_argument(
164
+ "--no-clear-key",
165
+ action="store_true",
166
+ help="Don't register the in-editor clear keymap at all",
167
+ )
168
+ edit_parser.add_argument(
169
+ "--history-key",
170
+ default=DEFAULT_HISTORY_KEY,
171
+ help=(
172
+ "Buffer-local normal-mode keymap that opens the archive directory "
173
+ f"(read-only) in a split (default: {DEFAULT_HISTORY_KEY})"
174
+ ),
175
+ )
176
+ edit_parser.add_argument(
177
+ "--no-history-key",
178
+ action="store_true",
179
+ help="Don't register the in-editor archive-browsing keymap at all",
180
+ )
181
+ edit_parser.add_argument(
182
+ "--help-key",
183
+ default=DEFAULT_HELP_KEY,
184
+ help=(
185
+ "Buffer-local normal-mode keymap that shows a cheatsheet of "
186
+ f"whichever keymaps are active (default: {DEFAULT_HELP_KEY})"
187
+ ),
188
+ )
189
+ edit_parser.add_argument(
190
+ "--no-help-key",
191
+ action="store_true",
192
+ help="Don't register the in-editor help keymap at all",
193
+ )
194
+ edit_parser.add_argument(
195
+ "--fold-threshold",
196
+ type=int,
197
+ default=DEFAULT_FOLD_THRESHOLD,
198
+ help=(
199
+ "Auto-fold a paste of this many lines or more, closed by default "
200
+ f"(za/zo/zc to toggle) (default: {DEFAULT_FOLD_THRESHOLD})"
201
+ ),
202
+ )
203
+ edit_parser.add_argument(
204
+ "--no-fold-paste",
205
+ action="store_true",
206
+ help="Don't auto-fold large pastes at all",
207
+ )
208
+ edit_parser.add_argument(
209
+ "--no-insert",
210
+ action="store_true",
211
+ help="Open in normal mode instead of dropping straight into insert mode",
212
+ )
213
+ edit_parser.set_defaults(func=cmd_edit)
214
+
215
+ where_parser = subparsers.add_parser(
216
+ "where", help="Print the resolved scratch prompt file path"
217
+ )
218
+ where_parser.set_defaults(func=cmd_where)
219
+
220
+ show_parser = subparsers.add_parser(
221
+ "show", help="Print the scratch prompt file's current content"
222
+ )
223
+ show_parser.set_defaults(func=cmd_show)
224
+
225
+ pop_parser = subparsers.add_parser(
226
+ "pop",
227
+ help="Print the current draft, archive it, and clear the file — one operation",
228
+ )
229
+ pop_parser.add_argument(
230
+ "--keep",
231
+ type=int,
232
+ default=None,
233
+ help=(
234
+ f"How many archived drafts to retain (default: {archive.DEFAULT_ARCHIVE_KEEP}, "
235
+ f"or ${archive.KEEP_ENV_VAR})"
236
+ ),
237
+ )
238
+ pop_parser.add_argument(
239
+ "--no-archive",
240
+ action="store_true",
241
+ help="Discard the draft instead of archiving it (e.g. it contained a secret)",
242
+ )
243
+ pop_parser.set_defaults(func=cmd_pop)
244
+
245
+ clear_parser = subparsers.add_parser(
246
+ "clear", help="Archive the current draft, then empty the scratch prompt file"
247
+ )
248
+ clear_parser.add_argument(
249
+ "--keep",
250
+ type=int,
251
+ default=None,
252
+ help=(
253
+ f"How many archived drafts to retain (default: {archive.DEFAULT_ARCHIVE_KEEP}, "
254
+ f"or ${archive.KEEP_ENV_VAR})"
255
+ ),
256
+ )
257
+ clear_parser.add_argument(
258
+ "--no-archive",
259
+ action="store_true",
260
+ help="Discard the current draft instead of archiving it first (e.g. it contained a secret)",
261
+ )
262
+ clear_parser.set_defaults(func=cmd_clear)
263
+
264
+ version_parser = subparsers.add_parser(
265
+ "version", help="Show version, promote, tag, or run a full release"
266
+ )
267
+ v_sub = version_parser.add_subparsers(dest="version_command")
268
+ promote_parser = v_sub.add_parser(
269
+ "promote",
270
+ help=(
271
+ "Bump semver and commit it (amends only if HEAD is unpushed/untagged; "
272
+ "otherwise creates chore(release): vX.Y.Z)"
273
+ ),
274
+ )
275
+ promote_parser.add_argument("part", choices=["major", "minor", "patch"])
276
+ version_tag_parser = v_sub.add_parser(
277
+ "tag",
278
+ help="Tag HEAD as vX.Y.Z from committed version; push branch then tag",
279
+ )
280
+ version_tag_parser.add_argument(
281
+ "--no-push",
282
+ dest="push",
283
+ action="store_false",
284
+ help="Create the local tag only (do not push branch or tag)",
285
+ )
286
+ version_tag_parser.add_argument(
287
+ "--no-push-branch",
288
+ dest="push_branch",
289
+ action="store_false",
290
+ help="When pushing, push only the tag (not the branch)",
291
+ )
292
+ version_tag_parser.set_defaults(push=True, push_branch=True)
293
+ version_release_parser = v_sub.add_parser(
294
+ "release", help="Atomic promote + tag + push branch + push tag"
295
+ )
296
+ version_release_parser.add_argument("part", choices=["major", "minor", "patch"])
297
+ version_release_parser.add_argument(
298
+ "--no-push",
299
+ dest="push",
300
+ action="store_false",
301
+ help="Promote and tag locally only",
302
+ )
303
+ version_release_parser.set_defaults(push=True, push_branch=True)
304
+ version_parser.set_defaults(func=cmd_version)
305
+
306
+ return parser
307
+
308
+
309
+ def main(argv: list[str] | None = None) -> None:
310
+ parser = build_parser()
311
+ args = parser.parse_args(argv)
312
+
313
+ if args.version:
314
+ verkit.display_version_info(Console(), PACKAGE_NAME)
315
+ sys.exit(0)
316
+
317
+ if args.command is None:
318
+ # Bare `map` == `map edit` with defaults.
319
+ args.command = "edit"
320
+ args.debounce = DEFAULT_DEBOUNCE_MS
321
+ args.nvim_bin = "nvim"
322
+ args.clean = False
323
+ args.clear_key = DEFAULT_CLEAR_KEY
324
+ args.no_clear_key = False
325
+ args.history_key = DEFAULT_HISTORY_KEY
326
+ args.no_history_key = False
327
+ args.help_key = DEFAULT_HELP_KEY
328
+ args.no_help_key = False
329
+ args.fold_threshold = DEFAULT_FOLD_THRESHOLD
330
+ args.no_fold_paste = False
331
+ args.no_insert = False
332
+ args.func = cmd_edit
333
+
334
+ sys.exit(args.func(args))
335
+
336
+
337
+ if __name__ == "__main__":
338
+ main()
@@ -0,0 +1,315 @@
1
+ """Launch the user's real Neovim on the scratch prompt file, with autosave bolted on.
2
+
3
+ This deliberately does not touch the user's ``init.lua`` or try to
4
+ reimplement Vim: it loads their actual config as-is (so keymaps, colorscheme,
5
+ and plugins "just work" without any import step) and layers small additions
6
+ on top via ``-c``. Each ``map`` invocation is its own throwaway nvim
7
+ process, so none of this can ever reach the user's other, real nvim
8
+ sessions — but not everything below is *buffer*-local within this one
9
+ session; see fold-on-paste.
10
+
11
+ * Autosave, debounced (``debounce_ms`` after the last edit) — the everyday
12
+ case — and immediately on ``FocusLost``/``BufLeave``, so switching to the
13
+ chat pane to hand the prompt off never races the debounce timer.
14
+ * Auto-reload on ``FocusGained``/``BufEnter``: the counterpart to the
15
+ above. Handing a draft off (e.g. via `/prompt`, which runs `map pop`
16
+ from the *agent's* process, not this nvim) clears the file from outside
17
+ this editor session — without this, the buffer you're still looking at
18
+ would keep showing the old, already-submitted text until you touched it.
19
+ * A ``clear_key`` mapping that archives the current draft (via ``map
20
+ clear``, so the CLI and the in-editor keymap share one implementation)
21
+ and reloads the now-empty buffer.
22
+ * A ``history_key`` mapping that lists archived drafts (newest first) in a
23
+ scratch split — ``<CR>`` opens one, read-only. Deliberately not
24
+ `:vsplit <archive_dir>` / netrw: netrw's directory browsing isn't
25
+ available at all under ``-u NONE`` (confirmed directly — even
26
+ `:Explore` is "not an editor command" there), and under a full config
27
+ it may be intercepted by whatever directory-handling plugin (neo-tree,
28
+ oil.nvim, ...) the user has, which is inconsistent with ``--clean``.
29
+ This tiny listing works identically either way.
30
+ * Fold-on-paste: pasting ``fold_threshold`` lines or more (bracketed
31
+ paste, `"+p`, `"*p`, ...) automatically folds (and closes) just those
32
+ lines, via Neovim's ``vim.paste()`` override — so dropping a long CLI
33
+ transcript or diff into the buffer doesn't bury the rest of what you're
34
+ writing. Standard Vim fold commands (``za``/``zo``/``zc``) toggle it;
35
+ nothing new to bind for that part. ``vim.paste`` is a *global* hook, not
36
+ a buffer-local option — this does apply to any other buffer opened
37
+ within this same throwaway session (e.g. one opened via the history
38
+ keymap), which is fine here since every buffer in this session is
39
+ another scratch markdown file.
40
+
41
+ The launched nvim gets ``MAP_SESSION=1`` in its environment — nothing in
42
+ this package reads it, but it's there for a user who wants to guard an
43
+ expensive line in their own init.lua (e.g. a `clipboard=unnamedplus` setting
44
+ that's slow specifically under WSL) behind ``if not vim.env.MAP_SESSION``,
45
+ without multi-agent-prompt ever touching that file itself.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import os
51
+ import shutil
52
+ import subprocess
53
+ from pathlib import Path
54
+
55
+ from multi_agent_prompt.paths import ARCHIVE_DIRNAME
56
+
57
+ DEFAULT_DEBOUNCE_MS = 2000
58
+ DEFAULT_NVIM_BIN = "nvim"
59
+ DEFAULT_CLEAR_KEY = "<leader>pc"
60
+ DEFAULT_HISTORY_KEY = "<leader>ph"
61
+ DEFAULT_HELP_KEY = "g?"
62
+ DEFAULT_FOLD_THRESHOLD = 6
63
+
64
+
65
+ def build_autosave_lua(
66
+ debounce_ms: int = DEFAULT_DEBOUNCE_MS,
67
+ clear_key: str | None = DEFAULT_CLEAR_KEY,
68
+ history_dir: Path | None = None,
69
+ history_key: str | None = DEFAULT_HISTORY_KEY,
70
+ help_key: str | None = DEFAULT_HELP_KEY,
71
+ fold_threshold: int | None = DEFAULT_FOLD_THRESHOLD,
72
+ ) -> str:
73
+ """Lua snippet, scoped to the current buffer only.
74
+
75
+ Always: autosave (debounced + on focus-lost) and auto-reload on
76
+ focus-gained. Optionally: the clear keymap, the archive-browsing keymap
77
+ (when ``history_dir`` is given), fold-on-paste (when ``fold_threshold``
78
+ is a positive int), and a ``g?`` cheatsheet listing whichever of those
79
+ are actually active — reflecting real overrides, not just the defaults,
80
+ since it's built from the same values passed in here.
81
+ """
82
+ lua = f"""
83
+ local uv = vim.uv or vim.loop
84
+ local timer = uv.new_timer()
85
+ local function save_now()
86
+ if vim.bo.modified then
87
+ vim.cmd("silent! write")
88
+ end
89
+ end
90
+ vim.api.nvim_create_autocmd({{"TextChanged", "TextChangedI"}}, {{
91
+ buffer = 0,
92
+ callback = function()
93
+ timer:stop()
94
+ timer:start({debounce_ms}, 0, vim.schedule_wrap(save_now))
95
+ end,
96
+ }})
97
+ vim.api.nvim_create_autocmd({{"FocusLost", "BufLeave"}}, {{
98
+ buffer = 0,
99
+ callback = function()
100
+ timer:stop()
101
+ save_now()
102
+ end,
103
+ }})
104
+ vim.opt.autoread = true
105
+ vim.api.nvim_create_autocmd({{"FocusGained", "BufEnter"}}, {{
106
+ buffer = 0,
107
+ callback = function()
108
+ -- Reloads from disk only if this buffer has no unsaved changes of its
109
+ -- own, so a draft in progress here is never silently discarded.
110
+ vim.cmd("silent! checktime")
111
+ end,
112
+ }})
113
+ """.strip()
114
+
115
+ if clear_key:
116
+ lua += f"""
117
+ vim.keymap.set("n", "{clear_key}", function()
118
+ timer:stop()
119
+ -- Deliberately not `silent!`: if the write fails, let the error abort
120
+ -- here rather than silently archiving/truncating stale disk content.
121
+ vim.cmd("silent write")
122
+ vim.fn.system("map clear")
123
+ vim.cmd("edit!")
124
+ end, {{ buffer = 0, desc = "multi-agent-prompt: archive and clear" }})
125
+ """.rstrip()
126
+
127
+ if history_dir and history_key:
128
+ hdir = str(history_dir)
129
+ # A self-contained scratch-buffer listing, not `:vsplit <dir>` / netrw:
130
+ # netrw's directory browsing isn't available under -u NONE at all
131
+ # (confirmed directly — even `:Explore` is "not an editor command"
132
+ # there), and under a full config it may be intercepted by whatever
133
+ # directory-handling plugin (neo-tree, oil.nvim, ...) the user has —
134
+ # fine on its own, but inconsistent with --clean. This works
135
+ # identically either way.
136
+ lua += f"""
137
+ vim.api.nvim_create_autocmd("BufReadPost", {{
138
+ pattern = "{hdir}/*",
139
+ callback = function()
140
+ vim.bo.readonly = true
141
+ vim.bo.modifiable = false
142
+ end,
143
+ desc = "multi-agent-prompt: archived drafts are read-only",
144
+ }})
145
+ vim.keymap.set("n", "{history_key}", function()
146
+ local dir = "{hdir}"
147
+ local ok, files = pcall(vim.fn.readdir, dir)
148
+ files = (ok and files) or {{}}
149
+ table.sort(files, function(a, b) return a > b end) -- filenames sort newest-first
150
+ if #files == 0 then
151
+ vim.notify("multi-agent-prompt: no archived drafts yet", vim.log.levels.INFO)
152
+ return
153
+ end
154
+ vim.cmd("vsplit")
155
+ local buf = vim.api.nvim_create_buf(false, true)
156
+ vim.api.nvim_win_set_buf(0, buf)
157
+ vim.bo[buf].buftype = "nofile"
158
+ vim.bo[buf].bufhidden = "wipe"
159
+ vim.bo[buf].filetype = "map-history"
160
+ vim.api.nvim_buf_set_lines(buf, 0, -1, false, files)
161
+ vim.bo[buf].modifiable = false
162
+ vim.keymap.set("n", "<CR>", function()
163
+ local line = vim.api.nvim_get_current_line()
164
+ if line ~= "" then
165
+ vim.cmd("edit " .. vim.fn.fnameescape(dir .. "/" .. line))
166
+ end
167
+ end, {{ buffer = buf, desc = "multi-agent-prompt: open archived draft" }})
168
+ end, {{ buffer = 0, desc = "multi-agent-prompt: browse archived prompts" }})
169
+ """.rstrip()
170
+
171
+ if fold_threshold and fold_threshold > 0:
172
+ # vim.paste() is Neovim's single hook for every paste source
173
+ # (bracketed/terminal paste, "+p, "*p, ...), called with the
174
+ # complete line list for a small paste (phase -1) or streamed in
175
+ # chunks for a large one (phase 1/2/3). Track cursor position across
176
+ # the call rather than trust `#lines` alone, since a chunked paste's
177
+ # individual calls don't each carry the full line count.
178
+ lua += f"""
179
+ local map_orig_paste = vim.paste
180
+ local map_paste_start = nil
181
+ vim.paste = function(lines, phase)
182
+ if phase == 1 or phase == -1 then
183
+ map_paste_start = vim.api.nvim_win_get_cursor(0)[1]
184
+ end
185
+ local ok = map_orig_paste(lines, phase)
186
+ if (phase == -1 or phase == 3) and map_paste_start then
187
+ local paste_end = vim.api.nvim_win_get_cursor(0)[1]
188
+ if paste_end - map_paste_start >= {fold_threshold} then
189
+ vim.cmd(string.format("%d,%dfold", map_paste_start, paste_end))
190
+ end
191
+ map_paste_start = nil
192
+ end
193
+ return ok
194
+ end
195
+ """.rstrip()
196
+
197
+ if help_key:
198
+ entries = []
199
+ if clear_key:
200
+ entries.append((clear_key, "archive current draft & clear"))
201
+ if history_dir and history_key:
202
+ entries.append((history_key, "browse archived drafts"))
203
+ if fold_threshold and fold_threshold > 0:
204
+ entries.append(("za/zo/zc", f"toggle fold ({fold_threshold}+ line pastes auto-fold)"))
205
+ entries.append((help_key, "show this help"))
206
+ width = max(len(k) for k, _ in entries)
207
+ help_lines = ", ".join(
208
+ f'" {k.ljust(width)} {desc}"' for k, desc in entries
209
+ )
210
+ lua += f"""
211
+ vim.keymap.set("n", "{help_key}", function()
212
+ local lines = {{ " multi-agent-prompt ", "", {help_lines} }}
213
+ local width = 0
214
+ for _, l in ipairs(lines) do
215
+ width = math.max(width, vim.fn.strdisplaywidth(l))
216
+ end
217
+ local buf = vim.api.nvim_create_buf(false, true)
218
+ vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
219
+ vim.bo[buf].modifiable = false
220
+ vim.bo[buf].bufhidden = "wipe"
221
+ local win = vim.api.nvim_open_win(buf, true, {{
222
+ relative = "cursor",
223
+ row = 1,
224
+ col = 0,
225
+ width = width + 2,
226
+ height = #lines,
227
+ style = "minimal",
228
+ border = "rounded",
229
+ }})
230
+ local close = function()
231
+ pcall(vim.api.nvim_win_close, win, true)
232
+ end
233
+ vim.keymap.set("n", "q", close, {{ buffer = buf, nowait = true }})
234
+ vim.keymap.set("n", "<Esc>", close, {{ buffer = buf, nowait = true }})
235
+ end, {{ buffer = 0, desc = "multi-agent-prompt: show keymap help" }})
236
+ """.rstrip()
237
+
238
+ return lua
239
+
240
+
241
+ def build_nvim_command(
242
+ file: Path,
243
+ debounce_ms: int = DEFAULT_DEBOUNCE_MS,
244
+ nvim_bin: str = DEFAULT_NVIM_BIN,
245
+ clean: bool = False,
246
+ clear_key: str | None = DEFAULT_CLEAR_KEY,
247
+ history_key: str | None = DEFAULT_HISTORY_KEY,
248
+ help_key: str | None = DEFAULT_HELP_KEY,
249
+ fold_threshold: int | None = DEFAULT_FOLD_THRESHOLD,
250
+ insert: bool = True,
251
+ ) -> list[str]:
252
+ """The argv to launch nvim on ``file`` with autosave (and the clear/history/help/fold keymaps) enabled.
253
+
254
+ ``clean=True`` passes ``-u NONE``, skipping the user's init.lua (and every
255
+ plugin it loads) entirely — a fast, minimal mode for when startup latency
256
+ from a large distro (LSP servers, treesitter, etc.) matters more than
257
+ having every keymap and plugin available for a quick one-file edit.
258
+
259
+ ``insert=True`` (default) drops straight into append-mode insert at the
260
+ end of the buffer — a scratch prompt file is written far more than it's
261
+ navigated, so requiring an `i`/`a`/`o` before typing is pure friction.
262
+ """
263
+ history_dir = file.parent / ARCHIVE_DIRNAME
264
+ lua = build_autosave_lua(
265
+ debounce_ms,
266
+ clear_key=clear_key,
267
+ history_dir=history_dir,
268
+ history_key=history_key,
269
+ help_key=help_key,
270
+ fold_threshold=fold_threshold,
271
+ )
272
+ argv = [nvim_bin]
273
+ if clean:
274
+ argv.append("-u")
275
+ argv.append("NONE")
276
+ argv += ["-c", f"lua {lua}"]
277
+ if insert:
278
+ argv += ["-c", "normal! G", "-c", "startinsert!"]
279
+ argv += [str(file)]
280
+ return argv
281
+
282
+
283
+ def nvim_available(nvim_bin: str = DEFAULT_NVIM_BIN) -> bool:
284
+ return shutil.which(nvim_bin) is not None
285
+
286
+
287
+ def open_editor(
288
+ file: Path,
289
+ debounce_ms: int = DEFAULT_DEBOUNCE_MS,
290
+ nvim_bin: str = DEFAULT_NVIM_BIN,
291
+ clean: bool = False,
292
+ clear_key: str | None = DEFAULT_CLEAR_KEY,
293
+ history_key: str | None = DEFAULT_HISTORY_KEY,
294
+ help_key: str | None = DEFAULT_HELP_KEY,
295
+ fold_threshold: int | None = DEFAULT_FOLD_THRESHOLD,
296
+ insert: bool = True,
297
+ ) -> int:
298
+ """Open ``file`` in nvim (inheriting the terminal) with autosave. Returns nvim's exit code."""
299
+ file.parent.mkdir(parents=True, exist_ok=True)
300
+ if not file.exists():
301
+ file.touch()
302
+ argv = build_nvim_command(
303
+ file,
304
+ debounce_ms=debounce_ms,
305
+ nvim_bin=nvim_bin,
306
+ clean=clean,
307
+ clear_key=clear_key,
308
+ history_key=history_key,
309
+ help_key=help_key,
310
+ fold_threshold=fold_threshold,
311
+ insert=insert,
312
+ )
313
+ env = {**os.environ, "MAP_SESSION": "1"}
314
+ result = subprocess.run(argv, check=False, env=env)
315
+ return result.returncode
@@ -0,0 +1,99 @@
1
+ """Resolve where a project's scratch prompt file (and its archive) live.
2
+
3
+ State lives under ``.ma/prompt/`` — ``.ma`` is the shared root directory for
4
+ the whole multi-agent-* product line (``map``'s CLI alias is itself the
5
+ shared ``ma`` prefix of ``map``/``mar``/``maa``), with each product getting
6
+ its own subdirectory so a project never accumulates a `.map/`, `.mar/`,
7
+ `.maa/`... pile of top-level dotfolders. Only this tool's own subdirectory
8
+ (``.ma/prompt/``) is touched here; task-agent's separate, already-shipped
9
+ ``.task-agent/`` convention is out of scope for this rename.
10
+
11
+ Resolution walks up from the start directory to the nearest ``.git`` root
12
+ (matching how most agent CLIs and task-agent itself scope their own config),
13
+ falling back to the start directory itself when no repo is found.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+
20
+ MA_DIRNAME = ".ma"
21
+ PRODUCT_DIRNAME = "prompt"
22
+ PROMPT_FILENAME = "current.md"
23
+ ARCHIVE_DIRNAME = "archive"
24
+
25
+ # Anything that already covers `.ma/prompt/` — including a broader existing
26
+ # `.ma/` entry a future sibling product's setup might have added — counts as
27
+ # "already ignored".
28
+ _COVERING_GITIGNORE_LINES = frozenset(
29
+ {
30
+ f"{MA_DIRNAME}/{PRODUCT_DIRNAME}/",
31
+ f"{MA_DIRNAME}/{PRODUCT_DIRNAME}",
32
+ f"/{MA_DIRNAME}/{PRODUCT_DIRNAME}/",
33
+ f"/{MA_DIRNAME}/{PRODUCT_DIRNAME}",
34
+ f"{MA_DIRNAME}/",
35
+ MA_DIRNAME,
36
+ f"/{MA_DIRNAME}/",
37
+ f"/{MA_DIRNAME}",
38
+ }
39
+ )
40
+ GITIGNORE_PATTERN = f"{MA_DIRNAME}/{PRODUCT_DIRNAME}/"
41
+
42
+
43
+ def find_repo_root(start: Path) -> Path:
44
+ """Walk up from ``start`` to the nearest directory containing ``.git``.
45
+
46
+ Returns ``start`` (resolved) if no ``.git`` is found before the
47
+ filesystem root.
48
+ """
49
+ current = start.resolve()
50
+ while True:
51
+ if (current / ".git").exists():
52
+ return current
53
+ parent = current.parent
54
+ if parent == current:
55
+ return start.resolve()
56
+ current = parent
57
+
58
+
59
+ def prompt_dir(start: Path | None = None) -> Path:
60
+ """The ``.ma/prompt`` directory for the project containing ``start`` (default cwd)."""
61
+ root = find_repo_root(start or Path.cwd())
62
+ return root / MA_DIRNAME / PRODUCT_DIRNAME
63
+
64
+
65
+ def prompt_file(start: Path | None = None) -> Path:
66
+ """The scratch prompt file path for the project containing ``start``."""
67
+ return prompt_dir(start) / PROMPT_FILENAME
68
+
69
+
70
+ def archive_dir(start: Path | None = None) -> Path:
71
+ """Where previously-cleared drafts for this project are kept."""
72
+ return prompt_dir(start) / ARCHIVE_DIRNAME
73
+
74
+
75
+ def ensure_gitignored(start: Path | None = None) -> bool:
76
+ """Append ``.ma/prompt/`` to the project's ``.gitignore`` if not already covered.
77
+
78
+ No-ops (returns False) when the project has no ``.git`` directory at all
79
+ (nothing to ignore against), or an existing line already covers it —
80
+ either the exact pattern or a broader ``.ma/`` entry. Returns True when
81
+ it actually modified (or created) ``.gitignore``.
82
+ """
83
+ root = find_repo_root(start or Path.cwd())
84
+ if not (root / ".git").exists():
85
+ return False
86
+
87
+ gitignore = root / ".gitignore"
88
+ existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
89
+ lines = existing.splitlines()
90
+
91
+ for line in lines:
92
+ if line.strip() in _COVERING_GITIGNORE_LINES:
93
+ return False
94
+
95
+ with gitignore.open("a", encoding="utf-8") as f:
96
+ if existing and not existing.endswith("\n"):
97
+ f.write("\n")
98
+ f.write(f"{GITIGNORE_PATTERN}\n")
99
+ return True
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.5
2
+ Name: multi-agent-prompt
3
+ Version: 0.1.0
4
+ Summary: A crash-proof, autosaving prompt editor for AI coding agent CLIs — compose in your real editor, hand off with a slash command
5
+ Author-email: Mark Stouffer <1802850+InTEGr8or@users.noreply.github.com>
6
+ License: MIT
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: rich>=13.9.4
9
+ Requires-Dist: verkit>=0.1.4
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Multi-Agent Prompt ✍️
13
+
14
+ A crash-proof, autosaving prompt editor for AI coding agent CLIs.
15
+
16
+ Chat text boxes lose long, carefully-composed prompts to a single wrong
17
+ keystroke, and give you almost no editing power while you write. `multi-agent-prompt`
18
+ moves composition out of the chat box entirely: write in a real Neovim, in a
19
+ split pane next to your agent session, autosaved to disk every couple of
20
+ seconds so a crash or a fat-fingered shortcut can never cost you more than a
21
+ moment of typing. When you're ready, hand it to the agent with `/prompt` —
22
+ which reads it, archives it, and clears the file in one step, so there's no
23
+ separate "now go clear it" to remember.
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install multi-agent-prompt
31
+ # or
32
+ uv tool install multi-agent-prompt
33
+ ```
34
+
35
+ Requires [Neovim](https://neovim.io/) — the autosave hook is Lua, so plain
36
+ Vim isn't supported.
37
+
38
+ ## Usage
39
+
40
+ Open a split pane next to your agent CLI (Windows Terminal: `Alt+Shift+-`;
41
+ tmux: `<prefix> "`) in the **same working directory**, then run:
42
+
43
+ ```bash
44
+ map
45
+ ```
46
+
47
+ This opens `.ma/prompt/current.md` — resolved to the current project's git
48
+ root, so every pane in the same repo shares one scratch file — in your actual
49
+ `nvim`, your actual `init.lua`, your actual keymaps and plugins. No config
50
+ import step, no reimplemented Vim subset: it's just Neovim. You land in
51
+ **insert mode already**, cursor at the end of the buffer, so there's no `i`
52
+ to press before you start typing (`--no-insert` if you'd rather open in
53
+ normal mode). A buffer-local autocmd (scoped only to this one buffer — it
54
+ never touches how you edit anything else) autosaves ~2 seconds after you stop
55
+ typing, and immediately when you switch away from the pane
56
+ (`FocusLost`/`BufLeave`), so the save always beats you to the chat window.
57
+
58
+ When you're done, switch back to the agent pane and either:
59
+
60
+ - Type `/prompt` (if the agent's install includes the skill — see below) —
61
+ reads the draft, archives it, and clears the file, all in one step, or
62
+ - Reference `@prompt.md` / `@.ma/prompt/current.md` directly, if your agent
63
+ supports file mentions and can see gitignored files — this is a plain
64
+ read, so unlike `/prompt` it doesn't archive or clear anything.
65
+
66
+ Pasting a long CLI transcript or diff in? Anything **6 lines or more**
67
+ (bracketed paste, `"+p`, `"*p` — any paste source) auto-folds, closed, so it
68
+ doesn't bury the rest of what you're writing — `za`/`zo`/`zc` (standard Vim)
69
+ toggle it open.
70
+
71
+ Also want to browse past drafts without leaving the editor? A buffer-local
72
+ keymap — `<leader>ph` by default — lists archived drafts (newest first) in a
73
+ split; `<CR>` opens one, read-only. Forgot what any of this is bound to?
74
+ Press `g?` — it shows exactly the keymaps active for *this* session
75
+ (respecting any `--clear-key`/`--history-key` overrides, and omitting
76
+ whichever ones you disabled):
77
+
78
+ ```
79
+ map # open the scratch file (same as `map edit`)
80
+ map where # print the resolved file path
81
+ map show # print its current content to stdout (does not clear it)
82
+ map pop # print it, archive it, and clear it — what /prompt uses
83
+ map clear # archive the current draft, then empty it, without printing it
84
+ map --clean # skip your init.lua entirely (-u NONE) for faster startup
85
+ ```
86
+
87
+ ```
88
+ map edit --clear-key '<F5>' # rebind the in-editor clear keymap
89
+ map edit --no-clear-key # don't register it at all
90
+ map edit --history-key '<F6>' # rebind the in-editor history-browse keymap
91
+ map edit --no-history-key # don't register it at all
92
+ map edit --help-key '<F1>' # rebind the in-editor g? cheatsheet keymap
93
+ map edit --no-help-key # don't register it at all
94
+ map edit --fold-threshold 3 # auto-fold pastes of 3+ lines instead of 6
95
+ map edit --no-fold-paste # don't auto-fold pastes at all
96
+ map edit --no-insert # open in normal mode instead of insert mode
97
+ map clear --keep 20 # override how many archived drafts to retain
98
+ map clear --no-archive # discard instead of archiving (e.g. it had a secret in it)
99
+ ```
100
+
101
+ `map pop` accepts the same `--keep`/`--no-archive` as `map clear`.
102
+
103
+ ### The archive
104
+
105
+ `map pop`/`map clear` — from the CLI or the in-editor keymap, which shells
106
+ out to `map clear` — never actually discard a non-empty draft; they move it
107
+ to `.ma/prompt/archive/<timestamp>-<seq>.md` first, then prune that archive
108
+ down to the most recent entries. Pruning is by **count**, not age (`--keep` /
109
+ `MAP_ARCHIVE_KEEP` env var, default **10**) — simpler to reason about than a
110
+ retention window, and it doesn't depend on the clock. Use `--no-archive` for
111
+ the one case that shouldn't be kept anywhere: you pasted a secret and want it
112
+ actually gone.
113
+
114
+ ## Performance
115
+
116
+ A `map` session opens your *entire* Neovim config, which is exactly the
117
+ point — but if that config is large, startup isn't free. Two things worth
118
+ knowing, from actually measuring it (not guessing):
119
+
120
+ - **It's not a one-time thing.** Whatever makes your first `map` slow will
121
+ make every subsequent one about equally slow, unless something changes
122
+ (lazy.nvim itself finishes installing/compiling once; LSP servers and
123
+ large plugin trees generally don't get meaningfully faster after that).
124
+ - **On WSL specifically, check for `vim.opt.clipboard = 'unnamedplus'` (or
125
+ similar) in your own init.lua.** Setting that option makes Neovim probe for
126
+ a clipboard provider immediately, and on WSL that probe walks every `PATH`
127
+ entry checking `executable()` — including everything under `/mnt/c/...`,
128
+ which is slow cross-filesystem interop. Measured on this project's own dev
129
+ machine: ~900ms startup with that line in play, ~160ms with the Windows
130
+ `PATH` entries stripped, ~4ms with `-u NONE`. That's not multi-agent-prompt
131
+ overhead; it's one line in a personal init.lua interacting badly with WSL,
132
+ and fixing it (or guarding it — see `MAP_SESSION` below) speeds up *every*
133
+ Neovim session, not just `map`.
134
+ - **`map --clean` (`-u NONE`)** sidesteps all of it by skipping your config
135
+ entirely — the built-in fast path when you don't need your plugins for a
136
+ quick edit.
137
+ - **`MAP_SESSION=1`** is set in the environment of every nvim `map` launches.
138
+ Nothing in this package reads it — it's there so you can guard an
139
+ expensive line in your *own* init.lua behind
140
+ `if not vim.env.MAP_SESSION then ... end`, if you want full-config speed
141
+ back without giving up faster `map` startup. That's your config to edit,
142
+ not something this tool does for you. This isn't just for the clipboard
143
+ line above — a scratch prompt buffer has no use for a file-tree sidebar or
144
+ git tooling either, and with [lazy.nvim](https://github.com/folke/lazy.nvim)
145
+ you can skip those specific plugins the same way, with `cond` on the spec:
146
+
147
+ ```lua
148
+ return {
149
+ 'nvim-neo-tree/neo-tree.nvim',
150
+ cond = not vim.env.MAP_SESSION,
151
+ -- ...
152
+ }
153
+ ```
154
+
155
+ Confirmed on this project's own dev config: with `neo-tree.nvim` and
156
+ `gitsigns.nvim` guarded this way, `<leader>e` (NeoTree's toggle) becomes a
157
+ no-op in a `map` session, and their entire module trees (dozens of
158
+ individual `require()` calls each) simply don't load — real startup
159
+ savings, not just fewer keymaps to trip over. `cond` is evaluated on every
160
+ start, so this takes effect immediately; no `:Lazy sync`/restart needed.
161
+ - **It's specifically a startup cost, not a per-save one.** The clipboard
162
+ probe runs once, while your init.lua is sourced, not again on autosave, on
163
+ `FocusLost`, or when you switch panes and type `/prompt` — that last step
164
+ never touches nvim at all (`map pop`/reading the file is a separate,
165
+ plain filesystem read and write). If a session still feels slow at the
166
+ hand-off moment specifically, that's terminal pane-switching or the
167
+ agent's own slash-command overhead, not this tool or Neovim.
168
+ - **`/prompt`'s own overhead is small, and it isn't Neovim's.** Measured on
169
+ this project's own dev machine: `map pop` runs in ~60-85ms end to end —
170
+ bare `python3 -c pass` alone is ~30ms of that, this package's own imports
171
+ add maybe another ~20ms, and the rest is the file read/archive/write. If
172
+ `/prompt` still feels slow, that latency lives in the *agent host's* own
173
+ tool-call round trip (spawning/sandboxing the shell command), not in
174
+ anything this package does — there's no nvim process in this path at all.
175
+ The one thing the `prompt` skill itself controls: it runs `map pop`
176
+ directly rather than checking `which map` first, since that check is a
177
+ second subprocess spawn that's a wasted round trip almost every time.
178
+
179
+ ## Agent integration
180
+
181
+ `skills/prompt/` is a portable [Agent Skill](https://code.claude.com/docs/en/skills)
182
+ that teaches a host to read `.ma/prompt/current.md` and treat its content as
183
+ the user's message when they type `/prompt`. See
184
+ [`skills/README.md`](./skills/README.md) for install paths.
185
+
186
+ ## Why not tmux `send-keys` / auto-injection?
187
+
188
+ That's the obvious next step — save, close the pane, and have the tool type
189
+ `/prompt` into the neighboring agent pane for you — but it needs a reliable
190
+ way to identify *which* pane is running the agent, which varies by terminal
191
+ (tmux panes, Windows Terminal panes, and OS-level input injection like
192
+ `xdotool`/`wtype` all solve a different half of that problem). The manual
193
+ handoff above works everywhere today; auto-injection is being explored as a
194
+ follow-up, potentially building on [`multi-agent-registry`](https://github.com/InTEGr8or/multi-agent-registry)'s
195
+ chat discovery to identify a live agent session (not just a recent one) in
196
+ the same directory.
197
+
198
+ If/when that lands: prefer sending a widely-supported "submit" keystroke —
199
+ `Ctrl+Enter` is the closest thing to a universal convention across chat
200
+ input boxes — into the target pane after typing `/prompt`, over anything
201
+ input-method-specific, so the same injection code has a chance of working
202
+ across agents rather than being re-tuned per host.
203
+
204
+ ## Design notes
205
+
206
+ - **The prompt file is per-project, not global.** `.ma/prompt/current.md`
207
+ resolves against the nearest `.git` root, so working on two projects in two
208
+ terminal tabs never mixes up their scratch files.
209
+ - **`.ma/` is a shared root, not this tool's alone.** It's the umbrella
210
+ directory for the whole multi-agent-* line (`map`'s CLI alias is the shared
211
+ `ma` prefix of `map`/`mar`/`maa`) — one dotfolder instead of one per
212
+ product. Only `.ma/prompt/` is this tool's; a sibling product could use
213
+ `.ma/registry/`, etc., without colliding. Note that `task-agent`'s existing
214
+ `.task-agent/` convention predates this and stays as-is — migrating an
215
+ already-shipped tool's config layout is a separate, larger decision.
216
+ - **`.gitignore` is managed for you.** The first time `map edit` runs in a
217
+ git repo, it appends `.ma/prompt/` to `.gitignore` if it isn't already
218
+ covered — including by a broader pre-existing `.ma/` entry.
219
+ - **Reading is non-destructive; handing off isn't, but it never discards
220
+ either.** `map show` and `@prompt.md` (file mention) just print/read — the
221
+ file is untouched either way. `/prompt` (`map pop`) clears it, because the
222
+ whole point is not needing a separate "now go clear it" step — but it
223
+ archives first, so "cleared" never means "gone". `--no-archive` is the
224
+ explicit opt-out for content that shouldn't be kept anywhere at all.
225
+ - **The editor notices when the file changes out from under it.** `/prompt`
226
+ runs `map pop` from the *agent's* process, not from inside the running
227
+ nvim session — so if you're still looking at that buffer when you hand a
228
+ draft off, it auto-reloads (`autoread` + `checktime` on
229
+ `FocusGained`/`BufEnter`) the next time you focus it, rather than keep
230
+ showing the text you already submitted. It won't discard anything you've
231
+ since typed there unsaved.
@@ -0,0 +1,9 @@
1
+ multi_agent_prompt/__init__.py,sha256=o0H7CBkPPY2AYgAK5VUYlDy7eFXaghzFSJrSikN93hk,115
2
+ multi_agent_prompt/archive.py,sha256=jmuYRKoJPTj6fn-V81oT5x6CLkyCkXqQFJMQM0isMJ4,3284
3
+ multi_agent_prompt/cli.py,sha256=4HYgnygXM9czpIeJ5xLOYfeAddV9NMBdajXkm_Wk7oM,11397
4
+ multi_agent_prompt/editor.py,sha256=bIVxqnph7qo3sXS-ceUBUbH_XmGDs2uxe5t6VjTFOS8,12349
5
+ multi_agent_prompt/paths.py,sha256=dsQ1p5FEVOvNEq9XOGF2yarGCM2fYYlzzj5FrC1W7Tg,3563
6
+ multi_agent_prompt-0.1.0.dist-info/METADATA,sha256=C1sqT-hdf1OURq2cPlVcJL-A8_6lGoy3jRfjrrp1Ytc,12208
7
+ multi_agent_prompt-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
8
+ multi_agent_prompt-0.1.0.dist-info/entry_points.txt,sha256=0GLTX8zgbyWGj0twq7A3vfBBe8wYIB9C8vV4ppKfiB0,101
9
+ multi_agent_prompt-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ map = multi_agent_prompt.cli:main
3
+ multi-agent-prompt = multi_agent_prompt.cli:main