attache 0.0.1__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.
Files changed (86) hide show
  1. attache-0.0.1/.gitattributes +3 -0
  2. attache-0.0.1/.github/workflows/ci.yml +67 -0
  3. attache-0.0.1/.github/workflows/docs.yml +72 -0
  4. attache-0.0.1/.github/workflows/release.yml +119 -0
  5. attache-0.0.1/.gitignore +13 -0
  6. attache-0.0.1/CLAUDE.md +75 -0
  7. attache-0.0.1/CONTRIBUTING.md +42 -0
  8. attache-0.0.1/LICENSE +21 -0
  9. attache-0.0.1/PKG-INFO +109 -0
  10. attache-0.0.1/README.md +79 -0
  11. attache-0.0.1/RELEASING.md +68 -0
  12. attache-0.0.1/docs/assets/openafterhours_icon_512.png +0 -0
  13. attache-0.0.1/docs/assets/stylesheets/oah-theme.css +167 -0
  14. attache-0.0.1/docs/configuration.md +354 -0
  15. attache-0.0.1/docs/design.md +37 -0
  16. attache-0.0.1/docs/developers/contributing.md +140 -0
  17. attache-0.0.1/docs/developers/releasing.md +88 -0
  18. attache-0.0.1/docs/index.md +93 -0
  19. attache-0.0.1/docs/pages.md +219 -0
  20. attache-0.0.1/docs/phase-1-ui.png +0 -0
  21. attache-0.0.1/docs/proposal.html +1263 -0
  22. attache-0.0.1/docs/quickstart.md +194 -0
  23. attache-0.0.1/docs/security.md +148 -0
  24. attache-0.0.1/docs/skills.md +203 -0
  25. attache-0.0.1/pyproject.toml +72 -0
  26. attache-0.0.1/src/attache/__init__.py +3 -0
  27. attache-0.0.1/src/attache/__main__.py +3 -0
  28. attache-0.0.1/src/attache/agent.py +85 -0
  29. attache-0.0.1/src/attache/app.py +82 -0
  30. attache-0.0.1/src/attache/backends/__init__.py +40 -0
  31. attache-0.0.1/src/attache/backends/anthropic.py +62 -0
  32. attache-0.0.1/src/attache/backends/claude_cli.py +160 -0
  33. attache-0.0.1/src/attache/backends/openai.py +101 -0
  34. attache-0.0.1/src/attache/bundled/attache.toml +56 -0
  35. attache-0.0.1/src/attache/bundled/memory/ATTACHE.md +11 -0
  36. attache-0.0.1/src/attache/bundled/memory/USER.md +18 -0
  37. attache-0.0.1/src/attache/bundled/skills/artifact-kit/SKILL.md +118 -0
  38. attache-0.0.1/src/attache/bundled/skills/artifact-kit/templates/board.html +315 -0
  39. attache-0.0.1/src/attache/bundled/skills/artifact-kit/templates/home.html +371 -0
  40. attache-0.0.1/src/attache/bundled/skills/artifact-kit/tokens.css +176 -0
  41. attache-0.0.1/src/attache/bundled/skills/reminders/SKILL.md +82 -0
  42. attache-0.0.1/src/attache/bundled/skills/skill-writer/SKILL.md +91 -0
  43. attache-0.0.1/src/attache/bundled/skills/todos/SKILL.md +91 -0
  44. attache-0.0.1/src/attache/channels.py +71 -0
  45. attache-0.0.1/src/attache/cli.py +126 -0
  46. attache-0.0.1/src/attache/config.py +160 -0
  47. attache-0.0.1/src/attache/router.py +248 -0
  48. attache-0.0.1/src/attache/scheduler.py +88 -0
  49. attache-0.0.1/src/attache/skills.py +138 -0
  50. attache-0.0.1/src/attache/static/bridge.js +73 -0
  51. attache-0.0.1/src/attache/static/fonts/OFL-Inter.txt +92 -0
  52. attache-0.0.1/src/attache/static/fonts/OFL-JetBrainsMono.txt +93 -0
  53. attache-0.0.1/src/attache/static/fonts/OFL-SpaceGrotesk.txt +93 -0
  54. attache-0.0.1/src/attache/static/fonts/inter-latin-400-normal.woff2 +0 -0
  55. attache-0.0.1/src/attache/static/fonts/inter-latin-500-normal.woff2 +0 -0
  56. attache-0.0.1/src/attache/static/fonts/inter-latin-600-normal.woff2 +0 -0
  57. attache-0.0.1/src/attache/static/fonts/inter-latin-700-normal.woff2 +0 -0
  58. attache-0.0.1/src/attache/static/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  59. attache-0.0.1/src/attache/static/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
  60. attache-0.0.1/src/attache/static/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
  61. attache-0.0.1/src/attache/static/fonts.css +33 -0
  62. attache-0.0.1/src/attache/static/oah-tokens.css +45 -0
  63. attache-0.0.1/src/attache/static/ui.html +363 -0
  64. attache-0.0.1/src/attache/store.py +234 -0
  65. attache-0.0.1/src/attache/tools.py +258 -0
  66. attache-0.0.1/src/attache/types.py +133 -0
  67. attache-0.0.1/src/attache/web.py +314 -0
  68. attache-0.0.1/tests/conftest.py +14 -0
  69. attache-0.0.1/tests/fixtures/claude_stream.jsonl +6 -0
  70. attache-0.0.1/tests/test_agent.py +98 -0
  71. attache-0.0.1/tests/test_app.py +111 -0
  72. attache-0.0.1/tests/test_backends.py +325 -0
  73. attache-0.0.1/tests/test_budget.py +53 -0
  74. attache-0.0.1/tests/test_bundled_skills.py +100 -0
  75. attache-0.0.1/tests/test_channels.py +101 -0
  76. attache-0.0.1/tests/test_claude_cli.py +352 -0
  77. attache-0.0.1/tests/test_cli.py +160 -0
  78. attache-0.0.1/tests/test_config.py +226 -0
  79. attache-0.0.1/tests/test_router.py +436 -0
  80. attache-0.0.1/tests/test_scheduler.py +218 -0
  81. attache-0.0.1/tests/test_skills.py +205 -0
  82. attache-0.0.1/tests/test_store.py +232 -0
  83. attache-0.0.1/tests/test_tools.py +349 -0
  84. attache-0.0.1/tests/test_web.py +657 -0
  85. attache-0.0.1/uv.lock +984 -0
  86. attache-0.0.1/zensical.toml +139 -0
