diff --git a/mubu/agent-harness/README.md b/mubu/agent-harness/README.md index e5fb93af2..add42ae38 100644 --- a/mubu/agent-harness/README.md +++ b/mubu/agent-harness/README.md @@ -23,6 +23,7 @@ What this gives you: - the canonical implementation now lives inside this directory - the same `cli-anything-mubu` console script is exposed - the main CLI is Click-based with grouped command domains +- no-argument daily helpers only work when `MUBU_DAILY_FOLDER` is configured - `skill_generator.py` can regenerate the packaged `skills/SKILL.md` Canonical implementation now lives under: @@ -47,4 +48,5 @@ Current state: - canonical package source is now under `agent-harness/cli_anything/mubu/...` - root-level wrappers preserve backward compatibility during development - grouped `discover` / `inspect` / `mutate` / `session` commands now exist +- daily-note helpers require an explicit folder reference unless `MUBU_DAILY_FOLDER` is set - the packaged `SKILL.md` is now generated from the canonical harness diff --git a/mubu/agent-harness/cli_anything/mubu/README.md b/mubu/agent-harness/cli_anything/mubu/README.md index 9357c2de8..cda0c89e0 100644 --- a/mubu/agent-harness/cli_anything/mubu/README.md +++ b/mubu/agent-harness/cli_anything/mubu/README.md @@ -9,6 +9,12 @@ This package lives in the CLI-Anything-aligned harness tree and exposes: - default REPL when no subcommand is supplied - REPL banner with app version, packaged skill path, and history path - persisted `current-doc` and `current-node` REPL context +- grouped `discover` / `inspect` / `mutate` / `session` commands + +Daily helpers are now explicit by default: + +- pass a daily-folder reference to `discover daily-current`, `inspect daily-nodes`, or `session use-daily` +- or set `MUBU_DAILY_FOLDER` if you want those helpers to work without an argument Canonical source paths: diff --git a/mubu/agent-harness/cli_anything/mubu/mubu_cli.py b/mubu/agent-harness/cli_anything/mubu/mubu_cli.py index 63a4700a7..83931178f 100644 --- a/mubu/agent-harness/cli_anything/mubu/mubu_cli.py +++ b/mubu/agent-harness/cli_anything/mubu/mubu_cli.py @@ -51,7 +51,7 @@ Builtins: exit, quit Leave the REPL use-doc Set the current document reference for this REPL session use-node Set the current node reference for this REPL session - use-daily Resolve and set the current daily document + use-daily [ref] Resolve and set the current daily document current-doc Show the current document reference current-node Show the current node reference clear-doc Clear the current document reference @@ -62,13 +62,15 @@ Builtins: Examples: recent --limit 5 --json - discover daily-current - discover daily-current --json - inspect daily-nodes --query 日志流 --json - session use-doc 'Workspace/Daily tasks/26.03.16' - mutate create-child @doc --parent-node-id node-demo1 --text 'scratch child' --json + discover daily-current '' + discover daily-current --json '' + inspect daily-nodes '' --query '' --json + session use-doc '' + mutate create-child @doc --parent-node-id --text 'scratch child' --json mutate delete-node @doc --node-id @node --json - update-text 'Workspace/Daily tasks/26.03.16' --node-id node-demo1 --text 'new text' --json + update-text '' --node-id --text 'new text' --json + +If you prefer no-argument daily helpers, set MUBU_DAILY_FOLDER=''. """ @@ -156,14 +158,15 @@ def append_command_history(command_line: str) -> None: save_session_state(session) -def resolve_current_daily_doc_ref(folder_ref: str = "Daily tasks") -> str: +def resolve_current_daily_doc_ref(folder_ref: str | None = None) -> str: + resolved_folder_ref = mubu_probe.resolve_daily_folder_ref(folder_ref) metas = mubu_probe.load_document_metas(mubu_probe.DEFAULT_STORAGE_ROOT) folders = mubu_probe.load_folders(mubu_probe.DEFAULT_STORAGE_ROOT) - docs, folder, ambiguous = mubu_probe.folder_documents(metas, folders, folder_ref) + docs, folder, ambiguous = mubu_probe.folder_documents(metas, folders, resolved_folder_ref) if folder is None: if ambiguous: - raise RuntimeError(mubu_probe.ambiguous_error_message("folder", folder_ref, ambiguous, "path")) - raise RuntimeError(f"folder not found: {folder_ref}") + raise RuntimeError(mubu_probe.ambiguous_error_message("folder", resolved_folder_ref, ambiguous, "path")) + raise RuntimeError(f"folder not found: {resolved_folder_ref}") selected, _ = mubu_probe.choose_current_daily_document(docs) if selected is None or not selected.get("doc_path"): raise RuntimeError(f"no current daily document found in {folder['path']}") @@ -334,15 +337,16 @@ def handle_repl_builtin(argv: list[str], session: dict[str, object]) -> tuple[bo click.echo(f"Current node: {node_ref}") return True, 0 if command == "use-daily": - folder_ref = " ".join(argv[1:]) if len(argv) > 1 else "Daily tasks" + folder_ref = " ".join(argv[1:]).strip() if len(argv) > 1 else None try: - doc_ref = resolve_current_daily_doc_ref(folder_ref) + resolved_folder_ref = mubu_probe.resolve_daily_folder_ref(folder_ref) + doc_ref = resolve_current_daily_doc_ref(resolved_folder_ref) except RuntimeError as exc: click.echo(str(exc), err=True) return True, 0 session["current_doc"] = doc_ref save_session_state(session) - append_command_history(f"use-daily {folder_ref}".strip()) + append_command_history(f"use-daily {resolved_folder_ref}") click.echo(f"Current doc: {doc_ref}") return True, 0 @@ -408,7 +412,7 @@ def cli(ctx: click.Context, json_output: bool) -> int: @cli.group(context_settings=CONTEXT_SETTINGS) def discover() -> None: - """Discovery commands for folders, documents, recency, and Daily tasks resolution.""" + """Discovery commands for folders, documents, recency, and daily-document resolution.""" @discover.command("docs", context_settings=CONTEXT_SETTINGS, add_help_option=False) @@ -618,12 +622,16 @@ def use_node(node_ref: tuple[str, ...]) -> int: @click.argument("folder_ref", nargs=-1) def use_daily(folder_ref: tuple[str, ...]) -> int: """Resolve and persist the current daily document reference.""" - value = " ".join(folder_ref) if folder_ref else "Daily tasks" - doc_ref = resolve_current_daily_doc_ref(value) + raw_value = " ".join(folder_ref).strip() if folder_ref else None + try: + resolved_folder_ref = mubu_probe.resolve_daily_folder_ref(raw_value) + doc_ref = resolve_current_daily_doc_ref(resolved_folder_ref) + except RuntimeError as exc: + raise click.ClickException(str(exc)) from exc session_state = load_session_state() session_state["current_doc"] = doc_ref save_session_state(session_state) - append_command_history(f"session use-daily {value}".strip()) + append_command_history(f"session use-daily {resolved_folder_ref}") click.echo(f"Current doc: {doc_ref}") return 0 diff --git a/mubu/agent-harness/cli_anything/mubu/skills/SKILL.md b/mubu/agent-harness/cli_anything/mubu/skills/SKILL.md index eb33a143c..60d654cef 100644 --- a/mubu/agent-harness/cli_anything/mubu/skills/SKILL.md +++ b/mubu/agent-harness/cli_anything/mubu/skills/SKILL.md @@ -21,6 +21,7 @@ pip install -e . - Python 3.10+ - An active Mubu desktop session on this machine - Local Mubu profile data available to the CLI +- Set `MUBU_DAILY_FOLDER` if you want no-argument daily helpers ## Entry Points @@ -36,7 +37,7 @@ When invoked without a subcommand, the CLI enters an interactive REPL session. ### Discover -Discovery commands for folders, documents, recency, and Daily tasks resolution. +Discovery commands for folders, documents, recency, and daily-document resolution. | Command | Description | |---------|-------------| @@ -123,9 +124,9 @@ Session and state commands for current document/node context and local command h ## Recommended Agent Workflow ```text -discover daily-current --json +discover daily-current '' --json -> -inspect daily-nodes --query '' --json +inspect daily-nodes '' --query '' --json -> session use-doc '' -> @@ -143,6 +144,7 @@ mutate update-text / create-child / delete-node --json 5. Prefer `--node-id` and `--parent-node-id` over text matching. 6. `delete-node` removes the full targeted subtree. 7. Even same-text updates can still advance document version history. +8. Pass a daily-folder reference explicitly or set `MUBU_DAILY_FOLDER` before using no-arg daily helpers. ## Examples @@ -161,10 +163,10 @@ cli-anything-mubu ### Discover Current Daily Note -Resolve the current daily note and emit JSON output for an agent. +Resolve the current daily note from an explicit folder reference. ```bash -cli-anything-mubu --json discover daily-current +cli-anything-mubu --json discover daily-current '' ``` @@ -173,7 +175,7 @@ cli-anything-mubu --json discover daily-current Inspect the exact outgoing payload before a live mutation. ```bash -cli-anything-mubu mutate update-text 'Workspace/Daily tasks/26.03.16' --node-id node-demo1 --text 'new text' --json +cli-anything-mubu mutate update-text '' --node-id --text 'new text' --json ``` diff --git a/mubu/agent-harness/cli_anything/mubu/tests/TEST.md b/mubu/agent-harness/cli_anything/mubu/tests/TEST.md index 6242b7014..e2bb0354f 100644 --- a/mubu/agent-harness/cli_anything/mubu/tests/TEST.md +++ b/mubu/agent-harness/cli_anything/mubu/tests/TEST.md @@ -5,17 +5,20 @@ This file follows the CLI-Anything habit of keeping the test plan and the execut ## Test Inventory Plan - `test_mubu_probe.py`: 26 unit / light integration tests planned +- `test_core.py`: 35 pure-logic contract tests planned - `test_cli_entrypoint.py`: 13 subprocess / entrypoint tests planned -- `test_agent_harness.py`: 9 packaging / harness-layout tests planned -- `test_live_api.py`: 6 opt-in live-session tests planned for a later phase +- `test_full_e2e.py`: 11 local-data end-to-end tests planned +- `test_agent_harness.py`: 11 packaging / harness-layout tests planned Current status: - `test_mubu_probe.py` exists and passes +- `test_core.py` exists and passes - `test_cli_entrypoint.py` exists and passes +- `test_full_e2e.py` exists and passes when local Mubu data is available - `test_agent_harness.py` exists and passes - canonical harness test modules now also exist under `agent-harness/cli_anything/mubu/tests/` -- `test_live_api.py` is not implemented yet because live mutation tests need explicit opt-in controls +- no separate `test_live_api.py` exists yet; local-data live coverage currently lives in `test_full_e2e.py` with skip guards and dry-run-first mutation checks ## Unit Test Plan @@ -103,6 +106,41 @@ Expected subprocess count: - 13 tests +### Module: `test_core.py` + +Behaviors covered now: + +- pure helper and transformation contracts +- plain-text and rich-text HTML conversion +- node id generation +- node iteration and path conversion +- folder index construction +- daily-title classification +- normalization helpers and revision parsing +- timestamp parsing and formatting +- default local-path discovery +- ambiguity message formatting +- document metadata enrichment and record deduplication + +Expected pure-logic count: + +- 35 tests + +### Module: `test_full_e2e.py` + +Behaviors covered now: + +- live local-data discovery commands +- current-daily resolution with `MUBU_DAILY_FOLDER` +- live node listing from the current daily note +- `session use-daily` persisted state +- REPL `use-daily` plus follow-on inspection +- dry-run `update-text`, `create-child`, and `delete-node` + +Expected local-data E2E count: + +- 11 tests + ### Module: `test_agent_harness.py` Behaviors covered now: @@ -119,7 +157,7 @@ Behaviors covered now: Expected packaging count: -- 9 tests +- 11 tests ## E2E Test Plan @@ -128,7 +166,7 @@ These workflows are currently verified manually against the real local Mubu sess Planned live scenarios: 1. read recent documents from the local desktop profile -2. resolve `Workspace/Daily tasks` and identify the current daily note +2. resolve `` and identify the current daily note 3. enumerate live nodes inside the current daily note 4. dry-run a text update and inspect the exact outgoing payload 5. execute one same-text live update to validate auth/member/version wiring @@ -157,10 +195,10 @@ What should be verified in later automated live tests: ### Workflow 1: Daily Note Discovery -- Simulates: Codex entering the same daily workspace the user is using +- Simulates: an agent entering a configured daily-note workspace - Operations chained: - `recent` - - `path-docs 'Workspace/Daily tasks'` + - `path-docs ''` - Verified: - folder path resolution - correct daily-note document ids @@ -170,8 +208,8 @@ What should be verified in later automated live tests: - Simulates: Codex locating the exact node to edit before sending any write - Operations chained: - - `open-path 'Workspace/Daily tasks/26.03.16'` - - `doc-nodes 'Workspace/Daily tasks/26.03.16' --query '日志流'` + - `open-path ''` + - `doc-nodes '' --query ''` - Verified: - live document lookup - correct node id @@ -181,7 +219,8 @@ What should be verified in later automated live tests: - Simulates: Codex jumping directly to the user's current daily note - Operations chained: - - `daily-current --json` + - `daily-current '' --json` + - `daily-current --json` with `MUBU_DAILY_FOLDER=''` - Verified: - date-like title filtering - template exclusion @@ -191,7 +230,8 @@ What should be verified in later automated live tests: - Simulates: Codex looking for an anchor inside today's daily note without manually resolving the path first - Operations chained: - - `daily-nodes --query '...'` + - `daily-nodes '' --query ''` + - `daily-nodes --query ''` with `MUBU_DAILY_FOLDER=''` - Verified: - current daily-note resolution - live document fetch @@ -244,17 +284,13 @@ What should be verified in later automated live tests: Command: ```bash -python3 -m unittest tests/test_mubu_probe.py tests/test_cli_entrypoint.py tests/test_agent_harness.py +CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything/mubu/tests -q ``` Latest result: ```text -................................................ ----------------------------------------------------------------------- -Ran 48 tests in 16.880s - -OK +96 passed ``` ### Syntax Verification @@ -284,6 +320,7 @@ Commands: .venv/bin/python -m pip install -e ./agent-harness .venv/bin/python -m pip install -e . .venv/bin/cli-anything-mubu --help +.venv/bin/cli-anything-mubu --json discover daily-current '' .venv/bin/cli-anything-mubu --json discover daily-current .venv/bin/cli-anything-mubu session status --json tmpdir=$(mktemp -d) @@ -294,7 +331,8 @@ Latest result: - both editable-install paths succeeded when run sequentially - installed `--help` exposes grouped `discover` / `inspect` / `mutate` / `session` domains -- installed `discover daily-current` resolved the real daily note `Workspace/Daily tasks/26.03.16` +- installed `discover daily-current ''` resolved the current daily note +- installed `discover daily-current` also works when `MUBU_DAILY_FOLDER` is configured - installed `session status --json` returned persisted state successfully - installed no-arg REPL started cleanly, displayed the packaged canonical skill path, and exited cleanly @@ -335,13 +373,15 @@ Latest result: Commands: ```bash -.venv/bin/cli-anything-mubu daily-current --json +.venv/bin/cli-anything-mubu discover daily-current '' --json +.venv/bin/cli-anything-mubu discover daily-current --json printf 'exit\n' | env CLI_ANYTHING_MUBU_STATE_DIR="$(mktemp -d)" .venv/bin/cli-anything-mubu ``` Latest result: -- installed `daily-current --json` passed against the real local Mubu session +- installed `discover daily-current '' --json` passed against the real local Mubu session +- installed no-arg `discover daily-current --json` passed when `MUBU_DAILY_FOLDER` was configured - installed REPL banner pointed to `agent-harness/cli_anything/mubu/skills/SKILL.md` ### Wheel Packaging Verification @@ -387,7 +427,8 @@ python3 -m venv .venv .venv/bin/cli-anything-mubu --help .venv/bin/cli-anything-mubu repl --help tmpdir=$(mktemp -d) && env CLI_ANYTHING_MUBU_STATE_DIR="$tmpdir" /usr/bin/zsh -lc "printf 'exit\n' | .venv/bin/cli-anything-mubu" -.venv/bin/cli-anything-mubu daily-current --json +.venv/bin/cli-anything-mubu discover daily-current '' --json +.venv/bin/cli-anything-mubu discover daily-current --json .venv/bin/python -m pip install -e ./agent-harness python3 agent-harness/setup.py --name python3 agent-harness/setup.py --version @@ -404,7 +445,8 @@ Latest result: - REPL can store and report the current node reference during a session - REPL can persist `current-node` across independent processes when given the same state directory - REPL can expand both `@doc` and `@node` into a real dry-run command -- installed console script can resolve the current daily note +- installed console script can resolve the current daily note from an explicit folder reference +- installed console script also supports no-arg daily resolution when `MUBU_DAILY_FOLDER` is set - `agent-harness/` now works as a real editable-install root - harness setup metadata reports the correct package identity @@ -413,14 +455,16 @@ Latest result: Commands executed on the real machine: ```bash -python3 mubu_probe.py path-docs 'Workspace/Daily tasks' --limit 5 --json -python3 mubu_probe.py daily-current --json -python3 mubu_probe.py daily-nodes --query '日志流' --json -python3 mubu_probe.py doc-nodes 'Workspace/Daily tasks/26.03.16' --query '日志流' --json -python3 mubu_probe.py create-child 'Workspace/Daily tasks/26.03.16' --parent-node-id node-demo1 --text 'CLI bridge dry run child' --note 'not executed' --json -python3 mubu_probe.py delete-node 'Workspace/Daily tasks/26.03.16' --node-id node-demo1 --json -python3 mubu_probe.py update-text 'Workspace/Daily tasks/26.03.16' --node-id node-demo1 --text '日志流' --json -python3 mubu_probe.py update-text 'Workspace/Daily tasks/26.03.16' --match-text '日志流' --text '日志流' --execute --json +python3 mubu_probe.py path-docs '' --limit 5 --json +python3 mubu_probe.py daily-current '' --json +MUBU_DAILY_FOLDER='' python3 mubu_probe.py daily-current --json +python3 mubu_probe.py daily-nodes '' --query '' --json +MUBU_DAILY_FOLDER='' python3 mubu_probe.py daily-nodes --query '' --json +python3 mubu_probe.py doc-nodes '' --query '' --json +python3 mubu_probe.py create-child '' --parent-node-id --text 'CLI bridge dry run child' --note 'not executed' --json +python3 mubu_probe.py delete-node '' --node-id --json +python3 mubu_probe.py update-text '' --node-id --text '' --json +python3 mubu_probe.py update-text '' --match-text '' --text '' --execute --json python3 - <<'PY' # create-child --execute scratch node, then delete-node --execute that exact node id PY @@ -428,23 +472,22 @@ PY Observed results: -- `path-docs` resolved folder id `folder-daily-01` -- current daily doc resolved to `doc-demo-01` -- `daily-current` resolved the same current daily path `Workspace/Daily tasks/26.03.16` in one step -- `daily-nodes` resolved the same current daily note and returned live node `node-demo1` -- `doc-nodes` resolved node id `node-demo1`, path `["nodes", 3, 0]`, and api path `["nodes", 3, "children", 0]` -- `create-child` dry-run resolved parent `node-demo1`, child insert index `4`, and child path `["nodes", 3, "children", 0, "children", 4]` -- `delete-node` dry-run resolved parent `qv9klzkq2L`, delete index `0`, and api path `["nodes", 3, "children", 0]` +- `path-docs` resolved the configured daily folder successfully +- `daily-current` resolved the same current daily note with both the explicit folder argument and `MUBU_DAILY_FOLDER` +- `daily-nodes` resolved the same current daily note and returned the targeted live node +- `doc-nodes` returned a stable node id plus both simplified and API paths for the target node +- `create-child` dry-run resolved the parent node, child insert index, and canonical child path +- `delete-node` dry-run resolved the parent id, delete index, and canonical API path - dry-run update produced the expected `CHANGE` payload - real execute returned success -- live document version advanced from `256` to `257` -- post-fetch verification confirmed the node text still read `日志流` -- reversible scratch create/delete advanced live version from `261` to `262` to `263` -- scratch node `hUVCZEUf3R` was present after create and absent after delete +- live document version advanced after execution +- post-fetch verification confirmed the target node text matched the requested value +- reversible scratch create/delete advanced live version on each execute call +- the scratch node was present after create and absent after delete ## Summary Statistics -- automated tests: 40 / 40 pass +- automated tests: 96 / 96 pass - syntax check: pass - help/CLI surface checks: pass - isolated install / entrypoint checks: pass diff --git a/mubu/agent-harness/cli_anything/mubu/tests/test_agent_harness.py b/mubu/agent-harness/cli_anything/mubu/tests/test_agent_harness.py index 9e1e9945c..8f1cd871d 100644 --- a/mubu/agent-harness/cli_anything/mubu/tests/test_agent_harness.py +++ b/mubu/agent-harness/cli_anything/mubu/tests/test_agent_harness.py @@ -126,6 +126,10 @@ class AgentHarnessPackagingTests(unittest.TestCase): self.assertIn("### Session", content) self.assertIn("| `status` |", content) self.assertIn("| `state-path` |", content) + self.assertIn("MUBU_DAILY_FOLDER", content) + self.assertNotIn("Workspace/Daily tasks", content) + self.assertNotIn("Daily tasks resolution", content) + self.assertIn("## Version\n\n0.1.0", content) finally: output_path.unlink(missing_ok=True) diff --git a/mubu/agent-harness/cli_anything/mubu/tests/test_cli_entrypoint.py b/mubu/agent-harness/cli_anything/mubu/tests/test_cli_entrypoint.py index 9bf3a7ea4..ed592356d 100644 --- a/mubu/agent-harness/cli_anything/mubu/tests/test_cli_entrypoint.py +++ b/mubu/agent-harness/cli_anything/mubu/tests/test_cli_entrypoint.py @@ -7,15 +7,61 @@ import unittest from pathlib import Path from cli_anything.mubu.mubu_cli import expand_repl_aliases_with_state -from mubu_probe import DEFAULT_BACKUP_ROOT, DEFAULT_STORAGE_ROOT +from mubu_probe import ( + DEFAULT_BACKUP_ROOT, + DEFAULT_STORAGE_ROOT, + build_folder_indexes, + choose_current_daily_document, + load_document_metas, + load_folders, +) REPO_ROOT = Path(__file__).resolve().parents[4] -SAMPLE_DOC_REF = "workspace/Daily tasks/2026.03.18" -SAMPLE_NODE_ID = "node-demo1" +SAMPLE_DOC_REF = "workspace/reference docs/sample-doc" +SAMPLE_NODE_ID = "node-sample-1" HAS_LOCAL_DATA = DEFAULT_BACKUP_ROOT.is_dir() and DEFAULT_STORAGE_ROOT.is_dir() +def detect_daily_folder_ref() -> str | None: + if not HAS_LOCAL_DATA: + return None + + metas = load_document_metas(DEFAULT_STORAGE_ROOT) + folders = load_folders(DEFAULT_STORAGE_ROOT) + _, folder_paths = build_folder_indexes(folders) + docs_by_folder: dict[str, list[dict[str, object]]] = {} + for meta in metas: + folder_id = meta.get("folder_id") + if isinstance(folder_id, str): + docs_by_folder.setdefault(folder_id, []).append(meta) + + best_path: str | None = None + best_score = -1 + for folder in folders: + folder_id = folder.get("folder_id") + if not isinstance(folder_id, str): + continue + _, candidates = choose_current_daily_document(docs_by_folder.get(folder_id, [])) + if not candidates: + continue + folder_path = folder_paths.get(folder_id, "") + if not folder_path: + continue + score = max( + max(item.get("updated_at") or 0, item.get("created_at") or 0) + for item in candidates + ) + if score > best_score: + best_score = score + best_path = folder_path + return best_path + + +DETECTED_DAILY_FOLDER_REF = detect_daily_folder_ref() +HAS_DAILY_FOLDER = HAS_LOCAL_DATA and DETECTED_DAILY_FOLDER_REF is not None + + def resolve_cli() -> list[str]: installed = shutil.which("cli-anything-mubu") if installed: @@ -191,9 +237,16 @@ class CliEntrypointTests(unittest.TestCase): self.assertEqual(final.returncode, 0, msg=final.stderr) self.assertIn("Current node: ", final.stdout) - @unittest.skipUnless(HAS_LOCAL_DATA, "Mubu local data directories not found") + @unittest.skipUnless(HAS_DAILY_FOLDER, "Mubu local data or daily folder not found") def test_grouped_discover_daily_current_supports_global_json_flag(self): - result = self.run_cli(["--json", "discover", "daily-current"]) + missing = self.run_cli(["--json", "discover", "daily-current"]) + self.assertNotEqual(missing.returncode, 0) + self.assertIn("MUBU_DAILY_FOLDER", missing.stderr) + + result = self.run_cli( + ["--json", "discover", "daily-current"], + extra_env={"MUBU_DAILY_FOLDER": DETECTED_DAILY_FOLDER_REF}, + ) self.assertEqual(result.returncode, 0, msg=result.stderr) self.assertIn('"doc_path"', result.stdout) diff --git a/mubu/agent-harness/cli_anything/mubu/tests/test_full_e2e.py b/mubu/agent-harness/cli_anything/mubu/tests/test_full_e2e.py index fd842dc68..13ce83391 100644 --- a/mubu/agent-harness/cli_anything/mubu/tests/test_full_e2e.py +++ b/mubu/agent-harness/cli_anything/mubu/tests/test_full_e2e.py @@ -22,7 +22,15 @@ REPO_ROOT = Path(__file__).resolve().parents[4] # Import mubu_probe defaults for path detection sys.path.insert(0, str(REPO_ROOT / "agent-harness")) try: - from mubu_probe import DEFAULT_BACKUP_ROOT, DEFAULT_LOG_ROOT, DEFAULT_STORAGE_ROOT + from mubu_probe import ( + DEFAULT_BACKUP_ROOT, + DEFAULT_LOG_ROOT, + DEFAULT_STORAGE_ROOT, + build_folder_indexes, + choose_current_daily_document, + load_document_metas, + load_folders, + ) finally: sys.path.pop(0) @@ -31,7 +39,62 @@ HAS_LOCAL_DATA = ( and DEFAULT_STORAGE_ROOT.is_dir() ) -SKIP_REASON = "Mubu local data directories not found" + +def detect_daily_folder_ref() -> str | None: + if not HAS_LOCAL_DATA: + return None + + metas = load_document_metas(DEFAULT_STORAGE_ROOT) + folders = load_folders(DEFAULT_STORAGE_ROOT) + _, folder_paths = build_folder_indexes(folders) + docs_by_folder: dict[str, list[dict[str, object]]] = {} + for meta in metas: + folder_id = meta.get("folder_id") + if isinstance(folder_id, str): + docs_by_folder.setdefault(folder_id, []).append(meta) + + best_path: str | None = None + best_score = -1 + for folder in folders: + folder_id = folder.get("folder_id") + if not isinstance(folder_id, str): + continue + _, candidates = choose_current_daily_document(docs_by_folder.get(folder_id, [])) + if not candidates: + continue + folder_path = folder_paths.get(folder_id, "") + if not folder_path: + continue + score = max( + max(item.get("updated_at") or 0, item.get("created_at") or 0) + for item in candidates + ) + if score > best_score: + best_score = score + best_path = folder_path + return best_path + + +DETECTED_DAILY_FOLDER_REF = detect_daily_folder_ref() +HAS_DAILY_FOLDER = HAS_LOCAL_DATA and DETECTED_DAILY_FOLDER_REF is not None + +SKIP_REASON = "Mubu local data or a daily-style folder was not found" +LIVE_API_SKIP_MARKERS = ( + "CERTIFICATE_VERIFY_FAILED", + "SSLCertVerificationError", + "Hostname mismatch", + "request failed for https://api2.mubu.com", + "urlopen error", +) + + +def assert_cli_success_or_skip(testcase: unittest.TestCase, result: subprocess.CompletedProcess) -> None: + if result.returncode == 0: + return + details = "\n".join(part for part in (result.stdout, result.stderr) if part).strip() + if any(marker in details for marker in LIVE_API_SKIP_MARKERS): + testcase.skipTest(f"live Mubu API unavailable in this environment: {details.splitlines()[-1]}") + testcase.fail(details or f"CLI exited with status {result.returncode}") def resolve_cli() -> list[str]: @@ -41,13 +104,15 @@ def resolve_cli() -> list[str]: return [sys.executable, "-m", "cli_anything.mubu"] -@unittest.skipUnless(HAS_LOCAL_DATA, SKIP_REASON) +@unittest.skipUnless(HAS_DAILY_FOLDER, SKIP_REASON) class DiscoverE2ETests(unittest.TestCase): CLI_BASE = resolve_cli() - def run_cli(self, args: list[str]) -> subprocess.CompletedProcess: + def run_cli(self, args: list[str], extra_env: dict | None = None) -> subprocess.CompletedProcess: env = os.environ.copy() env["PYTHONPATH"] = str(REPO_ROOT) + os.pathsep + env.get("PYTHONPATH", "") + if extra_env: + env.update(extra_env) return subprocess.run( self.CLI_BASE + args, capture_output=True, @@ -80,22 +145,27 @@ class DiscoverE2ETests(unittest.TestCase): self.assertGreater(len(data), 0) def test_daily_current_returns_doc_path(self): - result = self.run_cli(["daily-current", "--json"]) + result = self.run_cli( + ["daily-current", "--json"], + extra_env={"MUBU_DAILY_FOLDER": DETECTED_DAILY_FOLDER_REF}, + ) self.assertEqual(result.returncode, 0, msg=result.stderr) data = json.loads(result.stdout) # Response wraps document info in a nested structure doc = data.get("document", data) self.assertIn("doc_path", doc) - self.assertIn("Daily tasks", doc["doc_path"]) + self.assertIn(DETECTED_DAILY_FOLDER_REF, doc["doc_path"]) -@unittest.skipUnless(HAS_LOCAL_DATA, SKIP_REASON) +@unittest.skipUnless(HAS_DAILY_FOLDER, SKIP_REASON) class InspectE2ETests(unittest.TestCase): CLI_BASE = resolve_cli() - def run_cli(self, args: list[str]) -> subprocess.CompletedProcess: + def run_cli(self, args: list[str], extra_env: dict | None = None) -> subprocess.CompletedProcess: env = os.environ.copy() env["PYTHONPATH"] = str(REPO_ROOT) + os.pathsep + env.get("PYTHONPATH", "") + if extra_env: + env.update(extra_env) return subprocess.run( self.CLI_BASE + args, capture_output=True, @@ -111,14 +181,17 @@ class InspectE2ETests(unittest.TestCase): self.assertIsInstance(data, list) def test_daily_nodes_returns_node_list(self): - result = self.run_cli(["daily-nodes", "--json"]) - self.assertEqual(result.returncode, 0, msg=result.stderr) + result = self.run_cli( + ["daily-nodes", "--json"], + extra_env={"MUBU_DAILY_FOLDER": DETECTED_DAILY_FOLDER_REF}, + ) + assert_cli_success_or_skip(self, result) data = json.loads(result.stdout) self.assertIn("nodes", data) self.assertIsInstance(data["nodes"], list) -@unittest.skipUnless(HAS_LOCAL_DATA, SKIP_REASON) +@unittest.skipUnless(HAS_DAILY_FOLDER, SKIP_REASON) class SessionE2ETests(unittest.TestCase): CLI_BASE = resolve_cli() @@ -138,35 +211,43 @@ class SessionE2ETests(unittest.TestCase): def test_session_use_daily_sets_current_doc(self): with tempfile.TemporaryDirectory() as tmpdir: - env = {"CLI_ANYTHING_MUBU_STATE_DIR": tmpdir} + env = { + "CLI_ANYTHING_MUBU_STATE_DIR": tmpdir, + "MUBU_DAILY_FOLDER": DETECTED_DAILY_FOLDER_REF, + } self.run_cli(["session", "use-daily"], extra_env=env) result = self.run_cli(["session", "status", "--json"], extra_env=env) self.assertEqual(result.returncode, 0, msg=result.stderr) data = json.loads(result.stdout) self.assertIsNotNone(data.get("current_doc")) - self.assertIn("Daily tasks", data["current_doc"]) + self.assertIn(DETECTED_DAILY_FOLDER_REF, data["current_doc"]) def test_repl_use_daily_then_daily_nodes(self): with tempfile.TemporaryDirectory() as tmpdir: - env = {"CLI_ANYTHING_MUBU_STATE_DIR": tmpdir} + env = { + "CLI_ANYTHING_MUBU_STATE_DIR": tmpdir, + "MUBU_DAILY_FOLDER": DETECTED_DAILY_FOLDER_REF, + } result = self.run_cli( [], input_text="use-daily\ndaily-nodes --json\nexit\n", extra_env=env, ) - self.assertEqual(result.returncode, 0, msg=result.stderr) + assert_cli_success_or_skip(self, result) self.assertIn('"nodes"', result.stdout) -@unittest.skipUnless(HAS_LOCAL_DATA, SKIP_REASON) +@unittest.skipUnless(HAS_DAILY_FOLDER, SKIP_REASON) class MutateDryRunE2ETests(unittest.TestCase): """Test mutation commands in dry-run mode (no --execute).""" CLI_BASE = resolve_cli() - def run_cli(self, args: list[str]) -> subprocess.CompletedProcess: + def run_cli(self, args: list[str], extra_env: dict | None = None) -> subprocess.CompletedProcess: env = os.environ.copy() env["PYTHONPATH"] = str(REPO_ROOT) + os.pathsep + env.get("PYTHONPATH", "") + if extra_env: + env.update(extra_env) return subprocess.run( self.CLI_BASE + args, capture_output=True, @@ -177,7 +258,11 @@ class MutateDryRunE2ETests(unittest.TestCase): def _resolve_daily_node(self) -> tuple[str, str]: """Helper: get a stable daily document reference and first node id.""" - result = self.run_cli(["daily-nodes", "--json"]) + result = self.run_cli( + ["daily-nodes", "--json"], + extra_env={"MUBU_DAILY_FOLDER": DETECTED_DAILY_FOLDER_REF}, + ) + assert_cli_success_or_skip(self, result) data = json.loads(result.stdout) doc = data.get("document", data) doc_ref = doc.get("doc_id") or doc["doc_path"] diff --git a/mubu/agent-harness/mubu_probe.py b/mubu/agent-harness/mubu_probe.py index c5bb84ee2..19b53576d 100644 --- a/mubu/agent-harness/mubu_probe.py +++ b/mubu/agent-harness/mubu_probe.py @@ -87,6 +87,31 @@ DAILY_TITLE_RE = re.compile(r"^\d{2}\.\d{1,2}\.\d{1,2}(?:-\d{1,2}(?:\.\d{1,2})?) DEFAULT_DAILY_EXCLUDE_KEYWORDS = ("模板", "template") +def configured_daily_folder_ref(env: Mapping[str, str] | None = None) -> str | None: + env = env or os.environ + value = env.get("MUBU_DAILY_FOLDER", "") + if not isinstance(value, str): + return None + resolved = value.strip() + return resolved or None + + +def resolve_daily_folder_ref( + folder_ref: str | None, + env: Mapping[str, str] | None = None, +) -> str: + value = (folder_ref or "").strip() + if value: + return value + configured = configured_daily_folder_ref(env=env) + if configured: + return configured + raise RuntimeError( + "daily folder reference required; pass explicitly " + "or set MUBU_DAILY_FOLDER" + ) + + def extract_plain_text(value: Any) -> str: if value is None: return "" @@ -1468,7 +1493,7 @@ def build_parser() -> argparse.ArgumentParser: "daily-current", help="Resolve the current daily document from one Daily-style folder.", ) - daily_current_parser.add_argument("folder_ref", nargs="?", default="Daily tasks") + daily_current_parser.add_argument("folder_ref", nargs="?") daily_current_parser.add_argument("--storage-root", type=Path, default=DEFAULT_STORAGE_ROOT) daily_current_parser.add_argument("--limit", type=int, default=5) daily_current_parser.add_argument( @@ -1482,7 +1507,7 @@ def build_parser() -> argparse.ArgumentParser: "daily-nodes", help="List live nodes from the current daily document in one step.", ) - daily_nodes_parser.add_argument("folder_ref", nargs="?", default="Daily tasks") + daily_nodes_parser.add_argument("folder_ref", nargs="?") daily_nodes_parser.add_argument("--storage-root", type=Path, default=DEFAULT_STORAGE_ROOT) daily_nodes_parser.add_argument("--api-host", default=DEFAULT_API_HOST) daily_nodes_parser.add_argument("--query", default=None, help="Filter nodes by plain-text substring.") @@ -1695,11 +1720,15 @@ def main(argv: list[str] | None = None) -> int: if args.command == "daily-current": metas = load_document_metas(args.storage_root) folders = load_folders(args.storage_root) - docs, folder, ambiguous = folder_documents(metas, folders, args.folder_ref) + try: + folder_ref = resolve_daily_folder_ref(args.folder_ref) + except RuntimeError as exc: + parser.error(str(exc)) + docs, folder, ambiguous = folder_documents(metas, folders, folder_ref) if folder is None: if ambiguous: - parser.error(ambiguous_error_message("folder", args.folder_ref, ambiguous, "path")) - parser.error(f"folder not found: {args.folder_ref}") + parser.error(ambiguous_error_message("folder", folder_ref, ambiguous, "path")) + parser.error(f"folder not found: {folder_ref}") selected, candidates = choose_current_daily_document( docs, @@ -1733,11 +1762,15 @@ def main(argv: list[str] | None = None) -> int: metas = load_document_metas(args.storage_root) folders = load_folders(args.storage_root) - docs, folder, ambiguous = folder_documents(metas, folders, args.folder_ref) + try: + folder_ref = resolve_daily_folder_ref(args.folder_ref) + except RuntimeError as exc: + parser.error(str(exc)) + docs, folder, ambiguous = folder_documents(metas, folders, folder_ref) if folder is None: if ambiguous: - parser.error(ambiguous_error_message("folder", args.folder_ref, ambiguous, "path")) - parser.error(f"folder not found: {args.folder_ref}") + parser.error(ambiguous_error_message("folder", folder_ref, ambiguous, "path")) + parser.error(f"folder not found: {folder_ref}") selected, candidates = choose_current_daily_document( docs, diff --git a/mubu/agent-harness/skill_generator.py b/mubu/agent-harness/skill_generator.py index 49b438862..3e1a62749 100644 --- a/mubu/agent-harness/skill_generator.py +++ b/mubu/agent-harness/skill_generator.py @@ -92,9 +92,14 @@ def extract_system_package(content: str) -> Optional[str]: def extract_version_from_setup(setup_path: Path) -> str: content = setup_path.read_text(encoding="utf-8") - match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content) - if match: - return match.group(1) + direct_match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content) + if direct_match: + return direct_match.group(1) + + constant_match = re.search(r'PACKAGE_VERSION\s*=\s*["\']([^"\']+)["\']', content) + if constant_match: + return constant_match.group(1) + return "1.0.0" @@ -185,8 +190,8 @@ def generate_examples(software_name: str, command_groups: list[CommandGroup]) -> examples.append( Example( title="Discover Current Daily Note", - description="Resolve the current daily note and emit JSON output for an agent.", - code=f"""cli-anything-{software_name} --json discover daily-current""", + description="Resolve the current daily note from an explicit folder reference.", + code=f"""cli-anything-{software_name} --json discover daily-current ''""", ) ) if "mutate" in group_names: @@ -196,7 +201,7 @@ def generate_examples(software_name: str, command_groups: list[CommandGroup]) -> description="Inspect the exact outgoing payload before a live mutation.", code=( f"cli-anything-{software_name} mutate update-text " - "'Workspace/Daily tasks/26.03.16' --node-id node-demo1 --text 'new text' --json" + "'' --node-id --text 'new text' --json" ), ) ) @@ -269,6 +274,7 @@ def generate_skill_md_simple(metadata: SkillMetadata) -> str: "- Python 3.10+", "- An active Mubu desktop session on this machine", "- Local Mubu profile data available to the CLI", + "- Set `MUBU_DAILY_FOLDER` if you want no-argument daily helpers", "", "## Entry Points", "", @@ -296,9 +302,9 @@ def generate_skill_md_simple(metadata: SkillMetadata) -> str: "## Recommended Agent Workflow", "", "```text", - "discover daily-current --json", + "discover daily-current '' --json", " ->", - "inspect daily-nodes --query '' --json", + "inspect daily-nodes '' --query '' --json", " ->", "session use-doc ''", " ->", @@ -316,6 +322,7 @@ def generate_skill_md_simple(metadata: SkillMetadata) -> str: "5. Prefer `--node-id` and `--parent-node-id` over text matching.", "6. `delete-node` removes the full targeted subtree.", "7. Even same-text updates can still advance document version history.", + "8. Pass a daily-folder reference explicitly or set `MUBU_DAILY_FOLDER` before using no-arg daily helpers.", "", "## Examples", "", diff --git a/mubu/agent-harness/templates/SKILL.md.template b/mubu/agent-harness/templates/SKILL.md.template index 12e7766ac..b0dab914f 100644 --- a/mubu/agent-harness/templates/SKILL.md.template +++ b/mubu/agent-harness/templates/SKILL.md.template @@ -21,6 +21,7 @@ pip install -e . - Python 3.10+ - An active Mubu desktop session on this machine - Local Mubu profile data available to the CLI +- Set `MUBU_DAILY_FOLDER` if you want no-argument daily helpers ## Entry Points @@ -48,9 +49,9 @@ When invoked without a subcommand, the CLI enters an interactive REPL session. ## Recommended Agent Workflow ```text -discover daily-current --json +discover daily-current '' --json -> -inspect daily-nodes --query '' --json +inspect daily-nodes '' --query '' --json -> session use-doc '' -> @@ -68,6 +69,7 @@ mutate update-text / create-child / delete-node --json 5. Prefer `--node-id` and `--parent-node-id` over text matching. 6. `delete-node` removes the full targeted subtree. 7. Even same-text updates can still advance document version history. +8. Pass a daily-folder reference explicitly or set `MUBU_DAILY_FOLDER` before using no-arg daily helpers. ## Examples