interlatch 0.23.1__tar.gz → 0.23.2__tar.gz
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.
- {interlatch-0.23.1 → interlatch-0.23.2}/CHANGELOG.md +41 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/PKG-INFO +2 -2
- {interlatch-0.23.1 → interlatch-0.23.2}/README.ja.md +1 -1
- {interlatch-0.23.1 → interlatch-0.23.2}/README.md +1 -1
- interlatch-0.23.2/changelog.d/agent-icons.md +3 -0
- interlatch-0.23.2/changelog.d/devices-admin-only.md +3 -0
- interlatch-0.23.2/changelog.d/windows-polish.md +5 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/devices.md +3 -2
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/install.md +12 -5
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/devices.md +2 -2
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/install.md +11 -4
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/cli.py +4 -2
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/config.py +11 -3
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/copilot_parser.py +8 -3
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/hub.py +37 -20
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/install.py +2 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/menubar.py +18 -3
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/migrate.py +2 -1
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/notify.py +57 -1
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/people.py +3 -3
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/server.py +55 -28
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/util.py +73 -0
- interlatch-0.23.2/src/chronicle/web/AGENT-ICONS.txt +29 -0
- interlatch-0.23.2/src/chronicle/web/agent-antigravity.svg +1 -0
- interlatch-0.23.2/src/chronicle/web/agent-bob.svg +1 -0
- interlatch-0.23.2/src/chronicle/web/agent-chatgpt.svg +1 -0
- interlatch-0.23.2/src/chronicle/web/agent-claude-ai.svg +1 -0
- interlatch-0.23.2/src/chronicle/web/agent-claude.svg +1 -0
- interlatch-0.23.2/src/chronicle/web/agent-codex.svg +1 -0
- interlatch-0.23.2/src/chronicle/web/agent-copilot.svg +1 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/app.css +22 -2
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/app.js +48 -6
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/panel.css +5 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/panel.js +7 -2
- interlatch-0.23.2/src/chronicle/windows.py +569 -0
- interlatch-0.23.2/tests/golden.py +33 -0
- interlatch-0.23.2/tests/goldens/cli.json +2786 -0
- interlatch-0.23.2/tests/goldens/diagram.json +3077 -0
- interlatch-0.23.2/tests/goldens/parsers/antigravity.json +564 -0
- interlatch-0.23.2/tests/goldens/parsers/bob.json +604 -0
- interlatch-0.23.2/tests/goldens/parsers/claude_code.json +557 -0
- interlatch-0.23.2/tests/goldens/parsers/codex.json +1400 -0
- interlatch-0.23.2/tests/goldens/parsers/copilot.json +445 -0
- interlatch-0.23.2/tests/goldens/systems.json +1019 -0
- interlatch-0.23.2/tests/test_cli_pins.py +191 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_projects.py +7 -9
- interlatch-0.23.2/tests/test_map_goldens.py +66 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_notify.py +29 -0
- interlatch-0.23.2/tests/test_parser_goldens.py +151 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_people_web.py +5 -0
- interlatch-0.23.2/tests/test_route_matrix.py +508 -0
- interlatch-0.23.2/tests/test_windows.py +465 -0
- interlatch-0.23.1/changelog.d/agents-chronicle-retired.md +0 -3
- interlatch-0.23.1/changelog.d/docs-review.md +0 -4
- interlatch-0.23.1/changelog.d/interlatch-vscode.md +0 -4
- interlatch-0.23.1/changelog.d/leaderboard.md +0 -4
- interlatch-0.23.1/changelog.d/move-keeps-commands.md +0 -4
- interlatch-0.23.1/changelog.d/restart-animation.md +0 -5
- interlatch-0.23.1/changelog.d/vscode-marketplace.md +0 -2
- interlatch-0.23.1/changelog.d/windows.md +0 -3
- interlatch-0.23.1/src/chronicle/windows.py +0 -266
- interlatch-0.23.1/tests/test_windows.py +0 -203
- {interlatch-0.23.1 → interlatch-0.23.2}/.github/dependabot.yml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/changelog.yml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/ci.yml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/docs.yml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/.github/workflows/release.yml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/.gitignore +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/.pre-commit-config.yaml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/.python-version +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/CONTRIBUTING.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/LICENSE +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/ROADMAP.ja.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/ROADMAP.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/SECURITY.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/changelog.d/README.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/dev.sh +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docker/.dockerignore +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docker/.env.example +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docker/Caddyfile +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docker/Dockerfile +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docker/compose.yaml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docker/wheels/.gitignore +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/README.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/_site/hooks.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/analysis.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/cli.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/configuration.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/dashboard.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/demo/make_demo.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/demo/record_demo.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/design/ja/learning.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/design/learning.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/development.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/diagrams/analysis.excalidraw.svg +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/diagrams/architecture.excalidraw.svg +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/docker.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/demo.gif +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/home.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/map.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/palette.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/images/session.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/README.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/analysis.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/cli.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/configuration.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/dashboard.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/development.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/docker.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/join-a-hub.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/mcp.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/moving-from-chronicle.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/postgres.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/privacy.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/sources.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/suggestions.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/troubleshooting.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/ja/vscode.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/join-a-hub.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/mcp.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/moving-from-chronicle.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/postgres.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/privacy.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/sources.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/suggestions.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/troubleshooting.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/docs/vscode.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/.firebaserc +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/.gitignore +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/README.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/firebase.json +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/firestore.indexes.json +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/leaderboard/firestore.rules +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/mkdocs.yml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/README.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/art.json +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/filing.html +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/loops.json +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/loops.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/members.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/render.html +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/render.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/scenes-more.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/icons3d/scenes.html +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/Chronicle.icns +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/Interlatch.spec +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/build.sh +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/chronicle_app.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/entitlements.plist +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon-3d.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon-blueprint-prompt.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon-blueprint.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/icon.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/macos/make_icon.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/packaging/release.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/pyproject.toml +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/__init__.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/__main__.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/access.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/agents.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/analyze.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/antigravity_parser.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/artifacts.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/bob_parser.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/cases.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/chat_import.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/chatgpt_export.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/claude_export.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/codex_cloud.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/codex_parser.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/connectors.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/container.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/db.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/desktop.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/diagram.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/digest.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/export_md.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/friction.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/glossary.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/groups.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/hooks.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/i18n.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/i18n_ja.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/ingest.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/instructions.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/ladder.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/leaderboard.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/learning.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/llm.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/mcp_server.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/mirror.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/news.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/parser.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/pg.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/pricing.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/providers.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/redact.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/reviews.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/screen.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/search.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/session_export.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/statusline.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/suggest.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/synthesize.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/systems.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/tailnet.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/team_store.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/update.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/views.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/PLEX-OFL.txt +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-book.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-camera.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-chip.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-clock.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-cloud.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-coins.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-container.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-dashboard.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-dex.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-document.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-flag.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-folder.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-gateway.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-gears.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-glitch.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-globe.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-laptop.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-log.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-lumi.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-magnifier.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-pip.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-plug.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-rack.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/art-tink.webp +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/boot.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/favicon.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/i18n.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon-180.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon-192.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon-512.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/icon.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/index.html +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/ja.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/learning.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/manifest.webmanifest +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/panel.html +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-mono-400.woff2 +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-mono-500.woff2 +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-mono-600.woff2 +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-400.woff2 +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-500.woff2 +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-600.woff2 +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/plex-sans-700.woff2 +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/web/rough.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/src/chronicle/worker.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/antigravity_fixture.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/codex_fixture.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/conftest.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/copilot_fixture.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/friction_fixture.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_analysis.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_antigravity.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_artifacts.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_bob_analysis.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_boot_web.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_case_files.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_cases.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_chat_import.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_codex.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_codex_analysis.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_codex_cloud.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_container.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_copilot_bob.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_dedicated_hub.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_dedupe.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_desktop.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_diagram.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_ee_boundary.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_friction.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_glossary.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_group_sharing.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_accept.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_news.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_projects_listed.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_purge.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_hub_sharing_race.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_i18n_web.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_ingest.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_instructions.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_integration.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_ladder.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_language.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_leaderboard.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_learning_glance.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_learning_interests.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_learning_web.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_leave_project.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_menubar.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_migrate.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_mirror.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_parser.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_people.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_people_hub.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_project_groups.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_providers.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_queue.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_regressions.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_release.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_screen.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_security.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_setup.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_statusline.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_suggest.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_suggest_cli.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_systems.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_team.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_team_home.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/test_update.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/tests/windows_smoke.py +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/uv.lock +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/.vscodeignore +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/LICENSE +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/README.md +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/extension.js +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/media/icon.png +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/media/interlatch.svg +0 -0
- {interlatch-0.23.1 → interlatch-0.23.2}/vscode-extension/package.json +0 -0
|
@@ -3,6 +3,47 @@
|
|
|
3
3
|
<!-- Don't add lines here: a pull request puts its line in a new file in changelog.d/ (see changelog.d/README.md),
|
|
4
4
|
and each release moves the files it took into a dated section below. -->
|
|
5
5
|
|
|
6
|
+
## 0.23.1 (2026-10-13)
|
|
7
|
+
|
|
8
|
+
- **`agents-chronicle` is retired:** Interlatch is published only as `interlatch` now, and `agents-chronicle` is no
|
|
9
|
+
longer on PyPI. An install under the old name gets no more updates: **Update** in the dashboard still moves it to
|
|
10
|
+
`interlatch`, or see [Moving from Chronicle](docs/moving-from-chronicle.md) for the commands.
|
|
11
|
+
|
|
12
|
+
## 0.23.0 (2026-10-13)
|
|
13
|
+
|
|
14
|
+
- **Leaderboard:** sign in with Google and compare your hours of agent time, sessions and tokens over 7, 30 or 90
|
|
15
|
+
days with everyone else who joined, under **Leaderboard** in the dashboard (`interlatch leaderboard` shows where
|
|
16
|
+
you stand). Only the totals from this computer's coding-agent sessions are sent, with your first name (rename it
|
|
17
|
+
any time) and your Google picture. No email, projects, prompts or titles. **Leave** deletes your row.
|
|
18
|
+
|
|
19
|
+
## 0.22.1 (2026-10-11)
|
|
20
|
+
|
|
21
|
+
This release never reached PyPI (its upload failed after the repository's rename); these changes first shipped
|
|
22
|
+
there in 0.23.0.
|
|
23
|
+
|
|
24
|
+
- **Docs:** the quick links atop the docs home (Docs, 日本語, English) open their pages instead of a 404, Install
|
|
25
|
+
lists every way to analyze sessions (Claude Code, Codex, IBM Bob with a Bob API key, or a model provider's API with
|
|
26
|
+
your own key), and links, **What's new** and **Download** under **Status › Updates** among them, point at the
|
|
27
|
+
repository's new name, [Chatixia-AI/interlatch](https://github.com/Chatixia-AI/interlatch).
|
|
28
|
+
- **Moving from Chronicle keeps the commands:** **Update** on an `agents-chronicle` install no longer leaves it
|
|
29
|
+
without the `interlatch` and `chronicle` commands, which kept the dashboard from coming back and stopped the
|
|
30
|
+
background sync, the session hooks and the MCP server. Already hit by it? [Troubleshooting](docs/troubleshooting.md#moving-from-chronicle)
|
|
31
|
+
has the two commands that put it right.
|
|
32
|
+
- **Restarting, animated:** while the dashboard restarts after an update, its dialog turns the arrows inside a ring
|
|
33
|
+
that swells and fades, with a sweep along its foot (with animations off they turn slowly and the rest holds still).
|
|
34
|
+
The last places that still said Chronicle say Interlatch: the diagrams in the docs, the log file's lines, the
|
|
35
|
+
Postgres quick start, and the names the dashboard and menu-bar panel use for their requests and what the browser
|
|
36
|
+
remembers. Settings a browser kept under the old names carry over, and a sign-in to a hub from before stays valid.
|
|
37
|
+
- **Windows:** `uv tool install interlatch` and `interlatch install` now work on Windows 10 and 11. Task Scheduler runs
|
|
38
|
+
the 15-minute sync and keeps the dashboard up, without a window, and the hooks run in Git Bash and in PowerShell
|
|
39
|
+
([Windows](docs/install.md#windows)). A Windows computer can't join a team hub yet.
|
|
40
|
+
- **The VS Code extension is now Interlatch:** a new extension, `Chatixia-AI.interlatch` 0.3.0, with the sections
|
|
41
|
+
**Interlatch: This File** and **Interlatch: Files in Workspace**. Your `chronicle.*` settings still apply until you
|
|
42
|
+
set their `interlatch.*` counterparts. Uninstall the Chronicle extension after installing it; Interlatch offers to,
|
|
43
|
+
once ([VS Code extension](docs/vscode.md)).
|
|
44
|
+
- **The VS Code extension on the Marketplace:** install **Interlatch** from VS Code's Extensions view
|
|
45
|
+
([VS Code extension](docs/vscode.md#install)). Releases publish it whenever its own version goes up.
|
|
46
|
+
|
|
6
47
|
## 0.22.0 (2026-10-10)
|
|
7
48
|
|
|
8
49
|
- **Your Chronicle setup moves over by itself:** the first time the dashboard or the background sync starts,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: interlatch
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.2
|
|
4
4
|
Summary: Shared memory for your coding agents: records every session (Claude Code, Codex, Copilot, Bob, Antigravity) locally and keeps what they learned, each lesson linked to the session it came from.
|
|
5
5
|
Project-URL: Homepage, https://interlatch.com
|
|
6
6
|
Project-URL: Documentation, https://interlatch.com/docs/
|
|
@@ -191,7 +191,7 @@ sessions are archived and wait in the analysis queue.
|
|
|
191
191
|
|
|
192
192
|
**Windows or Linux?** The desktop app is macOS only. On Windows, `uv tool install interlatch` and `interlatch install`
|
|
193
193
|
work as on a Mac, with Task Scheduler running the sync and the dashboard ([Windows](https://github.com/Chatixia-AI/interlatch/blob/main/docs/install.md#windows)); a
|
|
194
|
-
Windows computer
|
|
194
|
+
Windows computer joining a team hub is untested yet. On Linux, `interlatch install` runs the sync and the dashboard
|
|
195
195
|
as systemd user units, so a Linux box can be the [hub](https://github.com/Chatixia-AI/interlatch/blob/main/docs/devices.md#a-linux-hub) for your other computers. A
|
|
196
196
|
team's hub also runs [in Docker](https://github.com/Chatixia-AI/interlatch/blob/main/docs/docker.md).
|
|
197
197
|
|
|
@@ -154,7 +154,7 @@ Ollama。**Status › Analysis** または `interlatch config set analysis.backe
|
|
|
154
154
|
|
|
155
155
|
**Windows や Linux は?** デスクトップアプリは macOS 専用です。Windows では `uv tool install interlatch` と `interlatch install` が
|
|
156
156
|
Mac と同じように使え、同期とダッシュボードはタスク スケジューラが動かします([Windows](docs/ja/install.md#windows))。
|
|
157
|
-
Windows
|
|
157
|
+
Windows のコンピューターからのチームのハブへの参加はまだ未検証です。Linux では `interlatch install` が同期とダッシュボードを systemd の
|
|
158
158
|
ユーザーユニットとして動かすので、Linux マシンをほかのコンピューターの[ハブ](docs/ja/devices.md#linux-のハブ)にできます。
|
|
159
159
|
チームのハブは [Docker](docs/ja/docker.md) でも動かせます。
|
|
160
160
|
|
|
@@ -156,7 +156,7 @@ sessions are archived and wait in the analysis queue.
|
|
|
156
156
|
|
|
157
157
|
**Windows or Linux?** The desktop app is macOS only. On Windows, `uv tool install interlatch` and `interlatch install`
|
|
158
158
|
work as on a Mac, with Task Scheduler running the sync and the dashboard ([Windows](docs/install.md#windows)); a
|
|
159
|
-
Windows computer
|
|
159
|
+
Windows computer joining a team hub is untested yet. On Linux, `interlatch install` runs the sync and the dashboard
|
|
160
160
|
as systemd user units, so a Linux box can be the [hub](docs/devices.md#a-linux-hub) for your other computers. A
|
|
161
161
|
team's hub also runs [in Docker](docs/docker.md).
|
|
162
162
|
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
- **Windows polish:** The dashboard can show an Interlatch icon in the notification area (turn on the menu-bar icon
|
|
2
|
+
under **Status › Recording**, or run `interlatch ui --menu-bar`): click it to open the dashboard, right-click for Sync
|
|
3
|
+
Now and Quit, which stops the dashboard until you next sign in. Release notifications show as Windows toasts. The VS
|
|
4
|
+
Code extension's file history and folder list, hub folders and projects, and a person's projects now match Windows
|
|
5
|
+
paths however they're spelled (`C:\work\app`, `c:/work/app`).
|
|
@@ -602,5 +602,6 @@ corrupted.
|
|
|
602
602
|
your phone, so the hub's tailnet address is out of their reach. Claude Code's Remote Control, from the phone into
|
|
603
603
|
a session on the hub, does have Interlatch's tools.
|
|
604
604
|
- **A copy to read offline** on the other computers.
|
|
605
|
-
- **A Windows computer in a hub.** Interlatch runs on Windows on its own ([Windows](install.md#windows)),
|
|
606
|
-
|
|
605
|
+
- **A Windows computer in a hub.** Interlatch runs on Windows on its own ([Windows](install.md#windows)), and the hub
|
|
606
|
+
matches Windows folders however they're spelled (`C:\work\app`, `c:/work/app`), but joining one from Windows is
|
|
607
|
+
untested.
|
|
@@ -117,7 +117,7 @@ launchd agents do not run alongside it (harmless, but redundant).
|
|
|
117
117
|
|
|
118
118
|
## Windows
|
|
119
119
|
|
|
120
|
-
On Windows 10 and 11 Interlatch is the command-line install; there is no desktop app
|
|
120
|
+
On Windows 10 and 11 Interlatch is the command-line install; there is no desktop app. In PowerShell:
|
|
121
121
|
|
|
122
122
|
```powershell
|
|
123
123
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # uv, if you don't have it yet
|
|
@@ -141,13 +141,20 @@ The hooks it adds to Claude Code run `C:/Users/<you>/.local/bin/interlatch.exe h
|
|
|
141
141
|
hook in Git Bash, or in PowerShell without Git for Windows, and forward slashes read the same in both. A folder with
|
|
142
142
|
a space in its name is written in its short form (`C:/Users/ADALOV~1/…`).
|
|
143
143
|
|
|
144
|
-
Interlatch reads and writes text in UTF-8, as the coding agents write it, whatever language Windows is set to.
|
|
144
|
+
Interlatch reads and writes text in UTF-8, as the coding agents write it, whatever language Windows is set to. It
|
|
145
|
+
matches a Windows path however an agent or VS Code spells it (`C:\work\app`, `c:/work/app`), so the
|
|
146
|
+
[VS Code extension](vscode.md)'s file history and folder list work there too.
|
|
147
|
+
|
|
148
|
+
The **Menu-bar icon** switch under **Settings › Status › Recording** puts Interlatch's icon in the notification area
|
|
149
|
+
(by the clock) while the Dashboard task runs the dashboard; `interlatch ui --menu-bar` shows it from a terminal. Click
|
|
150
|
+
it to open the dashboard, or right-click for **Sync Now** and **Quit Interlatch**. Quit stops the dashboard and the icon
|
|
151
|
+
until you next sign in, run `interlatch install`, or run `interlatch ui`; the Sync task keeps recording. A new
|
|
152
|
+
version's [notification](#updating) (`[updates] notify`) shows as a Windows notification.
|
|
145
153
|
|
|
146
154
|
Not on Windows yet:
|
|
147
155
|
|
|
148
|
-
- **Joining a team hub
|
|
149
|
-
|
|
150
|
-
- **Desktop notifications** of a new version, and the menu-bar icon.
|
|
156
|
+
- **Joining a team hub** is untested from Windows: the hub matches Windows folders however they're spelled, but no
|
|
157
|
+
team has run one with a Windows computer yet.
|
|
151
158
|
|
|
152
159
|
## The menu-bar icon
|
|
153
160
|
|
|
@@ -598,5 +598,5 @@ sudo tailscale set --operator=$USER # `interlatch tailnet on` が Tailscale
|
|
|
598
598
|
接続するので、ハブの tailnet 内のアドレスには届きません。スマートフォンから Claude Code の Remote Control でハブのセッションを
|
|
599
599
|
操作すれば、Interlatch のツールを使えます。
|
|
600
600
|
- **ほかのコンピューターでオフラインで読めるコピー。**
|
|
601
|
-
- **ハブに参加する Windows のコンピューター。** Interlatch は Windows
|
|
602
|
-
|
|
601
|
+
- **ハブに参加する Windows のコンピューター。** Interlatch は Windows 単体では動き([Windows](install.md#windows))、
|
|
602
|
+
ハブは Windows のフォルダーを書き方(`C:\work\app`、`c:/work/app`)にかかわらず照合しますが、Windows からの参加は未検証です。
|
|
@@ -113,7 +113,7 @@ Claude Code と Mac 本体には、次の 4 つを設定します(それぞれ
|
|
|
113
113
|
|
|
114
114
|
## Windows
|
|
115
115
|
|
|
116
|
-
Windows 10 と 11 では、Interlatch
|
|
116
|
+
Windows 10 と 11 では、Interlatch はコマンドライン版を使います。デスクトップアプリはありません。PowerShell で:
|
|
117
117
|
|
|
118
118
|
```powershell
|
|
119
119
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # uv, if you don't have it yet
|
|
@@ -138,12 +138,19 @@ Claude Code に追加するフックは `C:/Users/<you>/.local/bin/interlatch.ex
|
|
|
138
138
|
名前に空白を含むフォルダーは短い形式(`C:/Users/ADALOV~1/…`)で書きます。
|
|
139
139
|
|
|
140
140
|
Interlatch は、Windows の表示言語にかかわらず、コーディングエージェントが書くのと同じ UTF-8 でテキストを読み書きします。
|
|
141
|
+
Windows のパスは、エージェントや VS Code の書き方(`C:\work\app`、`c:/work/app`)にかかわらず同じものとして扱うので、
|
|
142
|
+
[VS Code 拡張](vscode.md)のファイル履歴とフォルダーの一覧も使えます。
|
|
143
|
+
|
|
144
|
+
**Settings › Status › Recording** の **Menu-bar icon** スイッチをオンにすると、Dashboard タスクがダッシュボードを動かしている間、
|
|
145
|
+
通知領域(時計の横)に Interlatch のアイコンが出ます。ターミナルからは `interlatch ui --menu-bar` で表示できます。クリックで
|
|
146
|
+
ダッシュボードを開き、右クリックで **Sync Now** と **Quit Interlatch** を選べます。Quit すると、次にサインインするか、
|
|
147
|
+
`interlatch install` か `interlatch ui` を実行するまで、ダッシュボードとアイコンは止まります(Sync タスクは記録を続けます)。
|
|
148
|
+
新しいバージョンの[通知](#アップデート)(`[updates] notify`)は Windows の通知として表示されます。
|
|
141
149
|
|
|
142
150
|
Windows ではまだできないこと:
|
|
143
151
|
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
- 新しいバージョンの**デスクトップ通知**と、メニューバーアイコン。
|
|
152
|
+
- **チームのハブへの参加**は Windows から未検証です。ハブは Windows のフォルダーを書き方にかかわらず照合しますが、
|
|
153
|
+
Windows のコンピューターを含むチームではまだ運用されていません。
|
|
147
154
|
|
|
148
155
|
## メニューバーアイコン
|
|
149
156
|
|
|
@@ -2364,6 +2364,7 @@ def _hub_leave_project(cfg, console, args) -> int:
|
|
|
2364
2364
|
from pathlib import Path
|
|
2365
2365
|
|
|
2366
2366
|
from . import hub
|
|
2367
|
+
from .util import normalize_project_path
|
|
2367
2368
|
|
|
2368
2369
|
if not cfg.is_spoke:
|
|
2369
2370
|
console.print("This computer has not joined a hub.")
|
|
@@ -2376,14 +2377,15 @@ def _hub_leave_project(cfg, console, args) -> int:
|
|
|
2376
2377
|
known = hub.handshake(cfg).get("projects") or []
|
|
2377
2378
|
except hub.HubError:
|
|
2378
2379
|
known = (hub.last_folders(cfg) or {}).get("projects") or []
|
|
2379
|
-
known += [{"path": x, "name": Path(x).name} for x in cfg.hub_left
|
|
2380
|
+
known += [{"path": x, "name": Path(x).name} for x in cfg.hub_left
|
|
2381
|
+
if x not in {normalize_project_path(k.get("path")) for k in known}]
|
|
2380
2382
|
project, problem = _pick_project(known, wanted)
|
|
2381
2383
|
if problem:
|
|
2382
2384
|
console.print(problem, highlight=False)
|
|
2383
2385
|
return 1
|
|
2384
2386
|
name = project.get("name") or Path(project["path"]).name
|
|
2385
2387
|
if args.action == "rejoin":
|
|
2386
|
-
if project["path"] not in cfg.hub_left:
|
|
2388
|
+
if normalize_project_path(project["path"]) not in cfg.hub_left:
|
|
2387
2389
|
console.print(f"This computer hasn't left {name}.", highlight=False)
|
|
2388
2390
|
return 0
|
|
2389
2391
|
cfg = hub.rejoin_project(cfg, project["path"])
|
|
@@ -11,6 +11,8 @@ from collections.abc import Mapping
|
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
+
from .util import is_abs_path, normalize_project_path, path_kind
|
|
15
|
+
|
|
14
16
|
DEFAULT_CONFIG_TOML = """\
|
|
15
17
|
# Interlatch configuration.
|
|
16
18
|
# Edit freely; changes apply on the next sync / worker run.
|
|
@@ -441,6 +443,12 @@ def _pick(cls, values: dict):
|
|
|
441
443
|
return cls(**{k: v for k, v in values.items() if k in allowed})
|
|
442
444
|
|
|
443
445
|
|
|
446
|
+
def _map_path(path: str) -> str:
|
|
447
|
+
"""A [hub] path_map side: a Windows folder as normalize_project_path spells it; a POSIX one without a trailing /
|
|
448
|
+
("/" becomes "", which maps every path)."""
|
|
449
|
+
return normalize_project_path(path) if path_kind(path) in ("drive", "unc") else path.rstrip("/")
|
|
450
|
+
|
|
451
|
+
|
|
444
452
|
def load_config(home: Path | None = None, *, create: bool = True) -> Config:
|
|
445
453
|
home = (home or chronicle_home()).expanduser()
|
|
446
454
|
path = home / "config.toml"
|
|
@@ -498,12 +506,12 @@ def load_config(home: Path | None = None, *, create: bool = True) -> Config:
|
|
|
498
506
|
server_menu_bar=bool(server.get("menu_bar", False)),
|
|
499
507
|
server_trusted_proxies=[str(x).strip() for x in server.get("trusted_proxies", ["127.0.0.1", "::1"]) or [] if str(x).strip()],
|
|
500
508
|
hub_url=str(hub.get("url") or "").strip().rstrip("/"),
|
|
501
|
-
hub_path_map={str(k)
|
|
502
|
-
hub_folders={str(Path(str(k)).expanduser())
|
|
509
|
+
hub_path_map={_map_path(str(k)): _map_path(str(v)) for k, v in path_map.items()} if isinstance(path_map, dict) else {},
|
|
510
|
+
hub_folders={normalize_project_path(str(Path(str(k)).expanduser())): normalize_project_path(str(v)) for k, v in folders.items()
|
|
503
511
|
if str(k).strip() and str(v).strip()} if isinstance(folders, dict) else {},
|
|
504
512
|
hub_share=share if share in SHARE_MODES else "everything",
|
|
505
513
|
hub_all_folders=hub.get("all_folders") is True, # only an explicit true shares every folder
|
|
506
|
-
hub_left=sorted({str(x)
|
|
514
|
+
hub_left=sorted({normalize_project_path(str(x)) for x in hub.get("left") or [] if is_abs_path(str(x))})
|
|
507
515
|
if isinstance(hub.get("left"), list) else [],
|
|
508
516
|
hub_accept="everything" if accept == "everything" else "knowledge", # a typo never lets transcripts in
|
|
509
517
|
hub_store=store if store in STORES else "",
|
|
@@ -47,12 +47,17 @@ def shift_iso(ts: str | None, ms: float) -> str | None:
|
|
|
47
47
|
return (dt + timedelta(milliseconds=ms)).isoformat(timespec="milliseconds").replace("+00:00", "Z") if dt else ts
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
_URI_DRIVE = re.compile(r"/[A-Za-z]:(?:/|$)") # a Windows drive as a URI's path spells it
|
|
51
|
+
|
|
52
|
+
|
|
50
53
|
def uri_path(uri) -> str | None:
|
|
51
|
-
"""file:///Users/x or a VS Code URI object -> /Users/x."""
|
|
54
|
+
"""file:///Users/x or a VS Code URI object -> /Users/x. On Windows file:///c%3A/x -> c:/x, not /c:/x."""
|
|
52
55
|
if isinstance(uri, dict):
|
|
53
|
-
|
|
56
|
+
p = uri.get("fsPath") or uri.get("path")
|
|
57
|
+
return p[1:] if isinstance(p, str) and _URI_DRIVE.match(p) else p
|
|
54
58
|
if isinstance(uri, str) and uri.startswith("file:"):
|
|
55
|
-
|
|
59
|
+
p = unquote(urlparse(uri).path)
|
|
60
|
+
return (p[1:] if _URI_DRIVE.match(p) else p) or None
|
|
56
61
|
if isinstance(uri, str) and uri.startswith("vscode-remote:"): # vscode-remote://ssh-remote+host/path -> host:/path
|
|
57
62
|
u = urlparse(uri)
|
|
58
63
|
host = unquote(u.netloc).split("+", 1)[-1]
|
|
@@ -49,7 +49,7 @@ from urllib.parse import quote, urlencode, urlparse
|
|
|
49
49
|
|
|
50
50
|
from . import __version__, news
|
|
51
51
|
from .config import Config
|
|
52
|
-
from .util import utcnow_iso
|
|
52
|
+
from .util import is_abs_path, normalize_project_path, path_kind, path_variants, utcnow_iso
|
|
53
53
|
|
|
54
54
|
log = logging.getLogger("interlatch.hub")
|
|
55
55
|
|
|
@@ -388,11 +388,14 @@ def spoke_repos(cfg: Config, roots: list[Root]) -> dict[str, list[str]]:
|
|
|
388
388
|
|
|
389
389
|
# ------------------------------------------------------------------ folders added by hand
|
|
390
390
|
def under(path: str | None, folder: str) -> bool:
|
|
391
|
-
"""`path` is `folder` or somewhere below it.
|
|
392
|
-
|
|
391
|
+
"""`path` is `folder` or somewhere below it, compared as normalize_project_path spells them (C:\\x is under c:/).
|
|
392
|
+
A POSIX folder never holds a Windows path (a drive or a UNC share), nor the other way round; no folder (""), nothing."""
|
|
393
|
+
if not path or not folder:
|
|
393
394
|
return False
|
|
394
|
-
folder =
|
|
395
|
-
|
|
395
|
+
path, folder = normalize_project_path(path), normalize_project_path(folder)
|
|
396
|
+
if path_kind(path) != path_kind(folder):
|
|
397
|
+
return False
|
|
398
|
+
return path == folder or path.startswith(folder if folder.endswith("/") else folder + "/")
|
|
396
399
|
|
|
397
400
|
|
|
398
401
|
def clean_folders(raw) -> dict[str, str]:
|
|
@@ -402,8 +405,8 @@ def clean_folders(raw) -> dict[str, str]:
|
|
|
402
405
|
out: dict[str, str] = {}
|
|
403
406
|
for k, v in list(raw.items())[:MAX_FOLDERS]:
|
|
404
407
|
k, v = str(k).strip(), str(v).strip()
|
|
405
|
-
if k
|
|
406
|
-
out[k
|
|
408
|
+
if is_abs_path(k) and is_abs_path(v) and len(k) <= 1024 and len(v) <= 1024 and "\0" not in k + v:
|
|
409
|
+
out[normalize_project_path(k)] = normalize_project_path(v)
|
|
407
410
|
return out
|
|
408
411
|
|
|
409
412
|
|
|
@@ -435,8 +438,8 @@ def clean_left(raw) -> list[str]:
|
|
|
435
438
|
"""A spoke's [hub] left as the hub accepts it: absolute project paths, a bounded number of them."""
|
|
436
439
|
if not isinstance(raw, list):
|
|
437
440
|
return []
|
|
438
|
-
return sorted({x
|
|
439
|
-
if isinstance(x, str) and x
|
|
441
|
+
return sorted({normalize_project_path(x) for x in raw[:MAX_FOLDERS]
|
|
442
|
+
if isinstance(x, str) and is_abs_path(x) and len(x) <= 1024 and "\0" not in x})
|
|
440
443
|
|
|
441
444
|
|
|
442
445
|
def hub_projects(conn, limit: int = 1000, only: list[str] | None = None) -> list[dict]:
|
|
@@ -874,7 +877,7 @@ def group_routes(cfg: Config, conn=None) -> GroupRoutes:
|
|
|
874
877
|
linked = {g["id"]: g["hub_project"] for g in groups(conn) if g.get("hub_project")}
|
|
875
878
|
paths = [r[0] for r in conn.execute("SELECT DISTINCT project_path FROM sessions WHERE project_path IS NOT NULL")]
|
|
876
879
|
group_of = {p: gid for p, (gid, _) in assign(conn, paths).items()}
|
|
877
|
-
active = {p: (linked[gid], gid) for p, gid in group_of.items() if gid in linked and p
|
|
880
|
+
active = {p: (linked[gid], gid) for p, gid in group_of.items() if gid in linked and is_abs_path(p)}
|
|
878
881
|
kept = {r[0]: r[1] for r in conn.execute("SELECT path, hub_project FROM group_shares") if r[0] not in active}
|
|
879
882
|
finally:
|
|
880
883
|
if own:
|
|
@@ -1149,11 +1152,12 @@ def _shared_records(cfg: Config, have: dict, *, scope: list[str] | None = None,
|
|
|
1149
1152
|
else routes.decide(rec["project_path"], cfg.hub_folders)
|
|
1150
1153
|
if via is not None:
|
|
1151
1154
|
goes, ok = via
|
|
1155
|
+
goes = normalize_project_path(goes) # [hub] left and a person's projects are spelled so
|
|
1152
1156
|
if not ok or goes in left or (scope is not None and goes not in scope):
|
|
1153
1157
|
excluded += 1 # kept out of the group, out of it now, left, or a project this person doesn't share
|
|
1154
1158
|
continue
|
|
1155
1159
|
elif limited or left:
|
|
1156
|
-
goes = destination(cfg, rec["project_path"], rec["remote"], remotes or {})
|
|
1160
|
+
goes = normalize_project_path(destination(cfg, rec["project_path"], rec["remote"], remotes or {}))
|
|
1157
1161
|
if goes in left or (limited and (goes is None or (scope is not None and goes not in scope))):
|
|
1158
1162
|
excluded += 1 # not one of the hub's projects, one this person doesn't share, or one it left
|
|
1159
1163
|
continue
|
|
@@ -1199,7 +1203,7 @@ def hub_project_of(cfg: Config, paths, conn=None) -> dict[str, dict]:
|
|
|
1199
1203
|
top = max((f for f in repos if under(path, f)), key=len, default=None)
|
|
1200
1204
|
via = None if top else routes.decide(path, cfg.hub_folders)
|
|
1201
1205
|
goes = repos[top] if top else (via[0] if via[1] else None) if via else destination(cfg, path, None, {})
|
|
1202
|
-
if goes and goes not in cfg.hub_left:
|
|
1206
|
+
if goes and normalize_project_path(goes) not in cfg.hub_left:
|
|
1203
1207
|
out[path] = {"path": goes, "name": names.get(goes) or Path(goes).name or goes}
|
|
1204
1208
|
return out
|
|
1205
1209
|
|
|
@@ -1483,19 +1487,21 @@ def add_folder(cfg: Config, folder: str, project: dict, hello: dict) -> tuple[Co
|
|
|
1483
1487
|
`project`; one this computer left is joined again. `hello`: the hub's answer to handshake(), for the git remotes
|
|
1484
1488
|
it knows. Returns the reloaded config and, when nothing changed because the folder's repository already goes
|
|
1485
1489
|
there by its git remote, that remote. Raises FolderError when the folder can't go there."""
|
|
1486
|
-
|
|
1490
|
+
folder = normalize_project_path(folder)
|
|
1491
|
+
project_path = normalize_project_path(project["path"])
|
|
1492
|
+
name = project.get("name") or Path(project_path).name
|
|
1487
1493
|
if not Path(folder).is_dir():
|
|
1488
1494
|
raise FolderError("{folder} is not a folder.", folder=folder)
|
|
1489
1495
|
info = git_info(folder)
|
|
1490
1496
|
owner = (hello.get("remotes") or {}).get(info[1]) if info else None
|
|
1491
|
-
if owner and owner !=
|
|
1497
|
+
if owner and normalize_project_path(owner) != project_path:
|
|
1492
1498
|
raise FolderError("{folder} is in the git repository {remote}, which the hub files under {owner}. A repository "
|
|
1493
1499
|
"belongs to one project, so its sessions can't go to {name}.",
|
|
1494
1500
|
folder=folder, remote=info[1], owner=Path(owner).name, name=name)
|
|
1495
|
-
left = [x for x in cfg.hub_left if x !=
|
|
1501
|
+
left = [x for x in cfg.hub_left if x != project_path]
|
|
1496
1502
|
if owner:
|
|
1497
1503
|
return _save_hub(cfg, left=left) if left != cfg.hub_left else cfg, info[1]
|
|
1498
|
-
return _save_hub(cfg, folders={**cfg.hub_folders, folder:
|
|
1504
|
+
return _save_hub(cfg, folders={**cfg.hub_folders, folder: project_path},
|
|
1499
1505
|
left=left if left != cfg.hub_left else None), None
|
|
1500
1506
|
|
|
1501
1507
|
|
|
@@ -1505,6 +1511,7 @@ def remove_folder(cfg: Config, folder: str, client: HubClient | None = None) ->
|
|
|
1505
1511
|
shares knowledge first has the hub take back what it shared from there (withdraw); one that sends transcripts
|
|
1506
1512
|
leaves them to the hub, which files them by their git remote or their own folder again. Returns the reloaded
|
|
1507
1513
|
config and how many sessions the hub took back (None: it wasn't asked)."""
|
|
1514
|
+
folder = normalize_project_path(folder)
|
|
1508
1515
|
if folder not in cfg.hub_folders:
|
|
1509
1516
|
raise FolderError("{folder} was not added.", folder=folder)
|
|
1510
1517
|
taken = None
|
|
@@ -1539,12 +1546,14 @@ def leave_project(cfg: Config, project: str) -> Config:
|
|
|
1539
1546
|
if not cfg.shares_knowledge:
|
|
1540
1547
|
raise FolderError("Only a computer that shares knowledge can leave a project: one that sends its transcripts "
|
|
1541
1548
|
"sends them all.")
|
|
1549
|
+
project = normalize_project_path(project)
|
|
1542
1550
|
return cfg if project in cfg.hub_left else _save_hub(cfg, left=[*cfg.hub_left, project])
|
|
1543
1551
|
|
|
1544
1552
|
|
|
1545
1553
|
@_one_change
|
|
1546
1554
|
def rejoin_project(cfg: Config, project: str) -> Config:
|
|
1547
1555
|
"""Undo leave_project: sessions filed under `project` are shared again, and its teammates' lessons come back."""
|
|
1556
|
+
project = normalize_project_path(project)
|
|
1548
1557
|
return _save_hub(cfg, left=[x for x in cfg.hub_left if x != project]) if project in cfg.hub_left else cfg
|
|
1549
1558
|
|
|
1550
1559
|
|
|
@@ -1963,10 +1972,16 @@ def team_lessons(cfg: Config, body: dict, person: dict | None = None, conn=None)
|
|
|
1963
1972
|
except HubError as exc:
|
|
1964
1973
|
log.warning("team store: this hub's own sessions not shared: %s", exc)
|
|
1965
1974
|
remotes = [normalize_remote(x) or x for x in body.get("remotes") or [] if isinstance(x, str) and len(x) <= 300][:500]
|
|
1966
|
-
projects = [x for x in body.get("projects") or []
|
|
1975
|
+
projects = [normalize_project_path(x) for x in body.get("projects") or []
|
|
1976
|
+
if isinstance(x, str) and is_abs_path(x) and len(x) <= 1024]
|
|
1967
1977
|
left = clean_left(body.get("left")) # projects the computer left: none of their lessons
|
|
1968
1978
|
within = projects_of(person)
|
|
1969
|
-
|
|
1979
|
+
|
|
1980
|
+
def spelled(paths): # a Windows project the store may hold as C:\x or c:/x (filed before it was normalized)
|
|
1981
|
+
return None if paths is None else list(dict.fromkeys(v for p in paths for v in path_variants(p)))
|
|
1982
|
+
|
|
1983
|
+
data = _in_store(lambda: store.lessons_for(machine_id, remotes, spelled(projects[:MAX_FOLDERS]), within=spelled(within),
|
|
1984
|
+
left=spelled(left)))
|
|
1970
1985
|
version = hashlib.sha256(json.dumps(data, sort_keys=True, default=str).encode()).hexdigest()[:32]
|
|
1971
1986
|
if body.get("version") == version:
|
|
1972
1987
|
return {"team": True, "version": version, "unchanged": True}
|
|
@@ -2243,13 +2258,15 @@ class ProjectResolver:
|
|
|
2243
2258
|
def resolve(self, machine_id: str | None, path: str | None, remote: str | None = None) -> str | None:
|
|
2244
2259
|
if not path or not machine_id or machine_id == self.me:
|
|
2245
2260
|
return self.project_of(path)
|
|
2261
|
+
if path_kind(path) in ("drive", "unc"): # another computer's Windows folder: spelled one way, as its [hub]
|
|
2262
|
+
path = normalize_project_path(path) # folders and left are, so C:\app and c:/app are one project here
|
|
2246
2263
|
return self.project_of(self._resolve(machine_id, path, remote))
|
|
2247
2264
|
|
|
2248
2265
|
def _resolve(self, machine_id: str, path: str, remote: str | None) -> str:
|
|
2249
2266
|
norm, top = normalize_remote(remote), None
|
|
2250
2267
|
for repo_top, repo_remote in sorted(self.repos(machine_id).values(), key=lambda v: -len(v[0])):
|
|
2251
|
-
if path
|
|
2252
|
-
top, norm = repo_top, norm or repo_remote
|
|
2268
|
+
if under(path, repo_top):
|
|
2269
|
+
top, norm = normalize_project_path(repo_top), norm or repo_remote
|
|
2253
2270
|
break
|
|
2254
2271
|
local_top = self.local_remotes().get(norm) if norm else None
|
|
2255
2272
|
added = self.folders(machine_id)
|
|
@@ -377,6 +377,8 @@ def _windows_install(cfg: Config, exe: str, label: str, args: list[str], *, ever
|
|
|
377
377
|
arguments = windows.runner_arguments(args, log=log, home=cfg.home, path_dirs=[str(Path(claude).parent)] if claude else [])
|
|
378
378
|
if dry_run:
|
|
379
379
|
return None
|
|
380
|
+
if label == UI_LABEL:
|
|
381
|
+
windows.clear_quit(cfg.home) # a Quit from the icon lasts until this: the dashboard runs again now
|
|
380
382
|
xml = windows.task_xml(description=description, command=str(pythonw), arguments=arguments,
|
|
381
383
|
every_minutes=every_minutes, keep_alive=keep_alive)
|
|
382
384
|
return windows.install_task(WINDOWS_TASKS[label], xml)
|
|
@@ -256,6 +256,13 @@ def setting_info(cfg: Config) -> dict:
|
|
|
256
256
|
from .install import UI_LABEL, launchd_status
|
|
257
257
|
from .update import install_method
|
|
258
258
|
|
|
259
|
+
if sys.platform == "win32":
|
|
260
|
+
from .windows import task_status
|
|
261
|
+
return {"supported": True, "on": cfg.server_menu_bar, "shown": SHOWN, "app_extra": True,
|
|
262
|
+
"login_item": bool(os.environ.get("INTERLATCH_WINDOWS_TASK")),
|
|
263
|
+
"agent": bool(task_status("Dashboard").get("installed")), "can_install": False,
|
|
264
|
+
"command": "", "listed_as": "Interlatch"}
|
|
265
|
+
|
|
259
266
|
if sys.platform != "darwin" or install_method()["kind"] in ("app", "container"): # the app always shows it
|
|
260
267
|
return {"supported": False}
|
|
261
268
|
cmd = extra_command()
|
|
@@ -307,6 +314,8 @@ def wants_menu_bar(cfg: Config) -> bool:
|
|
|
307
314
|
(or a checkout's dev server) adds no second icon."""
|
|
308
315
|
from .install import UI_LABEL
|
|
309
316
|
|
|
317
|
+
if sys.platform == "win32":
|
|
318
|
+
return cfg.server_menu_bar and bool(os.environ.get("INTERLATCH_WINDOWS_TASK"))
|
|
310
319
|
return (sys.platform == "darwin" and cfg.server_menu_bar
|
|
311
320
|
and os.environ.get("XPC_SERVICE_NAME") == UI_LABEL)
|
|
312
321
|
|
|
@@ -737,8 +746,15 @@ class StatusMenu:
|
|
|
737
746
|
|
|
738
747
|
# ---------------------------------------------------------------------------------------------- `interlatch ui`
|
|
739
748
|
def run_with_server(cfg: Config, httpd, url: str) -> bool:
|
|
740
|
-
"""`interlatch ui` with the menu-bar item: the dashboard serves on a thread, AppKit runs on this
|
|
741
|
-
until Quit or Ctrl+C. False, with nothing started, when
|
|
749
|
+
"""`interlatch ui` with the menu-bar item: the dashboard serves on a thread, AppKit (or Windows tray) runs on this
|
|
750
|
+
(main) thread until Quit or Ctrl+C. False, with nothing started, when not supported."""
|
|
751
|
+
global SHOWN
|
|
752
|
+
if sys.platform == "win32":
|
|
753
|
+
from .windows import run_tray_with_server
|
|
754
|
+
|
|
755
|
+
SHOWN = True # while the icon shows: run_tray_with_server returns when it is gone
|
|
756
|
+
SHOWN = run_tray_with_server(cfg, httpd, url)
|
|
757
|
+
return SHOWN
|
|
742
758
|
try:
|
|
743
759
|
import AppKit
|
|
744
760
|
from PyObjCTools import AppHelper
|
|
@@ -747,7 +763,6 @@ def run_with_server(cfg: Config, httpd, url: str) -> bool:
|
|
|
747
763
|
return False
|
|
748
764
|
from .install import UI_LABEL
|
|
749
765
|
|
|
750
|
-
global SHOWN
|
|
751
766
|
SHOWN = True
|
|
752
767
|
app = httpd.app
|
|
753
768
|
AppKit.NSApplication.sharedApplication().setActivationPolicy_(AppKit.NSApplicationActivationPolicyAccessory)
|
|
@@ -43,6 +43,7 @@ try:
|
|
|
43
43
|
except ImportError: # Windows, where Chronicle never ran: there is no folder of its to move
|
|
44
44
|
fcntl = None
|
|
45
45
|
from .install import LEGACY_BUNDLE_ID, LEGACY_MCP_NAME, MCP_NAME
|
|
46
|
+
from .util import abs_path_sql
|
|
46
47
|
|
|
47
48
|
log = logging.getLogger("interlatch.migrate")
|
|
48
49
|
|
|
@@ -314,7 +315,7 @@ def _projects(cfg: Config) -> list[Path]:
|
|
|
314
315
|
conn = sqlite3.connect(f"file:{cfg.db_path}?mode=ro", uri=True, timeout=30)
|
|
315
316
|
try:
|
|
316
317
|
rows = conn.execute("SELECT DISTINCT project_path FROM sessions WHERE agent = 'claude' "
|
|
317
|
-
"AND project_path
|
|
318
|
+
f"AND {abs_path_sql('project_path')}").fetchall() # /x, and on Windows C:\x
|
|
318
319
|
except sqlite3.Error:
|
|
319
320
|
return []
|
|
320
321
|
finally:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The background sync calls release_check() on every run. It asks pypi.org at most once a day (update.check_due) and
|
|
4
4
|
notifies once per release, so a notification that was ignored or dismissed does not come back. macOS posts through
|
|
5
|
-
osascript, Linux through notify-send;
|
|
5
|
+
osascript, Linux through notify-send, Windows through PowerShell (a toast); none needs anything installed.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -11,6 +11,7 @@ import logging
|
|
|
11
11
|
import platform
|
|
12
12
|
import shutil
|
|
13
13
|
import subprocess
|
|
14
|
+
import sys
|
|
14
15
|
|
|
15
16
|
from .config import Config
|
|
16
17
|
|
|
@@ -25,10 +26,65 @@ def _applescript(text: str) -> str:
|
|
|
25
26
|
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
# Windows shows a toast only for an app it knows by its AppUserModelID: an unknown one ("Interlatch") shows nothing and
|
|
30
|
+
# raises nothing. This is the ID of the Windows PowerShell shortcut that every Windows has (BurntToast uses it too).
|
|
31
|
+
WINDOWS_AUMID = r"{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe"
|
|
32
|
+
# The title and message come on stdin as JSON (ASCII: json.dumps escapes the rest), never inside the script itself.
|
|
33
|
+
# A toast, else (no WinRT) a notification-area balloon; exit 1 when neither could be shown.
|
|
34
|
+
WINDOWS_SCRIPT = r"""$ErrorActionPreference = 'Stop'
|
|
35
|
+
try {
|
|
36
|
+
$data = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
37
|
+
try {
|
|
38
|
+
$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
|
|
39
|
+
$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
|
40
|
+
$text = $xml.GetElementsByTagName('text')
|
|
41
|
+
$null = $text.Item(0).AppendChild($xml.CreateTextNode([string]$data.title))
|
|
42
|
+
$null = $text.Item(1).AppendChild($xml.CreateTextNode([string]$data.message))
|
|
43
|
+
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
|
44
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('%AUMID%').Show($toast)
|
|
45
|
+
} catch {
|
|
46
|
+
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
|
|
47
|
+
$icon = New-Object System.Windows.Forms.NotifyIcon
|
|
48
|
+
$icon.Icon = [System.Drawing.SystemIcons]::Information
|
|
49
|
+
$icon.Visible = $true
|
|
50
|
+
$icon.ShowBalloonTip(10000, [string]$data.title, [string]$data.message, [System.Windows.Forms.ToolTipIcon]::Info)
|
|
51
|
+
Start-Sleep -Seconds 8 # the balloon goes with its icon
|
|
52
|
+
$icon.Dispose()
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
exit 1
|
|
56
|
+
}
|
|
57
|
+
""".replace("%AUMID%", WINDOWS_AUMID)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def windows_command(ps: str) -> list[str]:
|
|
61
|
+
"""powershell.exe running WINDOWS_SCRIPT: -EncodedCommand (UTF-16LE, base64), so no quoting of the script is needed."""
|
|
62
|
+
import base64
|
|
63
|
+
|
|
64
|
+
return [ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand",
|
|
65
|
+
base64.b64encode(WINDOWS_SCRIPT.encode("utf-16-le")).decode("ascii")]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _post_windows(title: str, message: str) -> bool:
|
|
69
|
+
import json
|
|
70
|
+
|
|
71
|
+
ps = shutil.which("powershell.exe") or shutil.which("powershell")
|
|
72
|
+
if not ps:
|
|
73
|
+
return False
|
|
74
|
+
try: # no console window flashes up when the background sync (pythonw) posts it
|
|
75
|
+
proc = subprocess.run(windows_command(ps), input=json.dumps({"title": title, "message": message}), text=True,
|
|
76
|
+
capture_output=True, timeout=30, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
|
77
|
+
except (OSError, subprocess.SubprocessError):
|
|
78
|
+
return False
|
|
79
|
+
return proc.returncode == 0
|
|
80
|
+
|
|
81
|
+
|
|
28
82
|
def post(title: str, message: str) -> bool:
|
|
29
83
|
"""Show a desktop notification; False when this system has no way to (or it failed)."""
|
|
30
84
|
if platform.system() == "Darwin":
|
|
31
85
|
cmd = ["osascript", "-e", f"display notification {_applescript(message)} with title {_applescript(title)}"]
|
|
86
|
+
elif sys.platform == "win32" or platform.system() == "Windows":
|
|
87
|
+
return _post_windows(title, message)
|
|
32
88
|
elif shutil.which("notify-send"):
|
|
33
89
|
cmd = ["notify-send", "--app-name=Interlatch", title, message]
|
|
34
90
|
else:
|
|
@@ -39,7 +39,7 @@ import threading
|
|
|
39
39
|
import time
|
|
40
40
|
from datetime import datetime, timedelta, timezone
|
|
41
41
|
|
|
42
|
-
from .util import utcnow_iso
|
|
42
|
+
from .util import is_abs_path, normalize_project_path, utcnow_iso
|
|
43
43
|
|
|
44
44
|
ROLES = ("admin", "member", "readonly")
|
|
45
45
|
ROLE_RANK = {"readonly": 0, "member": 1, "admin": 2}
|
|
@@ -149,8 +149,8 @@ def clean_projects(projects) -> list[str] | None:
|
|
|
149
149
|
out = set()
|
|
150
150
|
for x in projects:
|
|
151
151
|
x = str(x or "").strip()
|
|
152
|
-
x = x
|
|
153
|
-
if not x
|
|
152
|
+
x = normalize_project_path(x)
|
|
153
|
+
if not is_abs_path(x) or len(x) > 1024 or "\0" in x:
|
|
154
154
|
raise PeopleError("{project} is not a project on this hub", project=x or "''")
|
|
155
155
|
out.add(x)
|
|
156
156
|
return sorted(out)
|