@@ -0,0 +1,3 @@
1
+ * text=auto eol=lf
2
+ *.png binary
3
+ *.woff2 binary
@@ -0,0 +1,67 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+ # release.yml calls this, so a tag is gated by exactly the checks main is gated by.
9
+ workflow_call:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ # A newer push makes an older run's answer irrelevant. Tags are exempt: cancelling a release
15
+ # mid-flight is never what anyone wants.
16
+ concurrency:
17
+ group: ci-${{ github.workflow }}-${{ github.ref }}
18
+ cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
19
+
20
+ env:
21
+ UV_LOCKED: "1" # a stale uv.lock fails CI instead of being re-resolved
22
+
23
+ jobs:
24
+ lint:
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 10
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+ - uses: astral-sh/setup-uv@v10.0.1
30
+ # Pinned to the locked version, so a new ruff release can't fail unrelated pull requests.
31
+ - name: Ruff lint
32
+ run: uvx ruff@0.17.1 check .
33
+ - name: Ruff format check
34
+ run: uvx ruff@0.17.1 format --check .
35
+
36
+ test:
37
+ runs-on: ${{ matrix.os }}
38
+ timeout-minutes: 20
39
+ strategy:
40
+ fail-fast: false
41
+ matrix:
42
+ os: [ubuntu-latest, windows-latest, macos-latest]
43
+ python-version: ["3.11", "3.12", "3.13"]
44
+ steps:
45
+ - uses: actions/checkout@v7
46
+ - uses: astral-sh/setup-uv@v10.0.1
47
+ with:
48
+ python-version: ${{ matrix.python-version }}
49
+ - name: Run tests (includes the core size budget)
50
+ run: uv run pytest
51
+
52
+ build:
53
+ runs-on: ubuntu-latest
54
+ timeout-minutes: 10
55
+ steps:
56
+ - uses: actions/checkout@v7
57
+ - uses: astral-sh/setup-uv@v10.0.1
58
+ - name: Build sdist and wheel
59
+ run: uv build
60
+ - name: Smoke-test the built wheel
61
+ run: |
62
+ wheel=$(ls dist/*.whl)
63
+ uvx --isolated --from "$wheel" attache --help
64
+ uvx --isolated --from "$wheel" attache init --home "$RUNNER_TEMP/home" --name CI
65
+ test -f "$RUNNER_TEMP/home/skills/todos/SKILL.md"
66
+ test -f "$RUNNER_TEMP/home/artifacts/home.html"
67
+ uv run --isolated --no-project --with "$wheel" python -c "import attache.app, attache.web"
@@ -0,0 +1,72 @@
1
+ name: Docs
2
+
3
+ # Build the Zensical documentation site and publish it to GitHub Pages.
4
+ # Pull requests build it too, without deploying, so a broken page shows up in review.
5
+ #
6
+ # One-time setup: Settings -> Pages -> Build and deployment -> Source = GitHub Actions.
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ paths:
11
+ - "docs/**"
12
+ - "zensical.toml"
13
+ - "pyproject.toml"
14
+ - "uv.lock"
15
+ - ".github/workflows/docs.yml"
16
+ pull_request:
17
+ paths:
18
+ - "docs/**"
19
+ - "zensical.toml"
20
+ - "pyproject.toml"
21
+ - "uv.lock"
22
+ - ".github/workflows/docs.yml"
23
+ workflow_dispatch:
24
+
25
+ # Least privilege: build only reads the repo; deploy alone gets the Pages and OIDC scopes.
26
+ permissions:
27
+ contents: read
28
+
29
+ # One Pages deploy at a time, and never cancel one halfway. Pull request builds get their own
30
+ # group, so they neither wait for nor cancel a deploy.
31
+ concurrency:
32
+ group: ${{ github.event_name == 'pull_request' && format('docs-{0}', github.ref) || 'pages' }}
33
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
34
+
35
+ env:
36
+ UV_LOCKED: "1" # a stale uv.lock fails the build instead of being re-resolved
37
+
38
+ # Actions are pinned to commit SHAs: the deploy job can write to the Pages site.
39
+ jobs:
40
+ build:
41
+ runs-on: ubuntu-latest
42
+ timeout-minutes: 10
43
+ steps:
44
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
45
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
46
+ - name: Configure Pages
47
+ if: github.event_name != 'pull_request'
48
+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
49
+ # --strict fails the build on a link to a missing page or anchor.
50
+ - name: Build the docs site
51
+ run: uv run --only-group docs zensical build --clean --strict
52
+ - name: Upload Pages artifact
53
+ if: github.event_name != 'pull_request'
54
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
55
+ with:
56
+ path: site
57
+
58
+ deploy:
59
+ if: github.event_name != 'pull_request'
60
+ needs: build
61
+ runs-on: ubuntu-latest
62
+ timeout-minutes: 10
63
+ permissions:
64
+ pages: write
65
+ id-token: write
66
+ environment:
67
+ name: github-pages
68
+ url: ${{ steps.deployment.outputs.page_url }}
69
+ steps:
70
+ - name: Deploy to GitHub Pages
71
+ id: deployment
72
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
@@ -0,0 +1,119 @@
1
+ name: Release
2
+
3
+ # Cutting a release is: bump `__version__` in src/attache/__init__.py, merge, tag `vX.Y.Z` on
4
+ # main, push the tag. Everything below happens on the tag. See RELEASING.md.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ # Actions are pinned to commit SHAs: this workflow can publish to PyPI and write releases.
13
+ jobs:
14
+ verify:
15
+ name: verify tag matches package version
16
+ runs-on: ubuntu-latest
17
+ timeout-minutes: 5
18
+ outputs:
19
+ version: ${{ steps.version.outputs.version }}
20
+ prerelease: ${{ steps.version.outputs.prerelease }}
21
+ steps:
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
23
+ # First and dependency-free: a tag that disagrees with __version__ should cost seconds,
24
+ # not a full test matrix.
25
+ - id: version
26
+ shell: python
27
+ run: |
28
+ import ast, os, re, sys
29
+ tree = ast.parse(open("src/attache/__init__.py", encoding="utf-8").read())
30
+ version = next(
31
+ node.value.value
32
+ for node in tree.body
33
+ if isinstance(node, ast.Assign) and node.targets[0].id == "__version__"
34
+ )
35
+ tag = os.environ["GITHUB_REF_NAME"].removeprefix("v")
36
+ if tag != version:
37
+ sys.exit(f"tag v{tag} does not match __version__ {version}")
38
+ prerelease = bool(re.search(r"(a|b|rc|dev)\d*$", version))
39
+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as out:
40
+ out.write(f"version={version}\nprerelease={str(prerelease).lower()}\n")
41
+
42
+ ci:
43
+ name: ci
44
+ needs: verify
45
+ uses: ./.github/workflows/ci.yml
46
+
47
+ build:
48
+ name: build release artifacts
49
+ needs: [verify, ci]
50
+ runs-on: ubuntu-latest
51
+ timeout-minutes: 10
52
+ steps:
53
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
54
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
55
+ - run: uv build
56
+ - name: Check the metadata PyPI will render
57
+ run: uvx twine check --strict dist/*
58
+ # The wheel filename is what gets uploaded, so assert on it rather than trusting that the
59
+ # build backend read the same version the verify job did.
60
+ - name: Confirm the artifacts carry the tagged version
61
+ env:
62
+ VERSION: ${{ needs.verify.outputs.version }}
63
+ run: |
64
+ set -euo pipefail
65
+ test -f "dist/attache-${VERSION}-py3-none-any.whl" || { ls -1 dist; exit 1; }
66
+ test -f "dist/attache-${VERSION}.tar.gz" || { ls -1 dist; exit 1; }
67
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
68
+ with:
69
+ name: dist
70
+ path: dist/
71
+ retention-days: 30
72
+
73
+ publish:
74
+ name: publish to PyPI
75
+ needs: [verify, build]
76
+ runs-on: ubuntu-latest
77
+ timeout-minutes: 10
78
+ # Trusted Publishing: PyPI accepts uploads from this repository, this workflow file and this
79
+ # environment, with no API token stored anywhere. The environment is also where a manual
80
+ # approval gate goes, should you ever want one before an upload.
81
+ environment:
82
+ name: pypi
83
+ url: https://pypi.org/p/attache
84
+ permissions:
85
+ id-token: write # mints the short-lived OIDC token PyPI exchanges for an upload token
86
+ steps:
87
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
88
+ with:
89
+ name: dist
90
+ path: dist/
91
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
92
+
93
+ github-release:
94
+ name: create the GitHub release
95
+ needs: [verify, publish]
96
+ runs-on: ubuntu-latest
97
+ timeout-minutes: 10
98
+ permissions:
99
+ contents: write
100
+ steps:
101
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
102
+ with:
103
+ fetch-depth: 0 # --generate-notes diffs against the previous tag
104
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
105
+ with:
106
+ name: dist
107
+ path: dist/
108
+ # Last, so a release never claims success for an upload that failed.
109
+ - name: gh release create
110
+ env:
111
+ GH_TOKEN: ${{ github.token }}
112
+ PRERELEASE: ${{ needs.verify.outputs.prerelease }}
113
+ run: |
114
+ set -euo pipefail
115
+ flags=(--generate-notes --verify-tag --title "${GITHUB_REF_NAME}")
116
+ if [ "${PRERELEASE}" = "true" ]; then
117
+ flags+=(--prerelease)
118
+ fi
119
+ gh release create "${GITHUB_REF_NAME}" "${flags[@]}" dist/*
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ .claude/
11
+ .idea/
12
+ .vscode/
13
+ site/
@@ -0,0 +1,75 @@
1
+ # Attaché: notes for agents and contributors
2
+
3
+ Attaché is a personal assistant packaged as a Python tool (`uvx attache`). The core is small on
4
+ purpose. Channels, email, file formats and integrations are **skills**, not core code. The
5
+ design is in `docs/proposal.html`; read §3 (architecture) and §10 (code budget) first.
6
+
7
+ ## Commands
8
+
9
+ ```
10
+ uv sync # install dev environment
11
+ uv run pytest # all tests, including the size budget
12
+ uv run ruff check . && uv run ruff format --check .
13
+ uv run attache --help
14
+ uv run --group docs zensical serve # docs site (zensical.toml, docs/); build with --strict
15
+ ```
16
+
17
+ The web UI, the bundled page kit and the docs site all use the OpenAfterHours design tokens
18
+ (`src/attache/static/oah-tokens.css`, vendored from rwa_calculator; a test guards drift). Fonts
19
+ are self-hosted in `src/attache/static/fonts/` (SIL OFL), so nothing loads from the internet.
20
+
21
+ ## Layout
22
+
23
+ ```
24
+ src/attache/
25
+ types.py shared dataclasses and protocols; every module talks through these
26
+ app.py builds Services and runs everything
27
+ config.py attache.toml -> Config; init_home()
28
+ store.py SQLite: messages, docs, tasks, sessions
29
+ skills.py skill discovery, prompt index, git commit/log/undo of <home>
30
+ scheduler.py fires due tasks
31
+ agent.py shared tool loop for API backends
32
+ backends/ anthropic, openai (chat + responses), claude_cli
33
+ tools.py the tools a turn may use, filtered by role
34
+ router.py inbound message -> access, trigger, role -> turn -> reply
35
+ channels.py channel manager, terminal channel
36
+ web.py Starlette app: chat page, artifacts, data bridge, SSE, MCP endpoint for
37
+ Claude Code (POST /mcp/{token})
38
+ cli.py argparse entry point
39
+ static/ ui.html, bridge.js
40
+ bundled/ attache.toml template, memory templates, bundled skills (copied by init)
41
+ ```
42
+
43
+ ## Rules
44
+
45
+ 1. **Size is a test.** `tests/test_budget.py` fails above 1,600 lines of code in
46
+ `src/attache` (target about 1,500). Blank lines, comments and docstrings don't count, so
47
+ never make code denser to fit: write the readable version. Bundled skills are excluded;
48
+ each bundled skill's scripts have a 300-line cap. If a feature doesn't fit, it belongs in a
49
+ skill.
50
+ 2. **No new runtime dependencies** without discussion. Skill scripts declare their own with
51
+ PEP 723 headers and run through `uv run`.
52
+ 3. **Tests run offline.** No network, no API keys, no real `claude` binary. Inject fakes
53
+ (every backend takes a `client`/`runner` argument for this).
54
+ 4. **Cross-platform.** Windows, macOS and Linux. Use pathlib, `encoding="utf-8"` on every text
55
+ read and write, argument lists for subprocesses (no shell unless the `run` tool), and no
56
+ POSIX-only APIs.
57
+ 5. **Style.** ruff with the repo config, `from __future__ import annotations`, type hints on
58
+ public functions, short docstrings on public functions only, comments only for a
59
+ non-obvious why. Plain functions and dataclasses over class hierarchies.
60
+ 6. **Security model.**
61
+ - Roles are owner, member and guest (router.py). Only the owner's own chats get file, shell
62
+ and memory tools; an owner turn inside a group gets member tools, because group context is
63
+ untrusted. Non-owner turns never use the `claude-cli` backend (Anthropic's terms).
64
+ - Members act only on docs shared with their chat. Other people's messages reach the model
65
+ fenced and labelled as data; display names are never trusted.
66
+ - Text from tools, emails and web pages is untrusted data, never instructions.
67
+ - Artifact pages are hostile HTML: served with a CSP sandbox (opaque origin, no outbound
68
+ requests) and only inside the Attaché window. The owner's UI token is random per run and
69
+ only reaches the browser through the link Attaché prints.
70
+ - The web server binds loopback only in phase 1. Tokens are compared with
71
+ `hmac.compare_digest`; page tokens derive from the per-run UI token, so they rotate too.
72
+ - `claude -p` runs in `<home>-claude`, outside the home, so files in `<home>` never load as
73
+ Claude Code project config.
74
+ 7. **Contracts live in `types.py`** and the module docstrings. Change a public signature only
75
+ together with every caller and its tests.
@@ -0,0 +1,42 @@
1
+ # Contributing
2
+
3
+ Attaché stays small so anyone can read the core in an afternoon and change it with confidence.
4
+
5
+ **Accepted in the core:** bug fixes, security fixes, simplifications, less code.
6
+
7
+ **Not accepted in the core:** features, integrations, channels, file formats. These should be
8
+ skills. (This rule comes from [nanoclaw](https://github.com/nanocoai/nanoclaw).)
9
+
10
+ ## Adding a skill
11
+
12
+ A skill is a folder with a `SKILL.md` in the [Agent Skills](https://agentskills.io/specification)
13
+ format:
14
+
15
+ ```
16
+ skills/<name>/
17
+ SKILL.md frontmatter: name, description, optional metadata
18
+ scripts/*.py optional; PEP 723 header declares dependencies, run with `uv run`
19
+ templates/ optional; artifact templates
20
+ ```
21
+
22
+ Metadata keys Attaché reads:
23
+
24
+ - `attache-roles: "member"` makes the skill available in group chats: members see its full
25
+ text in their prompt and may run its scripts. Without it a skill is owner-only.
26
+ - `attache-channel: "<command>"` marks a channel sidecar (phase 2).
27
+
28
+ Bundled skills live in `src/attache/bundled/skills/` and are copied into a user's home by
29
+ `attache init`. Each bundled skill's scripts are capped at 300 lines of code by
30
+ `tests/test_budget.py`.
31
+
32
+ ## The size budget
33
+
34
+ `tests/test_budget.py` fails when the core passes 1,600 lines of code. Blank lines, comments
35
+ and docstrings don't count, so write the clearest version of the code, not the shortest.
36
+
37
+ ## Before you open a pull request
38
+
39
+ ```
40
+ uv run ruff check . && uv run ruff format --check .
41
+ uv run pytest
42
+ ```
attache-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenAfterHours contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
attache-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: attache
3
+ Version: 0.0.1
4
+ Summary: A personal assistant you install with one command. Small core; channels, email and file formats are skills.
5
+ Project-URL: Homepage, https://github.com/OpenAfterHours/attache
6
+ Project-URL: Repository, https://github.com/OpenAfterHours/attache
7
+ Project-URL: Issues, https://github.com/OpenAfterHours/attache/issues
8
+ Author: OpenAfterHours contributors
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,assistant,claude,local-first,openai,skills
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Office/Business :: Scheduling
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: anthropic>=0.40
24
+ Requires-Dist: cronsim>=2.6
25
+ Requires-Dist: openai>=1.66
26
+ Requires-Dist: pyyaml>=6
27
+ Requires-Dist: starlette>=0.40
28
+ Requires-Dist: uvicorn>=0.30
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Attaché
32
+
33
+ A personal assistant you install with one command. It runs on your own machine, keeps your
34
+ to-dos and reminders, makes interactive pages on demand, and learns how you work through a
35
+ folder of skills it can edit itself. The core is just under 1,600 lines of Python code, the
36
+ cap CI enforces; everything else is a skill.
37
+
38
+ > **Status:** phase 1 of the [roadmap](docs/proposal.html): the local core, terminal and web
39
+ > chat. Slack and Signal arrive in phase 2, email (Outlook, Gmail, Titan) in phase 3.
40
+
41
+ **Documentation:** <https://openafterhours.github.io/attache/>.
42
+
43
+ ## Try it
44
+
45
+ ```
46
+ uvx attache # first run asks your name, creates ~/.attache, then opens a chat
47
+ uv tool install attache # keep it
48
+ attache chat # terminal chat, plus the web page
49
+ attache run # web page, reminders and channels, no terminal chat
50
+ attache doctor # check your setup
51
+ ```
52
+
53
+ ![The web page: chat on the left, the Home page with to-dos and reminders on the right](docs/phase-1-ui.png)
54
+
55
+ The terminal and the web page are one conversation. Attaché prints a link like
56
+ `http://127.0.0.1:8765/#t=...` when it starts; open that link to use the web page. The token in
57
+ it changes every run, so other programs on your machine can't drive your assistant.
58
+
59
+ Attaché talks to models through four adapters, chosen in `~/.attache/attache.toml`:
60
+
61
+ | `api` | Use it for |
62
+ |--------------------|-------------------------------------------------------------------|
63
+ | `claude-cli` | Claude Code (`claude -p`) on your own login. Your own chats only. |
64
+ | `anthropic` | The Claude API, or any Anthropic-compatible `base_url` |
65
+ | `openai-chat` | Ollama, LM Studio, vLLM, llama.cpp, OpenRouter, Groq, Azure |
66
+ | `openai-responses` | OpenAI reasoning models with tools |
67
+
68
+ ```toml
69
+ [routing]
70
+ own_chats = "claude" # your Claude login
71
+ group_chats = "claude-api" # an API key, per Anthropic's terms
72
+
73
+ [endpoints.claude]
74
+ api = "claude-cli"
75
+ model = "claude-sonnet-5-5"
76
+
77
+ [endpoints.claude-api]
78
+ api = "anthropic"
79
+ model = "claude-sonnet-5-5"
80
+ key_env = "ANTHROPIC_API_KEY"
81
+ ```
82
+
83
+ ## How it is put together
84
+
85
+ - **Skills** live in `~/.attache/skills/<name>/SKILL.md` ([Agent Skills](https://agentskills.io)
86
+ format). Only names and descriptions go into the prompt. The assistant edits skills when you
87
+ change how you work, and `~/.attache` is a git repo, so `attache skills log` and
88
+ `attache skills undo <name>` show and reverse every change.
89
+ - **Artifacts** are HTML pages the assistant writes into `~/.attache/artifacts/`. The local web
90
+ server adds a small `attache` bridge so a page can read and write real data with live updates.
91
+ Pages run sandboxed: they can't reach the internet or anything outside the data they declare.
92
+ - **Roles.** You are the owner. Group members (from phase 2) only reach what is shared with
93
+ their chat, and never files, shell or email. Your own turns inside a group get member tools
94
+ too, because the group's messages are untrusted.
95
+ - **Claude Code** runs as a backend with `claude -p` on your own login, for your own chats only.
96
+ Attaché's tools reach it through an MCP endpoint on the local web server. It runs in a
97
+ working folder next to your home (`~/.attache-claude`), so nothing written into `~/.attache`
98
+ can load as Claude Code project settings.
99
+
100
+ The full design, with the research behind it, is in [docs/proposal.html](docs/proposal.html).
101
+
102
+ ## Contributing
103
+
104
+ See [CONTRIBUTING.md](CONTRIBUTING.md): bug fixes and simplifications go in the core, features
105
+ go in skills.
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,79 @@
1
+ # Attaché
2
+
3
+ A personal assistant you install with one command. It runs on your own machine, keeps your
4
+ to-dos and reminders, makes interactive pages on demand, and learns how you work through a
5
+ folder of skills it can edit itself. The core is just under 1,600 lines of Python code, the
6
+ cap CI enforces; everything else is a skill.
7
+
8
+ > **Status:** phase 1 of the [roadmap](docs/proposal.html): the local core, terminal and web
9
+ > chat. Slack and Signal arrive in phase 2, email (Outlook, Gmail, Titan) in phase 3.
10
+
11
+ **Documentation:** <https://openafterhours.github.io/attache/>.
12
+
13
+ ## Try it
14
+
15
+ ```
16
+ uvx attache # first run asks your name, creates ~/.attache, then opens a chat
17
+ uv tool install attache # keep it
18
+ attache chat # terminal chat, plus the web page
19
+ attache run # web page, reminders and channels, no terminal chat
20
+ attache doctor # check your setup
21
+ ```
22
+
23
+ ![The web page: chat on the left, the Home page with to-dos and reminders on the right](docs/phase-1-ui.png)
24
+
25
+ The terminal and the web page are one conversation. Attaché prints a link like
26
+ `http://127.0.0.1:8765/#t=...` when it starts; open that link to use the web page. The token in
27
+ it changes every run, so other programs on your machine can't drive your assistant.
28
+
29
+ Attaché talks to models through four adapters, chosen in `~/.attache/attache.toml`:
30
+
31
+ | `api` | Use it for |
32
+ |--------------------|-------------------------------------------------------------------|
33
+ | `claude-cli` | Claude Code (`claude -p`) on your own login. Your own chats only. |
34
+ | `anthropic` | The Claude API, or any Anthropic-compatible `base_url` |
35
+ | `openai-chat` | Ollama, LM Studio, vLLM, llama.cpp, OpenRouter, Groq, Azure |
36
+ | `openai-responses` | OpenAI reasoning models with tools |
37
+
38
+ ```toml
39
+ [routing]
40
+ own_chats = "claude" # your Claude login
41
+ group_chats = "claude-api" # an API key, per Anthropic's terms
42
+
43
+ [endpoints.claude]
44
+ api = "claude-cli"
45
+ model = "claude-sonnet-5-5"
46
+
47
+ [endpoints.claude-api]
48
+ api = "anthropic"
49
+ model = "claude-sonnet-5-5"
50
+ key_env = "ANTHROPIC_API_KEY"
51
+ ```
52
+
53
+ ## How it is put together
54
+
55
+ - **Skills** live in `~/.attache/skills/<name>/SKILL.md` ([Agent Skills](https://agentskills.io)
56
+ format). Only names and descriptions go into the prompt. The assistant edits skills when you
57
+ change how you work, and `~/.attache` is a git repo, so `attache skills log` and
58
+ `attache skills undo <name>` show and reverse every change.
59
+ - **Artifacts** are HTML pages the assistant writes into `~/.attache/artifacts/`. The local web
60
+ server adds a small `attache` bridge so a page can read and write real data with live updates.
61
+ Pages run sandboxed: they can't reach the internet or anything outside the data they declare.
62
+ - **Roles.** You are the owner. Group members (from phase 2) only reach what is shared with
63
+ their chat, and never files, shell or email. Your own turns inside a group get member tools
64
+ too, because the group's messages are untrusted.
65
+ - **Claude Code** runs as a backend with `claude -p` on your own login, for your own chats only.
66
+ Attaché's tools reach it through an MCP endpoint on the local web server. It runs in a
67
+ working folder next to your home (`~/.attache-claude`), so nothing written into `~/.attache`
68
+ can load as Claude Code project settings.
69
+
70
+ The full design, with the research behind it, is in [docs/proposal.html](docs/proposal.html).
71
+
72
+ ## Contributing
73
+
74
+ See [CONTRIBUTING.md](CONTRIBUTING.md): bug fixes and simplifications go in the core, features
75
+ go in skills.
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,68 @@
1
+ # Releasing attache
2
+
3
+ A release is a pushed tag. The tag check, the full CI matrix, the build, the PyPI upload and the
4
+ GitHub release all happen in `.github/workflows/release.yml`.
5
+
6
+ ## One-time setup: PyPI Trusted Publishing
7
+
8
+ attache uploads to PyPI over OIDC. There is no API token in this repository and there should
9
+ never be one. PyPI needs to be told, once, which workflow it accepts uploads from.
10
+
11
+ `attache` has not been published yet, so this is a **pending publisher**. It becomes an ordinary
12
+ publisher the moment the first release lands, and that release also claims the name.
13
+
14
+ 1. Sign in at <https://pypi.org> and open **Account settings → Publishing**.
15
+ 2. Under *Add a new pending publisher*, choose **GitHub** and fill in exactly:
16
+
17
+ | Field | Value |
18
+ |---|---|
19
+ | PyPI project name | `attache` |
20
+ | Owner | `OpenAfterHours` |
21
+ | Repository name | `attache` |
22
+ | Workflow name | `release.yml` |
23
+ | Environment name | `pypi` |
24
+
25
+ 3. Save. All five fields must match or the upload is rejected. The environment name in
26
+ particular is easy to leave blank, and it is part of the claim.
27
+
28
+ ### The `pypi` environment
29
+
30
+ The `publish` job runs in a GitHub environment named `pypi`; GitHub creates it on the first
31
+ release. It pins the Trusted Publishing claim, and it is where an approval gate goes:
32
+
33
+ - **Settings → Environments → pypi → Required reviewers** makes a person approve each upload.
34
+ Leave it unprotected and a pushed tag publishes on its own, which is the intended default.
35
+ - A tag ruleset (**Settings → Rules → Rulesets**, target `v*`) limits who can create release
36
+ tags. Without one, anyone with write access can publish by pushing a tag.
37
+
38
+ ## Cutting a release
39
+
40
+ Versions follow PEP 440. The single source of truth is `__version__` in
41
+ `src/attache/__init__.py`; `pyproject.toml` reads it.
42
+
43
+ 1. Bump `__version__` in a pull request and merge it once CI is green.
44
+ 2. Tag the merge commit on `main` and push the tag:
45
+
46
+ ```bash
47
+ git checkout main
48
+ git pull
49
+ git tag v0.0.1
50
+ git push origin v0.0.1
51
+ ```
52
+
53
+ 3. Watch **Actions → Release**. In order, it:
54
+ - checks the tag matches `__version__`;
55
+ - runs the whole CI workflow (lint, tests on Ubuntu, Windows and macOS, the wheel smoke test);
56
+ - builds the sdist and wheel and checks their metadata with `twine check --strict`;
57
+ - uploads them to PyPI;
58
+ - creates the GitHub release with generated notes and both files attached.
59
+
60
+ A tag that doesn't match `__version__` fails in the first job, before anything is built. Versions
61
+ ending in `a`, `b`, `rc` or `dev` (for example `0.1.0rc1`) are marked as pre-releases on GitHub.
62
+
63
+ ## If a release fails
64
+
65
+ - **Before the publish job:** fix the problem on `main`, delete the tag
66
+ (`git push --delete origin v0.0.1` and `git tag -d v0.0.1`), and tag again.
67
+ - **After the upload:** PyPI never accepts the same version twice. Bump to the next version and
68
+ release that.