gfunk 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.
Files changed (99) hide show
  1. gfunk-0.1.0/.claude/CLAUDE.md +53 -0
  2. gfunk-0.1.0/.claude/settings.json +10 -0
  3. gfunk-0.1.0/.github/dependabot.yml +53 -0
  4. gfunk-0.1.0/.github/workflows/ci.yml +159 -0
  5. gfunk-0.1.0/.github/workflows/publish.yml +47 -0
  6. gfunk-0.1.0/.gitignore +15 -0
  7. gfunk-0.1.0/.pre-commit-config.yaml +73 -0
  8. gfunk-0.1.0/.ruff.toml +80 -0
  9. gfunk-0.1.0/CHANGELOG.md +75 -0
  10. gfunk-0.1.0/Justfile +21 -0
  11. gfunk-0.1.0/LICENSE +21 -0
  12. gfunk-0.1.0/PKG-INFO +13 -0
  13. gfunk-0.1.0/README.md +340 -0
  14. gfunk-0.1.0/docs/screenshots/dubs_groups.svg +238 -0
  15. gfunk-0.1.0/docs/screenshots/grind_expanded.svg +243 -0
  16. gfunk-0.1.0/docs/screenshots/grind_week.svg +241 -0
  17. gfunk-0.1.0/docs/screenshots/holla_filter.svg +239 -0
  18. gfunk-0.1.0/docs/screenshots/holla_labels.svg +236 -0
  19. gfunk-0.1.0/docs/screenshots/holla_messages.svg +238 -0
  20. gfunk-0.1.0/docs/screenshots/holla_preview.svg +238 -0
  21. gfunk-0.1.0/docs/screenshots/holla_sorted.svg +238 -0
  22. gfunk-0.1.0/docs/screenshots/regulate_audit.svg +238 -0
  23. gfunk-0.1.0/docs/screenshots/snoop_browse.svg +241 -0
  24. gfunk-0.1.0/docs/screenshots/snoop_preview.svg +241 -0
  25. gfunk-0.1.0/docs/screenshots/vibe_search.svg +238 -0
  26. gfunk-0.1.0/docs/screenshots/vibe_table.svg +237 -0
  27. gfunk-0.1.0/pyproject.toml +60 -0
  28. gfunk-0.1.0/scripts/capture_screenshots.py +155 -0
  29. gfunk-0.1.0/scripts/release.py +111 -0
  30. gfunk-0.1.0/src/gfunk/__init__.py +3 -0
  31. gfunk-0.1.0/src/gfunk/__main__.py +4 -0
  32. gfunk-0.1.0/src/gfunk/auth.py +119 -0
  33. gfunk-0.1.0/src/gfunk/bootstrap.py +183 -0
  34. gfunk-0.1.0/src/gfunk/browser.py +69 -0
  35. gfunk-0.1.0/src/gfunk/cache.py +123 -0
  36. gfunk-0.1.0/src/gfunk/cli.py +2089 -0
  37. gfunk-0.1.0/src/gfunk/demo_data.py +693 -0
  38. gfunk-0.1.0/src/gfunk/dubs.py +110 -0
  39. gfunk-0.1.0/src/gfunk/dubs_tui.py +316 -0
  40. gfunk-0.1.0/src/gfunk/errors.py +55 -0
  41. gfunk-0.1.0/src/gfunk/gmail.py +286 -0
  42. gfunk-0.1.0/src/gfunk/grind_tui.py +208 -0
  43. gfunk-0.1.0/src/gfunk/holla_tui.py +679 -0
  44. gfunk-0.1.0/src/gfunk/mcp_config.py +98 -0
  45. gfunk-0.1.0/src/gfunk/mothership.py +129 -0
  46. gfunk-0.1.0/src/gfunk/mothership_config.py +64 -0
  47. gfunk-0.1.0/src/gfunk/regulate.py +113 -0
  48. gfunk-0.1.0/src/gfunk/regulate_tui.py +331 -0
  49. gfunk-0.1.0/src/gfunk/snoop_tui.py +559 -0
  50. gfunk-0.1.0/src/gfunk/vibe.py +114 -0
  51. gfunk-0.1.0/src/gfunk/workspace.py +753 -0
  52. gfunk-0.1.0/tests/conftest.py +117 -0
  53. gfunk-0.1.0/tests/test_auth.py +181 -0
  54. gfunk-0.1.0/tests/test_bootstrap.py +157 -0
  55. gfunk-0.1.0/tests/test_browser.py +86 -0
  56. gfunk-0.1.0/tests/test_cache.py +95 -0
  57. gfunk-0.1.0/tests/test_cli.py +316 -0
  58. gfunk-0.1.0/tests/test_cli_bounce.py +123 -0
  59. gfunk-0.1.0/tests/test_cli_bounce_json.py +56 -0
  60. gfunk-0.1.0/tests/test_cli_dj.py +222 -0
  61. gfunk-0.1.0/tests/test_cli_dj_list.py +69 -0
  62. gfunk-0.1.0/tests/test_cli_drop.py +130 -0
  63. gfunk-0.1.0/tests/test_cli_dubs.py +126 -0
  64. gfunk-0.1.0/tests/test_cli_grind.py +89 -0
  65. gfunk-0.1.0/tests/test_cli_holla.py +79 -0
  66. gfunk-0.1.0/tests/test_cli_mothership.py +113 -0
  67. gfunk-0.1.0/tests/test_cli_mount_up.py +460 -0
  68. gfunk-0.1.0/tests/test_cli_peep.py +169 -0
  69. gfunk-0.1.0/tests/test_cli_peep_open.py +117 -0
  70. gfunk-0.1.0/tests/test_cli_regulate.py +103 -0
  71. gfunk-0.1.0/tests/test_cli_sample.py +116 -0
  72. gfunk-0.1.0/tests/test_cli_snoop_delete.py +51 -0
  73. gfunk-0.1.0/tests/test_cli_snoop_move.py +120 -0
  74. gfunk-0.1.0/tests/test_cli_snoop_peek.py +64 -0
  75. gfunk-0.1.0/tests/test_cli_snoop_sheet.py +97 -0
  76. gfunk-0.1.0/tests/test_cli_snoop_walk.py +58 -0
  77. gfunk-0.1.0/tests/test_cli_utils.py +267 -0
  78. gfunk-0.1.0/tests/test_cli_vibe.py +113 -0
  79. gfunk-0.1.0/tests/test_cli_vibe_command.py +76 -0
  80. gfunk-0.1.0/tests/test_demo_data.py +125 -0
  81. gfunk-0.1.0/tests/test_dubs.py +185 -0
  82. gfunk-0.1.0/tests/test_dubs_tui.py +222 -0
  83. gfunk-0.1.0/tests/test_errors.py +55 -0
  84. gfunk-0.1.0/tests/test_gmail.py +364 -0
  85. gfunk-0.1.0/tests/test_grind_render.py +159 -0
  86. gfunk-0.1.0/tests/test_grind_tui.py +247 -0
  87. gfunk-0.1.0/tests/test_holla_tui.py +683 -0
  88. gfunk-0.1.0/tests/test_mcp_config.py +144 -0
  89. gfunk-0.1.0/tests/test_mothership.py +93 -0
  90. gfunk-0.1.0/tests/test_mothership_config.py +71 -0
  91. gfunk-0.1.0/tests/test_regulate.py +222 -0
  92. gfunk-0.1.0/tests/test_regulate_tui.py +263 -0
  93. gfunk-0.1.0/tests/test_snoop_tui.py +427 -0
  94. gfunk-0.1.0/tests/test_vibe_tui.py +110 -0
  95. gfunk-0.1.0/tests/test_workspace.py +425 -0
  96. gfunk-0.1.0/tests/test_workspace_gmail.py +323 -0
  97. gfunk-0.1.0/tests/test_workspace_script_files.py +51 -0
  98. gfunk-0.1.0/tests/test_workspace_scripts.py +109 -0
  99. gfunk-0.1.0/uv.lock +1895 -0
