fridica 0.1.0__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.
@@ -0,0 +1,54 @@
1
+ name: Auto Tag on PR Merge
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [closed]
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: auto-tag-main
13
+ cancel-in-progress: false
14
+ queue: max
15
+
16
+ jobs:
17
+ create-tag:
18
+ if: github.event.pull_request.merged == true
19
+ permissions:
20
+ contents: write
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 10
23
+ steps:
24
+ - uses: actions/checkout@v5
25
+ with:
26
+ ref: ${{ github.event.pull_request.merge_commit_sha }}
27
+ fetch-depth: 0
28
+ token: ${{ github.token }}
29
+ - name: Verify merged commit belongs to main
30
+ run: git merge-base --is-ancestor HEAD origin/main
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: '3.11'
34
+ - name: Determine release tag
35
+ id: version
36
+ env:
37
+ LABELS_JSON: ${{ toJson(github.event.pull_request.labels) }}
38
+ run: python scripts/release.py next >> "$GITHUB_OUTPUT"
39
+ - name: Push tag for the merged commit
40
+ if: steps.version.outputs.create == 'true'
41
+ env:
42
+ RELEASE_TAG: ${{ steps.version.outputs.tag }}
43
+ run: |
44
+ git tag "$RELEASE_TAG"
45
+ git push origin "refs/tags/$RELEASE_TAG"
46
+ - name: Create GitHub release
47
+ env:
48
+ GH_TOKEN: ${{ github.token }}
49
+ GH_REPO: ${{ github.repository }}
50
+ RELEASE_TAG: ${{ steps.version.outputs.tag }}
51
+ run: |
52
+ if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
53
+ gh release create "$RELEASE_TAG" --verify-tag --title "Release $RELEASE_TAG" --generate-notes
54
+ fi
@@ -0,0 +1,71 @@
1
+ name: Continuous Integration
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+ workflow_call:
9
+ inputs:
10
+ ref:
11
+ type: string
12
+ required: true
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ test:
19
+ name: Test (${{ matrix.os }}, Python ${{ matrix.python }})
20
+ runs-on: ${{ matrix.os }}
21
+ timeout-minutes: 15
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ os: [ubuntu-latest, macos-latest]
26
+ python: ['3.11', '3.12', '3.13', '3.14']
27
+ steps:
28
+ - uses: actions/checkout@v5
29
+ with:
30
+ ref: ${{ inputs.ref || github.ref }}
31
+ fetch-depth: 0
32
+ persist-credentials: false
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: ${{ matrix.python }}
36
+ cache: pip
37
+ - name: Install and test
38
+ run: |
39
+ python -m pip install -e '.[dev]'
40
+ python -m pytest -q
41
+
42
+ package:
43
+ needs: test
44
+ runs-on: ubuntu-latest
45
+ timeout-minutes: 15
46
+ steps:
47
+ - uses: actions/checkout@v5
48
+ with:
49
+ ref: ${{ inputs.ref || github.ref }}
50
+ fetch-depth: 0
51
+ persist-credentials: false
52
+ - uses: actions/setup-python@v5
53
+ with:
54
+ python-version: '3.11'
55
+ - name: Build distributions
56
+ run: |
57
+ python -m pip install build twine
58
+ python -m build
59
+ python -m twine check dist/*
60
+ - name: Smoke test installed wheel
61
+ run: |
62
+ python -m pip install dist/*.whl
63
+ cd "$RUNNER_TEMP"
64
+ fridica --help
65
+ python -m fridica --version
66
+ python -c 'from importlib.resources import files; import fridica; from importlib.metadata import version; assert fridica.__version__ == version("fridica"); assert files("fridica").joinpath("manifest.yaml").is_file()'
67
+ - uses: actions/upload-artifact@v4
68
+ with:
69
+ name: distributions
70
+ path: dist/*
71
+ if-no-files-found: error
@@ -0,0 +1,71 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ tag:
7
+ description: Existing release tag, for example v0.1.0
8
+ type: string
9
+ required: true
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ concurrency:
15
+ group: publish-pypi-${{ inputs.tag }}
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ validate-tag:
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 5
22
+ outputs:
23
+ sha: ${{ steps.tag.outputs.sha }}
24
+ steps:
25
+ - uses: actions/checkout@v5
26
+ with:
27
+ fetch-depth: 0
28
+ persist-credentials: false
29
+ - name: Resolve a release tag on main
30
+ id: tag
31
+ env:
32
+ RELEASE_TAG: ${{ inputs.tag }}
33
+ run: |
34
+ [[ "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || exit 1
35
+ RELEASE_SHA=$(git rev-parse --verify "refs/tags/$RELEASE_TAG^{commit}")
36
+ git merge-base --is-ancestor "$RELEASE_SHA" origin/main
37
+ echo "sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
38
+
39
+ build:
40
+ needs: validate-tag
41
+ uses: ./.github/workflows/ci.yml
42
+ with:
43
+ ref: ${{ needs.validate-tag.outputs.sha }}
44
+
45
+ publish:
46
+ needs: [validate-tag, build]
47
+ runs-on: ubuntu-latest
48
+ timeout-minutes: 10
49
+ environment:
50
+ name: pypi
51
+ url: https://pypi.org/project/fridica/
52
+ steps:
53
+ - uses: actions/checkout@v5
54
+ with:
55
+ ref: ${{ needs.validate-tag.outputs.sha }}
56
+ persist-credentials: false
57
+ - uses: actions/setup-python@v5
58
+ with:
59
+ python-version: '3.11'
60
+ - uses: actions/download-artifact@v4
61
+ with:
62
+ name: distributions
63
+ path: dist
64
+ - name: Verify release artifacts
65
+ env:
66
+ RELEASE_TAG: ${{ inputs.tag }}
67
+ run: python scripts/release.py verify --tag "$RELEASE_TAG"
68
+ - name: Publish wheel and source distribution
69
+ uses: pypa/gh-action-pypi-publish@release/v1
70
+ with:
71
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ dist/
6
+ *.egg-info/
7
+ *.sqlite3*
8
+ .env
9
+
fridica-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.5
2
+ Name: fridica
3
+ Version: 0.1.0
4
+ Summary: A local personal agent connecting Slack to Claude Code and Codex
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: aiohttp<4,>=3.10
7
+ Requires-Dist: slack-sdk<4,>=3.33
8
+ Provides-Extra: dev
9
+ Requires-Dist: build>=1.2; extra == 'dev'
10
+ Requires-Dist: hatch-vcs>=0.4; extra == 'dev'
11
+ Requires-Dist: hatchling>=1.25; extra == 'dev'
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Requires-Dist: twine>=5; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Fridica
17
+
18
+ Fridica is a local personal agent that connects your Slack identity to Claude Code
19
+ or Codex. It listens in channels you choose, decides when to participate, works in
20
+ configured project directories, and replies in Slack threads as you. Replies carry
21
+ a `[via fridica]` label and machine-readable metadata.
22
+
23
+ This first release runs one owner per daemon and one Slack app per owner. Anyone
24
+ in an allowed channel can trigger workspace actions. It includes both CLI
25
+ backends, SQLite context and task storage, clarification conversations, and loop
26
+ limits. It does not include a shared relay, browser OAuth onboarding, MCP server,
27
+ or automation of the Claude/Codex desktop UI.
28
+
29
+ ## Install
30
+
31
+ Use macOS or Linux with Python 3.11 or newer. Install and authenticate either
32
+ [Claude Code](https://code.claude.com/docs/en/setup) or
33
+ [Codex CLI](https://developers.openai.com/codex/cli) separately.
34
+
35
+ ```bash
36
+ python -m venv .venv
37
+ source .venv/bin/activate
38
+ python -m pip install -e '.[dev]'
39
+ fridica init
40
+ ```
41
+
42
+ `init` creates `~/.config/fridica/config.toml`, without overwriting existing
43
+ configuration. For a different location, use `fridica init --config /path/config.toml`.
44
+
45
+ ## Configure Slack
46
+
47
+ 1. Open [Slack app management](https://api.slack.com/apps), choose **Create New
48
+ App → From an app manifest**, select your workspace, and paste
49
+ [`slack/manifest.yaml`](slack/manifest.yaml).
50
+ 2. In **Basic Information → App-Level Tokens**, generate an app token with
51
+ `connections:write`. Socket Mode must be enabled.
52
+ 3. Install the app to the workspace under **OAuth & Permissions**. Copy the
53
+ **User OAuth Token** beginning with `xoxp-`, not a bot token. Workspace
54
+ administrators may need to approve installation and requested scopes.
55
+ 4. Export both tokens in the terminal where Fridica will run:
56
+
57
+ ```bash
58
+ export FRIDICA_SLACK_APP_TOKEN='xapp-your-token'
59
+ export FRIDICA_SLACK_USER_TOKEN='xoxp-your-token'
60
+ ```
61
+
62
+ 5. Edit the generated TOML file. Supply your Slack member ID, workspace ID,
63
+ channel IDs, an existing project directory, and an owner profile describing
64
+ your projects and expertise. Choose `backend = "claude"` or `"codex"`.
65
+ Authenticate the selected CLI before starting Fridica.
66
+
67
+ Example configuration (replace the IDs and directory):
68
+
69
+ ```toml
70
+ owner_id = "U123ABC"
71
+ workspace_id = "T123ABC"
72
+ channels = ["C123ABC"]
73
+ workspace = "~/projects/my-project"
74
+ additional_workspaces = []
75
+ backend = "codex"
76
+ profile = "I maintain the simulation package and help diagnose test failures."
77
+ general_messages = true
78
+ context_limit = 50
79
+ timeout = 600
80
+ cooldown = 60
81
+ max_turns = 6
82
+ ```
83
+
84
+ The manifest enables public-channel events. For private channels, explicitly add
85
+ the `groups:history` and `groups:read` user scopes and the `message.groups` user
86
+ event, reinstall the app, and add the channel ID to your configuration. DMs and
87
+ group DMs are outside this release's channel policy. Fridica never assumes that
88
+ an app can see everything your Slack account can see: scopes, subscriptions,
89
+ membership, and workspace policy determine delivery.
90
+
91
+ Each owner must create a separate Slack app for this release. Multiple Socket
92
+ Mode connections to a shared app divide events between connections; they do not
93
+ broadcast every event to every owner. See [Slack's Socket Mode documentation](https://docs.slack.dev/apis/events-api/using-socket-mode/).
94
+
95
+ ## Run
96
+
97
+ ```bash
98
+ fridica doctor
99
+ fridica start --observe-only
100
+ fridica start
101
+ ```
102
+
103
+ `doctor` checks local configuration, token presence, executable availability,
104
+ and required CLI flags without invoking a model. `start` verifies the Slack user
105
+ and workspace identity and channel membership. `--observe-only` records messages
106
+ without invoking either model or posting replies. Stop with Ctrl-C or SIGTERM.
107
+ All subcommands accept `--config PATH`; `python -m fridica` is also supported.
108
+
109
+ Fridica responds to mentions of the owner and follow-ups while a task is waiting
110
+ for clarification. Other messages pass through a separate classification call
111
+ with tools disabled. Classification failure means silence. Set
112
+ `general_messages = false` to disable unsolicited participation. The owner’s own
113
+ messages supply context but never directly trigger their agent.
114
+
115
+ Replies stay in their original thread. Each thread has a persistent six-turn
116
+ default budget; use a new thread for a new task after that budget is exhausted.
117
+ Generated messages initiate responses only when explicitly addressed or following
118
+ an active task. A per-channel cooldown limits unsolicited replies. Other agents'
119
+ metadata is a loop-control hint, not an authorization credential.
120
+
121
+ ## Workspace authority
122
+
123
+ The selected agent can read, edit, and run commands using its provider-supported
124
+ sandbox in the configured workspace roots. Task-command network access is
125
+ disabled. Provider API access is still needed to run the model. Claude requires
126
+ its sandbox dependencies (including bubblewrap and socat on Linux). Fridica does
127
+ not enable bypass-permission flags or automatically approve broader access.
128
+ Claude performs file changes through sandboxed Bash; its built-in Edit and Write
129
+ tools are not exposed, because their permissions are separate from the Bash sandbox.
130
+ Blocked actions require local intervention; there is no remote approval UI.
131
+
132
+ Only grant access to project directories you intend Slack participants to use.
133
+ The provider sandboxes may permit reads beyond writable project directories and
134
+ use temporary files; Fridica does not claim complete filesystem read isolation.
135
+ Managed provider settings and project instructions remain part of the execution
136
+ environment. Slack tokens are removed from agent subprocess environments, but
137
+ do not store credentials in project files accessible to the agent.
138
+
139
+ Fridica supplies its own bounded conversation history for each invocation;
140
+ existing desktop conversations are not imported. The optional `model` setting
141
+ is passed to the selected provider. No model name or paid API key is required by
142
+ Fridica itself; each CLI uses its own authentication and billing.
143
+
144
+ ## Local state and recovery
145
+
146
+ State defaults to `~/.local/state/fridica/state.sqlite3`; override `state_path`
147
+ with an absolute path outside agent workspaces. It contains message text,
148
+ task results, and delivery state, so treat it as private local data. A file lock
149
+ prevents two processes from opening the same state database. Context sent to the
150
+ model is bounded; stored history is retained until you remove the database while
151
+ the daemon is stopped. There is no historical Slack backfill on startup.
152
+
153
+ Incoming events are persisted before acknowledgment. Agent runs are serialized,
154
+ and their results are saved before Slack delivery. Rate-limited replies retry
155
+ without rerunning the agent. Interrupted executions and uncertain deliveries
156
+ are not retried automatically because file changes or Slack posts may already
157
+ have occurred. Startup logs their event IDs. Inspect them locally:
158
+
159
+ ```bash
160
+ sqlite3 ~/.local/state/fridica/state.sqlite3 \
161
+ "SELECT event_id,state FROM events WHERE state IN ('interrupted','ambiguous','failed','blocked');"
162
+ ```
163
+
164
+ Check the workspace and Slack thread before requesting work again in a new
165
+ thread. Restarting cannot guarantee exactly-once execution across an external
166
+ agent, filesystem, and Slack. Raw subprocess output and Slack tokens are not
167
+ logged. Stop the daemon and use a separate state database when changing owner.
168
+
169
+ ## Python interfaces
170
+
171
+ `fridica.models` defines `Message`, `ConversationContext`, `Decision`,
172
+ `AgentResult`, `AgentBackend`, and `Transport`. `fridica.replica.Replica` combines
173
+ configuration, storage, a backend, and a transport. Alternative backends implement
174
+ `async classify(message, context)` and `async respond(message, context)`;
175
+ transports implement `async send(message, result, task_id, turn)` and return the
176
+ confirmed message timestamp. Backend responses contain `text` and a status of
177
+ `complete`, `waiting`, or `blocked`.
178
+
179
+ ## Validate
180
+
181
+ ```bash
182
+ python -m pytest
183
+ python -m build --no-isolation
184
+ fridica --help
185
+ python -m fridica --version
186
+ ```
187
+
188
+ Tests use fake Slack clients and fake agent processes and require no tokens or
189
+ live model calls. For a live smoke test, select one test channel and an empty
190
+ project directory, run `doctor`, then run `start --observe-only`. Have another
191
+ member post a message and verify the observation log. Restart normally and ask
192
+ that member to mention you with a request to create a small text file. Check the
193
+ file and labelled threaded reply. Request a file without specifying its location
194
+ to exercise clarification, then try a request outside configured write roots to
195
+ verify blocked behavior. Repeat with the other backend. Live tests can consume
196
+ provider credits and require your Slack installation and CLI login.
197
+
198
+ ## CI, releases, and deployment
199
+
200
+ The workflows follow snapy's CI → automatic tag → manual PyPI publishing flow,
201
+ adapted for a pure-Python package. Fridica produces one universal wheel and one
202
+ source distribution, rather than platform-specific compiled wheels.
203
+
204
+ - **Continuous Integration** (`.github/workflows/ci.yml`) runs on pull requests
205
+ and pushes to `main`. It tests Python 3.11–3.14 on Ubuntu and macOS, builds both
206
+ distributions after every matrix job passes, checks package metadata, and
207
+ smoke-tests the installed wheel and bundled Slack manifest. Tests use fake
208
+ agents and Slack clients; no Slack/model credentials are required.
209
+ - **Auto Tag on PR Merge** (`cd.yml`) tags the exact merge commit and creates a
210
+ GitHub release. The first tag is `v0.1.0`; subsequent merges default to a patch
211
+ bump. Add one of `release:major`, `release:minor`, or `release:patch` to select
212
+ the increment. Multiple release labels fail the job. Rerunning an already
213
+ tagged merge reuses its tag and repairs a missing GitHub release.
214
+ Authentication uses the automatically supplied `GITHUB_TOKEN`, with
215
+ `contents: write` permission limited to the tagging job. No GitHub App,
216
+ private key, or personal access token is needed.
217
+ Merged fork PRs are supported through a merged-only `pull_request_target`
218
+ event; the checkout is verified to belong to `main` before release code runs.
219
+ Tag jobs use [GitHub's concurrency queue](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency)
220
+ to run serially (up to 100 pending runs).
221
+ - **Publish to PyPI** (`release.yml`) is manually dispatched with an existing
222
+ stable tag, such as `v0.1.0`. It verifies the tag belongs to `main`, reruns the
223
+ full CI workflow on its resolved commit, checks that both artifact versions
224
+ match the requested tag, then publishes those exact artifacts. Publishing
225
+ does not run on every merge or tag push.
226
+
227
+ Versions come from Git tags using
228
+ [hatch-vcs](https://github.com/ofek/hatch-vcs). `fridica.__version__` and the CLI
229
+ read installed package metadata. Untagged/dirty checkouts produce development
230
+ versions; reinstall an editable checkout after changing tags to refresh its
231
+ installed version. Full Git history is fetched in CI. Source distributions carry
232
+ version metadata so they also build without Git.
233
+
234
+ Repository maintainers must configure these GitHub settings before using CD:
235
+
236
+ 1. Ensure repository/organization Actions policies allow the tagging job's
237
+ `GITHUB_TOKEN` to have **Contents: write** permission. The workflow requests
238
+ this explicitly; do not create a token secret. If tag rules restrict `v*`
239
+ creation, configure them to allow this workflow's tag creation. The built-in
240
+ token does not bypass repository rules. Existing `BUMP_BOT_APP_ID` and
241
+ `BUMP_BOT_PRIVATE_KEY` settings are unused and can be removed from Fridica.
242
+ 2. Create a GitHub Actions environment named `pypi`. Add `PYPI_API_TOKEN` as an
243
+ environment secret using a PyPI account authorized to publish `fridica`.
244
+ Configure required reviewers if publication needs an approval gate. A new
245
+ PyPI project may need an account-scoped token for its first upload; replace
246
+ it with a project-scoped token afterward.
247
+ 3. Protect `main` and require CI before merging. Auto-tagging reacts to a merge,
248
+ so branch protection supplies its CI gate. Publishing independently reruns
249
+ all tests. Require the matrix test jobs and the `package` job as checks.
250
+ 4. After a release tag exists, open **Actions → Publish to PyPI → Run workflow**
251
+ on `main`, enter the tag, and approve the `pypi` environment if configured.
252
+ PyPI versions are immutable; use a new tag for changed artifacts rather than
253
+ overwriting a published version.
254
+
255
+ Tags and releases created using `GITHUB_TOKEN` do not trigger downstream
256
+ tag-push or release-event workflows. Fridica's publishing workflow is manually
257
+ dispatched and reruns CI itself, so it does not depend on those events. See
258
+ [GitHub's workflow-trigger rules](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow).
259
+
260
+ Only the package is deployed by CI. Run the daemon on each owner's machine,
261
+ where their Slack tokens, agent authentication, and project directories live:
262
+
263
+ ```bash
264
+ python -m pip install --upgrade 'fridica==0.1.0'
265
+ fridica doctor
266
+ fridica start
267
+ ```
268
+
269
+ Replace `0.1.0` with the published version. Stop the running daemon before an
270
+ upgrade, then restart it in the same environment. No remote daemon, Slack app,
271
+ GitHub secrets, or PyPI project is provisioned by installing these workflows.
272
+
273
+ For local workflow linting with actionlint 1.7.12, use
274
+ `actionlint -ignore 'unexpected key "queue" for "concurrency" section' .github/workflows/*.yml`.
275
+ That version's schema predates GitHub's documented `queue` field; the exception
276
+ only suppresses that schema mismatch.