missioncache-install 1.0.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
+ """MissionCache installer - bootstrap package for MissionCache on Claude Code."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,424 @@
1
+ """Entry point for `uvx missioncache-install` / `pipx run missioncache-install`.
2
+
3
+ Invocation patterns:
4
+ uvx missioncache-install # interactive install wizard
5
+ uvx missioncache-install --all # install all components non-interactively
6
+ uvx missioncache-install --dashboard # install only the dashboard
7
+ uvx missioncache-install --all --no-statusline # install everything except the statusline
8
+ uvx missioncache-install --update # refresh whatever is in state.json
9
+ uvx missioncache-install --uninstall # interactive uninstall wizard (TTY only)
10
+ uvx missioncache-install --uninstall --all # uninstall every tracked component
11
+ uvx missioncache-install --uninstall codex,vscode # uninstall a specific list
12
+ uvx missioncache-install --local # maintainer mode: editable installs from clone
13
+
14
+ Project data and DBs at `~/.missioncache/` are never touched by any uninstall flow.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ from . import __version__, installers, state, ui, wizard
24
+ from .wizard import COMMAND_IMPLIES
25
+
26
+
27
+ DEFAULT_PORT = 8787
28
+
29
+ # Sentinel for bare `--uninstall` (no value supplied). A unique object so
30
+ # `is` comparison distinguishes it from `--uninstall ""` (empty list, e.g.
31
+ # unset shell var) and from `--uninstall foo,bar` (positive list). Cannot
32
+ # collide with any string a user could pass on the CLI.
33
+ INTERACTIVE_WIZARD = object()
34
+
35
+
36
+ def build_parser() -> argparse.ArgumentParser:
37
+ p = argparse.ArgumentParser(
38
+ prog="missioncache-install",
39
+ description="Bootstrap installer for MissionCache (project manager for Claude Code).",
40
+ )
41
+ p.add_argument(
42
+ "--version", action="version", version=f"%(prog)s {__version__}"
43
+ )
44
+
45
+ # `--all` and `--update` are install-time verbs and remain mutually
46
+ # exclusive. `--uninstall` is a different verb and lives outside the
47
+ # group so it can compose with `--all` (uninstall everything tracked,
48
+ # bypass wizard) and accept an optional positive list.
49
+ action = p.add_mutually_exclusive_group()
50
+ action.add_argument(
51
+ "--all", action="store_true",
52
+ help="With no other verb: install all components non-interactively. "
53
+ "With --uninstall: remove every tracked component, bypassing the "
54
+ "interactive wizard.",
55
+ )
56
+ action.add_argument(
57
+ "--update", action="store_true",
58
+ help="Update installed components in place (reads state).",
59
+ )
60
+ p.add_argument(
61
+ "--uninstall",
62
+ nargs="?",
63
+ const=INTERACTIVE_WIZARD,
64
+ default=None,
65
+ metavar="COMP1,COMP2",
66
+ help="Uninstall components. Bare flag opens the interactive wizard "
67
+ "(requires TTY). Pass a comma-separated component list (e.g. "
68
+ "`--uninstall codex,vscode`) for non-interactive removal of "
69
+ "specific components. Combine with `--all` to remove everything "
70
+ "tracked. Project data at ~/.missioncache/ is never touched.",
71
+ )
72
+
73
+ # Per-component opt-in flags. Any of these triggers non-interactive mode
74
+ # for exactly the components listed (in combination with --no-* opt-outs).
75
+ opt_in = p.add_argument_group(
76
+ "component opt-in (non-interactive)",
77
+ "Install only the components listed. Can be combined with --no-* to "
78
+ "exclude specific ones from --all.",
79
+ )
80
+ for flag, dest in (
81
+ ("--plugin", "plugin"),
82
+ ("--dashboard", "dashboard"),
83
+ ("--missioncache-auto", "missioncache_auto"),
84
+ ("--statusline", "statusline"),
85
+ ("--rules", "rules"),
86
+ ("--user-commands", "user_commands"),
87
+ ("--missioncache-db", "missioncache_db"),
88
+ ("--codex", "codex"),
89
+ ("--codex-commands", "codex_commands"),
90
+ ("--opencode", "opencode"),
91
+ ("--opencode-commands", "opencode_commands"),
92
+ ("--vscode", "vscode"),
93
+ ("--vscode-commands", "vscode_commands"),
94
+ ):
95
+ opt_in.add_argument(flag, dest=dest, action="store_true")
96
+
97
+ opt_out = p.add_argument_group(
98
+ "component opt-out",
99
+ "Exclude specific components from --all (e.g. `--all --no-statusline`). "
100
+ "`--no-codex-commands` keeps the Codex MCP server but skips the slash "
101
+ "command plugin (same for opencode / vscode).",
102
+ )
103
+ for flag, dest in (
104
+ ("--no-plugin", "no_plugin"),
105
+ ("--no-dashboard", "no_dashboard"),
106
+ ("--no-missioncache-auto", "no_missioncache_auto"),
107
+ ("--no-statusline", "no_statusline"),
108
+ ("--no-rules", "no_rules"),
109
+ ("--no-user-commands", "no_user_commands"),
110
+ ("--no-missioncache-db", "no_missioncache_db"),
111
+ ("--no-codex", "no_codex"),
112
+ ("--no-codex-commands", "no_codex_commands"),
113
+ ("--no-opencode", "no_opencode"),
114
+ ("--no-opencode-commands", "no_opencode_commands"),
115
+ ("--no-vscode", "no_vscode"),
116
+ ("--no-vscode-commands", "no_vscode_commands"),
117
+ ):
118
+ opt_out.add_argument(flag, dest=dest, action="store_true")
119
+
120
+ p.add_argument(
121
+ "--local", action="store_true",
122
+ help="Maintainer mode: editable installs + local marketplace from the "
123
+ "current clone. Auto-detected when run from a repo root.",
124
+ )
125
+ p.add_argument(
126
+ "--no-service", action="store_true",
127
+ help="Skip launchd/systemd service registration (dashboard will not auto-start).",
128
+ )
129
+ p.add_argument(
130
+ "--port", type=int, default=DEFAULT_PORT,
131
+ help=f"Dashboard port (default: {DEFAULT_PORT}).",
132
+ )
133
+ p.add_argument(
134
+ "--yes", "-y", action="store_true",
135
+ help="Skip per-file confirmations (still honors --no-* component opt-outs).",
136
+ )
137
+ return p
138
+
139
+
140
+ def _explicit_components(args: argparse.Namespace) -> list[str]:
141
+ """Components explicitly opted in via --plugin / --dashboard / etc."""
142
+ return [
143
+ c for c in installers.ALL_COMPONENTS
144
+ if getattr(args, c, False)
145
+ ]
146
+
147
+
148
+ def _excluded_components(args: argparse.Namespace) -> set[str]:
149
+ """Components explicitly opted out via --no-*.
150
+
151
+ Also auto-excludes a slash command companion when its parent MCP component
152
+ is excluded - slash commands need the MCP server to function, so installing
153
+ `codex_commands` without `codex` is a foot-gun. Users who really want that
154
+ asymmetry can override by passing `--codex-commands` explicitly.
155
+ """
156
+ excluded = {
157
+ c for c in installers.ALL_COMPONENTS
158
+ if getattr(args, f"no_{c}", False)
159
+ }
160
+ for parent, child in COMMAND_IMPLIES.items():
161
+ if parent in excluded and not getattr(args, child, False):
162
+ excluded.add(child)
163
+ return excluded
164
+
165
+
166
+ def _expand_implies(selected: list[str], excluded: set[str]) -> list[str]:
167
+ """Auto-add slash command companions for selected MCP integration parents.
168
+
169
+ `--codex` (or any `--all` that includes codex) implicitly turns on
170
+ `codex_commands` so that opting in to the Codex integration delivers the
171
+ full parity experience by default. Use `--no-codex-commands` to install
172
+ MCP without slash commands. Same pattern for opencode and vscode.
173
+
174
+ No-op if the child is already in `selected` (e.g. user passed both
175
+ `--codex` and `--codex-commands`) or explicitly excluded.
176
+ """
177
+ out = list(selected)
178
+ for parent, child in COMMAND_IMPLIES.items():
179
+ if parent in out and child not in out and child not in excluded:
180
+ out.append(child)
181
+ return out
182
+
183
+
184
+ def _resolve_mode_and_repo(args: argparse.Namespace) -> tuple[str, Path | None]:
185
+ """Decide pypi vs local mode and locate the repo root if local."""
186
+ cwd = Path.cwd()
187
+ marker = cwd / ".claude-plugin" / "plugin.json"
188
+ if args.local:
189
+ if not marker.exists():
190
+ ui.fail(
191
+ f"--local requires running from a orbit-pm clone "
192
+ f"(expected {marker} to exist)."
193
+ )
194
+ return "local", cwd
195
+ if marker.exists():
196
+ # Silent auto-detect: if they're in a clone, assume maintainer workflow.
197
+ return "local", cwd
198
+ return "pypi", None
199
+
200
+
201
+ def _expand_command_pairs(requested: list[str], installed: list[str]) -> list[str]:
202
+ """Auto-add `<tool>_commands` when uninstalling its parent `<tool>`.
203
+
204
+ Symmetric counterpart to the wizard's COMMAND_IMPLIES install-side pairing:
205
+ install pairs codex+codex_commands, so uninstall should too. Only adds the
206
+ child if it's still in the tracked-installed list (user may have already
207
+ removed it independently).
208
+
209
+ Asymmetric on purpose: removing `codex_commands` does NOT also remove
210
+ `codex` - users may want MCP without slash commands.
211
+ """
212
+ out = list(requested)
213
+ for parent, child in COMMAND_IMPLIES.items():
214
+ if parent in out and child in installed and child not in out:
215
+ out.append(child)
216
+ return out
217
+
218
+
219
+ def _filter_known_state(tracked: list[str]) -> list[str]:
220
+ """Drop state.json keys that are no longer in `ALL_COMPONENTS`.
221
+
222
+ Schema-evolution defense: if a future release deletes a component, an
223
+ older state.json may still name it. Filter and warn so the user sees
224
+ the orphan instead of a silent no-op or KeyError downstream.
225
+ """
226
+ valid = [c for c in tracked if c in installers.ALL_COMPONENTS]
227
+ unknown = [c for c in tracked if c not in installers.ALL_COMPONENTS]
228
+ if unknown:
229
+ ui.warn(
230
+ f"State file references unknown components: {', '.join(unknown)}.\n"
231
+ " These are not in this missioncache-install version's ALL_COMPONENTS list. "
232
+ "Skipping them."
233
+ )
234
+ return valid
235
+
236
+
237
+ def _run_uninstall(args: argparse.Namespace, ctx: installers.InstallContext) -> int:
238
+ """Dispatch the three uninstall patterns.
239
+
240
+ Patterns:
241
+ - `--uninstall --all` -> remove every tracked component. Refuses (warn +
242
+ no-op) if no state is tracked, matching `update_all`'s safer pattern.
243
+ - `--uninstall comp1,comp2` -> remove the listed components. Errors if
244
+ state is empty, list is empty after parsing, components are unknown,
245
+ or components aren't currently installed. Auto-expands `<tool>` to
246
+ include `<tool>_commands` if the latter is still tracked.
247
+ - `--uninstall` (bare, sentinel `INTERACTIVE_WIZARD`) -> interactive
248
+ wizard. Errors on non-TTY shells or empty state.
249
+
250
+ Combining `--all` with a positive list (e.g. `--uninstall foo --all`) is
251
+ an ambiguous error. Empty-string input (e.g. unset shell var) is rejected.
252
+ """
253
+ uninstall_arg = args.uninstall
254
+ bypass_wizard = args.all
255
+
256
+ if isinstance(uninstall_arg, str) and bypass_wizard:
257
+ # `--uninstall foo --all` is ambiguous: positive list AND --all both
258
+ # specified. (`--uninstall --all` alone has uninstall_arg=sentinel.)
259
+ ui.fail(
260
+ "Pass either `--uninstall --all` (everything tracked) OR "
261
+ "`--uninstall <list>` (specific components), not both."
262
+ )
263
+ raise AssertionError("unreachable") # ui.fail exits
264
+
265
+ if bypass_wizard:
266
+ tracked = _filter_known_state(state.installed_components())
267
+ if not tracked:
268
+ ui.warn(
269
+ "No tracked components to uninstall. State file is empty or "
270
+ "missing.\n"
271
+ " If you installed missioncache manually outside the installer, "
272
+ "remove components by hand or restore the state file at "
273
+ f"{state.STATE_FILE}."
274
+ )
275
+ return 0
276
+ installers.uninstall_components(tracked, ctx)
277
+ return 0
278
+
279
+ if isinstance(uninstall_arg, str):
280
+ # Empty-string from unset shell var (`--uninstall "$EMPTY"`) lands
281
+ # here as `""` and is rejected loudly. Bare flag would have been
282
+ # the sentinel, never `""`.
283
+ if not uninstall_arg.strip():
284
+ ui.fail(
285
+ f"Empty `--uninstall` argument: {uninstall_arg!r}.\n"
286
+ " Pass a comma-separated component list, `--all`, or invoke "
287
+ "without a value to open the interactive wizard."
288
+ )
289
+ raise AssertionError("unreachable") # ui.fail exits
290
+
291
+ requested = [
292
+ c.strip().lower().replace("-", "_")
293
+ for c in uninstall_arg.split(",")
294
+ if c.strip()
295
+ ]
296
+ # Dedup while preserving first-occurrence order.
297
+ requested = list(dict.fromkeys(requested))
298
+
299
+ # Separator-only input (`,`, ` , , `, etc.) bypasses the whitespace
300
+ # guard above (",".strip() == ",") but yields an empty list after
301
+ # the if-c.strip() filter. Without this check we'd silently no-op
302
+ # via uninstall_components([]) - same failure mode the empty-string
303
+ # guard prevents.
304
+ if not requested:
305
+ ui.fail(
306
+ f"No component names found in `--uninstall {uninstall_arg!r}`.\n"
307
+ " Input contained only commas/whitespace. Pass a real "
308
+ "component list, `--all`, or invoke without a value for the "
309
+ "interactive wizard."
310
+ )
311
+ raise AssertionError("unreachable") # ui.fail exits
312
+
313
+ unknown = [c for c in requested if c not in installers.ALL_COMPONENTS]
314
+ if unknown:
315
+ ui.fail(
316
+ f"Unknown components: {', '.join(unknown)}.\n"
317
+ f" Valid components: {', '.join(installers.ALL_COMPONENTS)}"
318
+ )
319
+ raise AssertionError("unreachable") # ui.fail exits
320
+
321
+ installed = _filter_known_state(state.installed_components())
322
+ if not installed:
323
+ ui.fail(
324
+ "No prior missioncache-install was tracked.\n"
325
+ " Use `--uninstall --all` to attempt a best-effort uninstall."
326
+ )
327
+ raise AssertionError("unreachable") # ui.fail exits
328
+
329
+ # Auto-expand <tool> to include <tool>_commands. Inform the user
330
+ # so they don't think we're going off-script. Re-dedupe in case
331
+ # they explicitly passed both parent and child.
332
+ expanded = list(dict.fromkeys(_expand_command_pairs(requested, installed)))
333
+ added = [c for c in expanded if c not in requested]
334
+ if added:
335
+ ui.detail(
336
+ f"Auto-adding paired components: {', '.join(added)} "
337
+ "(parent install pairs them; uninstall keeps the pairing)."
338
+ )
339
+
340
+ not_installed = [c for c in expanded if c not in installed]
341
+ if not_installed:
342
+ ui.fail(
343
+ f"Not currently installed: {', '.join(not_installed)}.\n"
344
+ f" Currently installed: {', '.join(installed)}"
345
+ )
346
+ raise AssertionError("unreachable") # ui.fail exits
347
+
348
+ installers.uninstall_components(expanded, ctx)
349
+ return 0
350
+
351
+ # Bare `--uninstall` -> interactive wizard.
352
+ components = wizard.run_uninstall_wizard()
353
+ if components is None:
354
+ # Wizard either reported an error itself (already exited) or the
355
+ # user cancelled. Either way, no work to do.
356
+ return 0
357
+
358
+ # Mirror the positive-list path: auto-expand <tool> -> <tool>_commands
359
+ # so picking `codex` from the wizard menu also removes its paired
360
+ # slash-command plugin. Without this, the wizard path would leave
361
+ # orphaned `/missioncache-*` commands pointing at a removed MCP integration.
362
+ installed = _filter_known_state(state.installed_components())
363
+ expanded = list(dict.fromkeys(_expand_command_pairs(components, installed)))
364
+ added = [c for c in expanded if c not in components]
365
+ if added:
366
+ ui.detail(
367
+ f"Auto-adding paired components: {', '.join(added)} "
368
+ "(parent install pairs them; uninstall keeps the pairing)."
369
+ )
370
+ installers.uninstall_components(expanded, ctx)
371
+ return 0
372
+
373
+
374
+ def main() -> int:
375
+ args = build_parser().parse_args()
376
+ if args.uninstall is not None and args.update:
377
+ ui.fail("--uninstall and --update cannot be combined (different verbs).")
378
+ raise AssertionError("unreachable") # ui.fail exits
379
+ mode, repo_root = _resolve_mode_and_repo(args)
380
+ state.set_mode(mode)
381
+ ctx = installers.InstallContext(
382
+ mode=mode,
383
+ repo_root=repo_root,
384
+ skip_service=args.no_service,
385
+ port=args.port,
386
+ assume_yes=args.yes,
387
+ )
388
+
389
+ if args.uninstall is not None:
390
+ return _run_uninstall(args, ctx)
391
+
392
+ if args.update:
393
+ installers.update_all(ctx)
394
+ return 0
395
+
396
+ explicit = _explicit_components(args)
397
+ excluded = _excluded_components(args)
398
+
399
+ if args.all or explicit:
400
+ base = list(installers.ALL_COMPONENTS) if args.all else explicit
401
+ selected = [c for c in base if c not in excluded]
402
+ selected = _expand_implies(selected, excluded)
403
+ # statusline needs the missioncache-statusline entry point, which ships in the
404
+ # missioncache-dashboard package. Installing statusline without dashboard wires
405
+ # settings.json to a command that won't resolve. Auto-add dashboard.
406
+ if "statusline" in selected and "dashboard" not in selected and "dashboard" not in excluded:
407
+ ui.warn("statusline depends on missioncache-dashboard (provides the missioncache-statusline entry point). Adding dashboard to the install.")
408
+ selected.insert(selected.index("statusline"), "dashboard")
409
+ if not selected:
410
+ ui.warn("Component selection is empty after applying --no-* flags.")
411
+ return 0
412
+ ui.banner()
413
+ ui.info(f"Installing: {', '.join(c.replace('_', '-') for c in selected)}")
414
+ installers.install_components(selected, ctx)
415
+ ui.success_banner(selected, dashboard_port=ctx.port)
416
+ return 0
417
+
418
+ # Default: interactive wizard.
419
+ wizard.run(ctx)
420
+ return 0
421
+
422
+
423
+ if __name__ == "__main__":
424
+ sys.exit(main())
@@ -0,0 +1,137 @@
1
+ ---
2
+ description: "Mark an active project as completed and archive files"
3
+ argument-hint: "[project-name]"
4
+ ---
5
+
6
+ # Complete Project
7
+
8
+ Mark a project as completed and optionally move MissionCache files to the completed folder.
9
+
10
+ ## Quick Start
11
+
12
+ 1. **If project name provided:**
13
+ ```
14
+ mcp__plugin_missioncache_pm__complete_task(project_name="<name>", move_files=true)
15
+ ```
16
+
17
+ 2. **If no project name, list active projects:**
18
+ ```
19
+ mcp__plugin_missioncache_pm__list_active_tasks()
20
+ ```
21
+ Then ask user to select one.
22
+
23
+ ## Workflow
24
+
25
+ ### Step 1: Confirm Project
26
+
27
+ If project name not provided, list active projects and ask user to select.
28
+
29
+ ### Step 2: Show Summary
30
+
31
+ Before completing, show the user:
32
+ - Total time invested
33
+ - Progress (should be 100%)
34
+ - What will happen (files moved, status changed)
35
+
36
+ ### Step 3: Complete
37
+
38
+ Call `mcp__plugin_missioncache_pm__complete_task` which:
39
+ 1. Updates project status to "completed" in database
40
+ 2. Moves files from `~/.missioncache/active/<name>/` to `~/.missioncache/completed/<name>/`
41
+ 3. Records completion timestamp
42
+
43
+ ### Step 4: Process Time Tracking
44
+
45
+ Call `mcp__plugin_missioncache_pm__process_heartbeats()` to finalize time tracking.
46
+
47
+ ### Step 5: Clear Statusline
48
+
49
+ Remove the project pointer so the statusline stops showing the completed project name. Mirrors the resolver in `/missioncache:new` / `/missioncache:load` (filesystem primary, term-env fallback) and uses direct SQL because the dashboard has no DELETE endpoint for project_state. Silently no-ops on quick-install setups without `hooks-state.db`.
50
+
51
+ ```bash
52
+ # Primary: env var set by Claude Code 2.1.132+ in every Bash tool subprocess.
53
+ SESSION_ID="$CLAUDE_CODE_SESSION_ID"
54
+
55
+ # Fallback for older Claude Code versions. SessionStart hook writes the
56
+ # authoritative current-session pointer at ~/.claude/hooks/state/cwd-session/
57
+ # <sanitized-cwd>.json; transcript mtime walk catches sessions that started
58
+ # before the pointer mechanism landed.
59
+ if [ -z "$SESSION_ID" ]; then
60
+ CWD_KEY=$(pwd | sed 's|/|-|g')
61
+ POINTER_FILE="$HOME/.claude/hooks/state/cwd-session/${CWD_KEY}.json"
62
+ if [ -r "$POINTER_FILE" ]; then
63
+ SESSION_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin)['sessionId'])" < "$POINTER_FILE" 2>/dev/null)
64
+ fi
65
+ [ -z "$SESSION_ID" ] && SESSION_ID=$(ls -t "$HOME/.claude/projects/${CWD_KEY}"/*.jsonl 2>/dev/null | head -1 | xargs -I{} basename {} .jsonl)
66
+ fi
67
+
68
+ # Delete project_state row. String concatenation, not f-strings, because
69
+ # Python <=3.11 rejects backslashes inside f-string expressions and the
70
+ # outer single-quoted bash heredoc forces escaped double quotes inside.
71
+ if [ -n "$SESSION_ID" ]; then
72
+ SESSION_ID="$SESSION_ID" python3 -c '
73
+ import os, sqlite3
74
+ sid = os.environ["SESSION_ID"]
75
+ conn = sqlite3.connect(os.path.expanduser("~/.claude/hooks-state.db"))
76
+ cur = conn.execute("DELETE FROM project_state WHERE session_id = ?", (sid,))
77
+ conn.commit()
78
+ print("Cleared project_state for session " + sid + " (rows: " + str(cur.rowcount) + ")")
79
+ ' 2>/dev/null
80
+
81
+ # Also delete the per-session project pointer written by /missioncache:load and /missioncache:new.
82
+ # Read by find_task_for_cwd (missioncache-db); leaving it in place would make /missioncache:save
83
+ # still find this task after completion.
84
+ rm -f "$HOME/.claude/hooks/state/projects/${SESSION_ID}.json" 2>/dev/null
85
+ fi
86
+ ```
87
+
88
+ ### Step 6: Share Dashboard Link (if running)
89
+
90
+ Probe the dashboard and, if reachable, include a deep link in the completion summary so the user can jump straight to the archived project view. Skip silently if the dashboard is not installed or not running - dead links train users to ignore the dashboard entirely.
91
+
92
+ Replace `<project-name>` with the kebab-case project name, then run:
93
+
94
+ ```bash
95
+ PROJECT_NAME='<project-name>'
96
+ DASHBOARD_URL="${MISSIONCACHE_DASHBOARD_URL:-http://localhost:8787}"
97
+ if curl -sf -o /dev/null --max-time 1 "${DASHBOARD_URL}/health" 2>/dev/null; then
98
+ echo "Dashboard: ${DASHBOARD_URL}/#projects?task=$PROJECT_NAME"
99
+ fi
100
+ ```
101
+
102
+ If the probe succeeds, include the emitted URL as a "Dashboard" line in the completion summary shown below. If it emits nothing, omit the line entirely.
103
+
104
+ ## Example Output
105
+
106
+ ```
107
+ ## Completing Project: kafka-consumer-fix
108
+
109
+ **Time Invested:** 4h 30m
110
+ **Progress:** 8/8 tasks (100%)
111
+ **Status:** active -> completed
112
+
113
+ Moving files:
114
+ ~/.missioncache/active/kafka-consumer-fix/ -> ~/.missioncache/completed/kafka-consumer-fix/
115
+
116
+ Project completed successfully!
117
+
118
+ Summary:
119
+ - Total time: 4h 30m
120
+ - Sessions: 12
121
+ - Completed at: 2026-01-20 15:30
122
+ - Dashboard: http://localhost:8787/#projects?task=kafka-consumer-fix
123
+ ```
124
+
125
+ ## Options
126
+
127
+ - `move_files=true` (default): Move MissionCache files to completed/
128
+ - `move_files=false`: Keep files in active/ (useful for reference)
129
+
130
+ ## MCP Tools Used
131
+
132
+ | Tool | Purpose |
133
+ |------|---------|
134
+ | `mcp__plugin_missioncache_pm__list_active_tasks` | List projects if none specified |
135
+ | `mcp__plugin_missioncache_pm__get_task` | Get project details for summary |
136
+ | `mcp__plugin_missioncache_pm__complete_task` | Mark complete and move files |
137
+ | `mcp__plugin_missioncache_pm__process_heartbeats` | Finalize time tracking |