@@ -0,0 +1,53 @@
1
+ # gfunk
2
+
3
+ CLI and MCP server for Google Workspace. Python 3.13+, uv, strict mypy.
4
+
5
+ ## Quick reference
6
+
7
+ ```bash
8
+ uv sync # install
9
+ uv run pytest tests/ -q # run tests (uses -n 6 via xdist)
10
+ uv run pre-commit run --all-files # lint, format, type-check, test
11
+ uv run gfunk --version # smoke test
12
+ ```
13
+
14
+ ## Stack
15
+
16
+ - **Runtime:** Python 3.13+, uv
17
+ - **CLI framework:** argparse (src/gfunk/cli.py)
18
+ - **TUI:** Textual (src/gfunk/vibe.py)
19
+ - **MCP server:** mcp library, stdio transport (src/gfunk/mothership.py)
20
+ - **Auth:** Google OAuth2 Desktop flow, token cached at ~/.config/gfunk/
21
+ - **Cache:** SQLite at ~/.local/share/gfunk/cache.db
22
+ - **Linting:** ruff (strict rule set in .ruff.toml), mypy strict
23
+ - **Tests:** pytest + pytest-xdist (`-n 6`)
24
+
25
+ ## Layout
26
+
27
+ - `src/gfunk/` — all source
28
+ - `tests/` — one test file per module, named `test_<module>.py` or `test_cli_<command>.py`
29
+ - `pyproject.toml` — deps, mypy config, commitizen config
30
+ - `.ruff.toml` — linter/formatter rules
31
+ - `.pre-commit-config.yaml` — full hook suite
32
+
33
+ ## Conventions
34
+
35
+ - Every subcommand has exactly two names: one flavorful, one literal (e.g. `vibe` / `sheet`, `mothership` / `mcp`). No more, no less.
36
+ - Commit messages follow Conventional Commits (commitizen enforced).
37
+ - No comments unless the "why" is non-obvious.
38
+ - Type hints on all function signatures (mypy strict).
39
+ - Tests use synthetic fixtures only — never real customer data, names, or IDs.
40
+ - `block-private-terms` pre-commit hook catches PII in fixtures.
41
+
42
+ ## Auth and scopes
43
+
44
+ Scopes are declared in `src/gfunk/auth.py`. A test asserts the exact scope list — any
45
+ change to scopes must update the test. Start read-only, widen deliberately.
46
+
47
+ ## Adding a subcommand
48
+
49
+ 1. Add the parser in `src/gfunk/cli.py` (with aliases).
50
+ 2. Add the handler function in cli.py or a new module.
51
+ 3. If it's an MCP tool too, register it in `src/gfunk/mothership.py`.
52
+ 4. Write tests: `tests/test_cli_<name>.py`.
53
+ 5. Update the vocabulary table in `README.md`.
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "gfunk": {
4
+ "command": "gfunk",
5
+ "args": [
6
+ "mothership"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,53 @@
1
+ version: 2
2
+
3
+ updates:
4
+ # Runtime dependencies. Google's client libraries ship security fixes on
5
+ # their own cadence, so this is the update stream that actually matters.
6
+ - package-ecosystem: uv
7
+ directory: /
8
+ schedule:
9
+ interval: weekly
10
+ open-pull-requests-limit: 5
11
+ groups:
12
+ # One PR for the routine churn; anything major still arrives alone and
13
+ # gets read properly.
14
+ python-minor-and-patch:
15
+ patterns: ['*']
16
+ update-types: [minor, patch]
17
+ commit-message:
18
+ prefix: 'fix'
19
+ prefix-development: 'chore'
20
+ # Wait a week before proposing a newly-published version. The supply-chain
21
+ # attack this blocks is a malicious release that gets auto-merged within
22
+ # minutes of publication; a week is long enough that a compromised version
23
+ # is usually yanked before Dependabot ever opens the PR. Dependabot's
24
+ # *security* updates are a separate stream and are not delayed by this.
25
+ cooldown:
26
+ default-days: 7
27
+
28
+ # A compromised or abandoned action runs with the workflow's token. Keep
29
+ # these current and pinned to a major.
30
+ - package-ecosystem: github-actions
31
+ directory: /
32
+ schedule:
33
+ interval: weekly
34
+ groups:
35
+ actions:
36
+ patterns: ['*']
37
+ commit-message:
38
+ prefix: 'ci'
39
+ cooldown:
40
+ default-days: 7
41
+
42
+ # pre-commit hook revs are pinned by rev: and go stale silently otherwise.
43
+ - package-ecosystem: pre-commit
44
+ directory: /
45
+ schedule:
46
+ interval: weekly
47
+ groups:
48
+ hooks:
49
+ patterns: ['*']
50
+ commit-message:
51
+ prefix: 'ci'
52
+ cooldown:
53
+ default-days: 7
@@ -0,0 +1,159 @@
1
+ # Lint, type-check, and test every push and PR.
2
+ #
3
+ # gfunk is public and holds a user's Workspace credentials at runtime, so the
4
+ # security jobs are part of CI rather than a separate optional workflow.
5
+ name: ci
6
+
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ pull_request:
11
+
12
+ # Least privilege by default; a job that needs more asks for it itself.
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: ${{ github.workflow }}-${{ github.ref }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ check:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
+ with:
26
+ persist-credentials: false
27
+
28
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
29
+ with:
30
+ python-version: '3.14'
31
+ enable-cache: true
32
+
33
+ - run: uv sync
34
+
35
+ - name: Lint
36
+ run: uv run --with ruff ruff check --config .ruff.toml src tests
37
+
38
+ - name: Format
39
+ run: uv run --with ruff ruff format --check --config .ruff.toml src tests
40
+
41
+ - name: Type-check
42
+ run: uv run --with mypy mypy
43
+
44
+ - name: Test
45
+ run: uv run pytest -q --cov --cov-report json
46
+
47
+ - name: Upload coverage
48
+ if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
49
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
50
+ with:
51
+ name: coverage
52
+ path: coverage.json
53
+
54
+ - name: Build
55
+ run: uv build
56
+
57
+ # `--version` must answer on a bare wheel with no dev group, no config,
58
+ # and no credentials -- that is the state a bug reporter is in.
59
+ - name: --version works from the built wheel alone
60
+ run: |
61
+ cd "$(mktemp -d)"
62
+ uv run --isolated --no-project \
63
+ --with "$GITHUB_WORKSPACE"/dist/*.whl gfunk --version
64
+
65
+ hooks:
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
69
+ with:
70
+ # gitleaks needs history to scan; a shallow clone scans one commit.
71
+ fetch-depth: 0
72
+ # No job here pushes, so leaving the token in .git/config only
73
+ # widens what a compromised step could walk off with.
74
+ persist-credentials: false
75
+
76
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
77
+ with:
78
+ python-version: '3.14'
79
+ enable-cache: true
80
+
81
+ - run: uv sync
82
+
83
+ # The same suite the contributor's commit ran, so a bypassed local hook
84
+ # is caught here rather than in review.
85
+ - name: pre-commit
86
+ run: uv run --with pre-commit pre-commit run --all-files --show-diff-on-failure
87
+
88
+ screenshots:
89
+ runs-on: ubuntu-latest
90
+ steps:
91
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
92
+ with:
93
+ persist-credentials: false
94
+
95
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
96
+ with:
97
+ python-version: '3.14'
98
+ enable-cache: true
99
+
100
+ - run: uv sync
101
+
102
+ - name: Regenerate screenshots
103
+ run: uv run python scripts/capture_screenshots.py
104
+
105
+ - name: Check for drift
106
+ run: |
107
+ if ! git diff --quiet docs/screenshots/; then
108
+ echo "::error::Screenshots are stale. Run: uv run python scripts/capture_screenshots.py"
109
+ git diff --stat docs/screenshots/
110
+ exit 1
111
+ fi
112
+
113
+ codeql:
114
+ runs-on: ubuntu-latest
115
+ permissions:
116
+ # Read the code to analyze it, and write the results to the Security tab.
117
+ contents: read
118
+ security-events: write
119
+ steps:
120
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
121
+ with:
122
+ persist-credentials: false
123
+
124
+ - uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
125
+ with:
126
+ languages: python
127
+ queries: security-extended
128
+
129
+ - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
130
+
131
+ coverage-badge:
132
+ if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
133
+ needs: check
134
+ runs-on: ubuntu-latest
135
+ permissions:
136
+ contents: write
137
+ steps:
138
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # zizmor: ignore[artipacked]
139
+
140
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
141
+ with:
142
+ name: coverage
143
+
144
+ - name: Generate badge
145
+ run: |
146
+ COV=$(python3 -c "import json; print(int(json.load(open('coverage.json'))['totals']['percent_covered']))")
147
+ if [ "$COV" -ge 90 ]; then COLOR="brightgreen";
148
+ elif [ "$COV" -ge 75 ]; then COLOR="green";
149
+ elif [ "$COV" -ge 60 ]; then COLOR="yellow";
150
+ else COLOR="red"; fi
151
+ mkdir -p badges
152
+ curl -sS "https://img.shields.io/badge/coverage-${COV}%25-${COLOR}" -o badges/coverage.svg
153
+
154
+ - uses: peaceiris/actions-gh-pages@1ef5a1b1df4c63fe21a2242edbee6cac921ece01 # v4.1.0
155
+ with:
156
+ github_token: ${{ secrets.GITHUB_TOKEN }}
157
+ publish_dir: ./badges
158
+ destination_dir: badges
159
+ keep_files: true
@@ -0,0 +1,47 @@
1
+ # Publish to PyPI when a version tag is pushed (trusted publisher / OIDC).
2
+ # Use: scripts/release.py to bump, review, tag, and push.
3
+ name: publish
4
+
5
+ on:
6
+ push:
7
+ tags:
8
+ - "v*"
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
18
+ with:
19
+ persist-credentials: false
20
+
21
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
22
+ with:
23
+ python-version: '3.14'
24
+ enable-cache: false
25
+
26
+ - run: uv build
27
+
28
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+
33
+ publish:
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment: pypi
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+
45
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
46
+ with:
47
+ print-hash: true
gfunk-0.1.0/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ *credentials.json
2
+ **.env
3
+ token.json
4
+ *token.json
5
+ *.pem
6
+ *.key
7
+ build/
8
+ **/build/**
9
+ .venv/
10
+ **/__pycache__
11
+ **/*.egg-info
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .vscode/
15
+ dist/
@@ -0,0 +1,73 @@
1
+ default_install_hook_types: [pre-commit, commit-msg]
2
+ default_stages: [pre-commit]
3
+
4
+ repos:
5
+ # Fail a bad commit message in ~0.3s rather than after the whole suite.
6
+ - repo: https://github.com/dannybrown37/git-a-grip
7
+ rev: v0.12.0
8
+ hooks:
9
+ - id: commitizen-early
10
+ fail_fast: true
11
+
12
+ - repo: https://github.com/commitizen-tools/commitizen
13
+ rev: v4.17.1
14
+ hooks:
15
+ - id: commitizen
16
+ stages: [commit-msg]
17
+
18
+ # Anything below that reaches `git commit` is in a public repo forever.
19
+ - repo: https://github.com/gitleaks/gitleaks
20
+ rev: v8.30.1
21
+ hooks:
22
+ - id: gitleaks
23
+
24
+ - repo: https://github.com/pre-commit/pre-commit-hooks
25
+ rev: v6.0.0
26
+ hooks:
27
+ - id: detect-private-key
28
+ - id: check-added-large-files
29
+ args: [--maxkb=512]
30
+ - id: end-of-file-fixer
31
+ - id: trailing-whitespace
32
+ - id: check-merge-conflict
33
+ - id: check-toml
34
+ - id: check-yaml
35
+ - id: mixed-line-ending
36
+ # A stray credentials.json or token.json is gitignored, but -f defeats
37
+ # that; this refuses the file itself.
38
+ - id: forbid-new-submodules
39
+
40
+ # A workflow runs with a token; audit it like code.
41
+ - repo: https://github.com/rhysd/actionlint
42
+ rev: v1.7.12
43
+ hooks:
44
+ - id: actionlint
45
+
46
+ - repo: https://github.com/zizmorcore/zizmor-pre-commit
47
+ rev: v1.29.0
48
+ hooks:
49
+ - id: zizmor
50
+
51
+ - repo: https://github.com/dannybrown37/git-a-grip
52
+ rev: v0.12.0
53
+ hooks:
54
+ - id: ruff-check
55
+ args: [--config, .ruff.toml]
56
+ - id: ruff-format
57
+ args: [--config, .ruff.toml]
58
+ - id: mypy
59
+ # A secret scanner cannot find a real customer name or sheet ID. This
60
+ # can. Public repo + Workspace data makes it the load-bearing one.
61
+ - id: block-private-terms
62
+ - id: embed-tree
63
+ - id: pytest
64
+ args: [tests/, -q]
65
+
66
+ - repo: local
67
+ hooks:
68
+ - id: screenshots
69
+ name: screenshots
70
+ language: system
71
+ entry: bash -c 'uv run python scripts/capture_screenshots.py && git add docs/screenshots/*.svg'
72
+ files: '(src/gfunk/(holla_tui|grind_tui|snoop_tui|dubs_tui|regulate_tui|vibe|demo_data)\.py|scripts/capture_screenshots\.py)$'
73
+ pass_filenames: false
gfunk-0.1.0/.ruff.toml ADDED
@@ -0,0 +1,80 @@
1
+ line-length = 88
2
+ show-fixes = true
3
+ target-version = "py313"
4
+
5
+ [lint]
6
+ select = [
7
+ "A",
8
+ "ANN",
9
+ "ARG",
10
+ "ASYNC",
11
+ "B",
12
+ "BLE",
13
+ "C4",
14
+ "C90",
15
+ "COM",
16
+ "D200",
17
+ "D201",
18
+ "D202",
19
+ "D205",
20
+ "D212",
21
+ "E",
22
+ "EM",
23
+ "ERA",
24
+ "EXE",
25
+ "F",
26
+ "FBT",
27
+ "G",
28
+ "I",
29
+ "ICN",
30
+ "ISC",
31
+ "N",
32
+ "PGH",
33
+ "PIE",
34
+ "PL",
35
+ "PT",
36
+ "PTH",
37
+ "PYI",
38
+ "Q",
39
+ "RET",
40
+ "RSE",
41
+ "RUF",
42
+ "S",
43
+ "SIM",
44
+ "SLF",
45
+ "T20",
46
+ "TCH",
47
+ "TID",
48
+ "TRY",
49
+ "UP",
50
+ "W",
51
+ "YTT",
52
+ ]
53
+ ignore = [
54
+ "COM812",
55
+ "I001",
56
+ "ISC001",
57
+ "PLC0415",
58
+ "PLR0913",
59
+ "S101",
60
+ # Python 3.14 evaluates annotations lazily (PEP 649), so hiding imports in a
61
+ # TYPE_CHECKING block buys nothing and costs readability.
62
+ "TRY003",
63
+ ]
64
+
65
+ [lint.per-file-ignores]
66
+ # The CLI is the one place printing to stdout is the product.
67
+ "src/gfunk/cli.py" = ["T20", "ANN401"]
68
+ # Browser-open fallback: only path when no browser is reachable, prints the link.
69
+ "src/gfunk/browser.py" = ["T20"]
70
+ # ANN401: JSON payloads and googleapiclient's `build()` results are genuinely
71
+ # untyped; narrowing them would be a lie rather than a type.
72
+ "src/gfunk/cache.py" = ["ANN401"]
73
+ "src/gfunk/workspace.py" = ["ANN401"]
74
+ "src/gfunk/mothership.py" = ["ANN401"]
75
+ # Workspace is duck-typed here too, same as cli.py's snoop/regulate helpers.
76
+ "src/gfunk/regulate_tui.py" = ["ANN401"]
77
+ # PLR2004/EM101: a literal in an assertion is the assertion.
78
+ # Screenshot capture script prints progress.
79
+ "scripts/*" = ["T20"]
80
+ "tests/*" = ["SLF001", "S105", "S106", "PLR2004", "EM101"]
@@ -0,0 +1,75 @@
1
+ ## v0.1.0 (2026-08-28)
2
+
3
+ ### Feat
4
+
5
+ - snoop should color code by file type now
6
+ - add recency and size sorting options to 'snoop' command
7
+ - add gmail integration and a 'grind' command that shows agenda for next X days
8
+ - expose gfunk vibe
9
+ - default snoop command to opening a TUI with doc viewer
10
+ - pdf attachments in holla archive works!
11
+ - add a modal during archive processing (which can take a while for labels)
12
+ - allow deletion of empty labels (warn for non-empty and reject)
13
+ - holla command gets preview pane with scroll plus delete shortcut
14
+ - continue expanding gfunk holla features
15
+ - set up holla / emails command
16
+ - gfunk dubs finds duplicates or suspected duplicates, TUI view
17
+ - gfunk snoop now offers multi-selection with tab
18
+ - gfunk regulate has a revoke option now
19
+ - regulate prints a nice tui view
20
+ - snoop gets limited live preview and a delete option (trash)
21
+ - add dj push/pull
22
+ - set up apps scripts api access, dj runs now prints recent runs
23
+ - reorganize and give flavor names for each topic in the help menu
24
+ - gfunk drop uploads a file
25
+ - gfunk vibe gets a simple TUI viewer and replaces sample
26
+ - add a (fairly weak) dj subcommand that opens google scripts pages
27
+ - gfunk slide for moving items around drive with fzf and browsing
28
+ - gfunk peep to view docs or sheets in the terminal, fzf selection
29
+ - add gfunk dig/open, gfunk bounce/export
30
+ - initial prototype ready
31
+
32
+ ### Fix
33
+
34
+ - badge/readme update
35
+ - handle some test warnings
36
+ - trailing whitespace in readme
37
+ - show enter instructions on snoop command
38
+ - pre-commit + CI guard for regenerating screenshots when TUIs change
39
+ - improve 'gfunk vibe' menu
40
+ - blue->purple for clarity
41
+ - TUI menu was duplicative, now it's not
42
+ - pytext-xdist to cut test suite time in half, justfile with release script
43
+ - publish.yml for PyPI
44
+ - mothership/mcp should be tool-specific, details on cache in README
45
+ - add license, gitignore terms
46
+ - remove backlog
47
+ - add lookback on grind command
48
+ - improve grind tui
49
+ - TUI descriptions in help
50
+ - bug on vibe TUI, specifically label vibe TUI
51
+ - visual bug when deleting holla items down from >1 to 1
52
+ - when backing up attachments, create a directory and group renders + attachments in it
53
+ - lint
54
+ - cli options tweak; fix ruff toml issue driving me crazy in nvim
55
+ - header with flavor
56
+ - pre-commit passage
57
+ - y/n casing
58
+ - make revoke/delete easier in gfunk regulate
59
+ - Greatly improve layout of TUI on regulate command
60
+ - dj triggers label clarity
61
+ - add scripts.projects scope
62
+ - hanging tests, more flavor names
63
+ - merge everything into snoop, too many similar commands
64
+ - merged dig into peep, added a print action for snoop, update rules
65
+ - merge snoop and slide commands into one
66
+ - improve cli help
67
+ - remove get-down, it was confusing overlap, just a single setup/login/mount-up command
68
+ - downgrade to 3.13 for ruff lsp reasons
69
+ - slight update to alias and description for gfunk sample
70
+ - gfunk dig doesn't ask for ID if Ctrl+C out of fzf
71
+ - mothership has install and serve sub commands now
72
+ - narrow dig to just opening a file in your browser
73
+ - alias sample, remove mix, alias mothership
74
+ - pass linting
75
+ - make mount-up do better bootstrapping
gfunk-0.1.0/Justfile ADDED
@@ -0,0 +1,21 @@
1
+ # gfunk task runner
2
+
3
+ default:
4
+ @echo "Usage: just <recipe>"
5
+ @echo ""
6
+ @echo " release Interactive version bump, review, and publish"
7
+ @echo " install Install dependencies"
8
+ @echo " test Run the test suite with coverage"
9
+ @echo " lint Run all pre-commit hooks"
10
+
11
+ install:
12
+ uv sync
13
+
14
+ release:
15
+ uv run python scripts/release.py
16
+
17
+ test:
18
+ uv run pytest tests/ -q --cov=gfunk --cov-report=term-missing
19
+
20
+ lint:
21
+ uv run pre-commit run --all-files
gfunk-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Danny Brown
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.
gfunk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.5
2
+ Name: gfunk
3
+ Version: 0.1.0
4
+ Summary: MCP server and CLI for programmatic Google Workspace access
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: google-api-python-client>=2.198.0
8
+ Requires-Dist: google-auth-httplib2>=0.4.1
9
+ Requires-Dist: google-auth-oauthlib>=1.4.0
10
+ Requires-Dist: mcp>=2.0.0
11
+ Requires-Dist: tabulate>=0.10.0
12
+ Requires-Dist: textual>=3.0.0
13
+ Requires-Dist: xhtml2pdf>=0.2.17