gumloop 0.4.2__tar.gz → 0.4.4__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.
- gumloop-0.4.4/.github/workflows/release.yml +46 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/PKG-INFO +20 -4
- {gumloop-0.4.2 → gumloop-0.4.4}/README.md +19 -3
- {gumloop-0.4.2 → gumloop-0.4.4}/pyproject.toml +4 -1
- gumloop-0.4.4/scripts/install.sh +286 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_version.py +1 -1
- gumloop-0.4.4/src/gumloop/cli/commands/update.py +164 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/main.py +5 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/types.py +9 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/uv.lock +0 -1
- {gumloop-0.4.2 → gumloop-0.4.4}/.env.example +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/.gitignore +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/.pre-commit-config.yaml +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/.python-version +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/LICENSE +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/scripts/check +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/scripts/fmt +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/scripts/sdk_examples.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_auth.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_client.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_http.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/_args_input.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/_downloads.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/_sync_output.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/agents.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/artifacts.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/auth.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/brain.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/chat.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/mcp.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/sessions.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/skills.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/sync.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/console.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/context.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/credentials.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/errors.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/oauth.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/errors.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/oauth.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/py.typed +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/agents.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/artifacts.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/brain.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/chat.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/mcp.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/sessions.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/skills.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/sync.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/teams.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/spec/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/spec/_compat.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/spec/_extensions.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/archive.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/bundle.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/bundle_validation.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/errors.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/hashing.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/installed.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/local_state.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/lock.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/markers.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/reconcile.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/reporting.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/results.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/run.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/scheduler.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/target_files.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/targets.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/wire.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/conftest.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/sync_scenario_helpers.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/sync_test_fakes.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_agents.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_args_input.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_artifacts.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_brain.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_chat.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_context.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_credentials.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_home_isolation.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_login.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_main.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_mcp.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sessions.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_skills.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_bundle.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_command.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_command_safety.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_contract_fixtures.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_enrollment.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_fixture_corpus.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_hashing.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_installed.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_reconciliation_safety.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_scheduler_macos.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_targets.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/conftest.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/README.md +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/generated-bundle-transfer-limit.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-bytes-per-file-limit.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-device.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-fifo.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-files-per-skill-limit.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-nul-name.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-socket.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-symlink.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-total-uncompressed-limit.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-windows-drive-prefix.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/bundles/newer-than-plan.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/bundles/normal.zip +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/catalog.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/corpus.sha256 +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/hashes/content-v1.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/hashes/manifest-v1.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-identity.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-install-name.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/newer-than-plan.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/normal-bundle.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/below-pro.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/lost-membership.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/normal-plan.json +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/integration/conftest.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/integration/test_live.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/__init__.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/conftest.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/helpers.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_agents.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_artifacts.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_brain.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_chat.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_client.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_mcp.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_oauth.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_sessions.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_skills.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_sync.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_teams.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/skill_sync_fixtures.py +0 -0
- {gumloop-0.4.2 → gumloop-0.4.4}/tests/test_client.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # PyPI trusted publishing
|
|
14
|
+
contents: write # create the GitHub Release
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
19
|
+
|
|
20
|
+
- name: Verify tag matches package version
|
|
21
|
+
run: |
|
|
22
|
+
version="$(uvx hatchling version)"
|
|
23
|
+
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
|
|
24
|
+
echo "Tag ${GITHUB_REF_NAME} does not match package version ${version}" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
# --check-url skips files already on PyPI so the job is safe to rerun
|
|
32
|
+
# if a later step fails.
|
|
33
|
+
- name: Publish to PyPI
|
|
34
|
+
run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/
|
|
35
|
+
|
|
36
|
+
# A tag created via the Releases UI already has a release; just attach
|
|
37
|
+
# the artifacts to it.
|
|
38
|
+
- name: Create GitHub Release
|
|
39
|
+
env:
|
|
40
|
+
GH_TOKEN: ${{ github.token }}
|
|
41
|
+
run: |
|
|
42
|
+
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
|
|
43
|
+
gh release upload "${GITHUB_REF_NAME}" dist/* --clobber
|
|
44
|
+
else
|
|
45
|
+
gh release create "${GITHUB_REF_NAME}" dist/* --generate-notes --verify-tag
|
|
46
|
+
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gumloop
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Python client for the Gumloop API
|
|
5
5
|
Project-URL: Homepage, https://github.com/gumloop/gumloop-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/gumloop/gumloop-py/issues
|
|
@@ -25,12 +25,28 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
# Gumloop Python Client
|
|
27
27
|
|
|
28
|
-
A Python client for the Gumloop API that makes it easy to run and monitor Gumloop flows.
|
|
28
|
+
A Python client for the Gumloop API that makes it easy to run and monitor Gumloop flows, plus the `gumloop` CLI.
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## CLI
|
|
31
|
+
|
|
32
|
+
Install on macOS or Linux:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -fsSL https://gumloop.com/cli/install.sh | sh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The installer is fully self-contained under `~/.gumloop` — it ships its own Python, never touches your system Python, and needs no sudo. Run `gumloop --help` to get started and update any time with:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
gumloop update
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## SDK
|
|
45
|
+
|
|
46
|
+
To use the client as a library in your own Python project:
|
|
31
47
|
|
|
32
48
|
```bash
|
|
33
|
-
|
|
49
|
+
uv add gumloop
|
|
34
50
|
```
|
|
35
51
|
|
|
36
52
|
## Usage
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
# Gumloop Python Client
|
|
2
2
|
|
|
3
|
-
A Python client for the Gumloop API that makes it easy to run and monitor Gumloop flows.
|
|
3
|
+
A Python client for the Gumloop API that makes it easy to run and monitor Gumloop flows, plus the `gumloop` CLI.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
Install on macOS or Linux:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
curl -fsSL https://gumloop.com/cli/install.sh | sh
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The installer is fully self-contained under `~/.gumloop` — it ships its own Python, never touches your system Python, and needs no sudo. Run `gumloop --help` to get started and update any time with:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gumloop update
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## SDK
|
|
20
|
+
|
|
21
|
+
To use the client as a library in your own Python project:
|
|
6
22
|
|
|
7
23
|
```bash
|
|
8
|
-
|
|
24
|
+
uv add gumloop
|
|
9
25
|
```
|
|
10
26
|
|
|
11
27
|
## Usage
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gumloop"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Python client for the Gumloop API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -32,6 +32,9 @@ dependencies = [
|
|
|
32
32
|
"typer>=0.25.1",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
path = "src/gumloop/_version.py"
|
|
37
|
+
|
|
35
38
|
[project.scripts]
|
|
36
39
|
gumloop = "gumloop.cli.main:main"
|
|
37
40
|
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Gumloop CLI installer: curl -fsSL https://gumloop.com/cli/install.sh | sh
|
|
3
|
+
|
|
4
|
+
set -eu
|
|
5
|
+
|
|
6
|
+
UV_VERSION="0.11.31"
|
|
7
|
+
PYTHON_VERSION="3.12"
|
|
8
|
+
INSTALLER_VERSION="1"
|
|
9
|
+
|
|
10
|
+
GUMLOOP_HOME="${GUMLOOP_INSTALL_DIR:-$HOME/.gumloop}"
|
|
11
|
+
VENV_DIR="$GUMLOOP_HOME/venv"
|
|
12
|
+
UV_BIN="$GUMLOOP_HOME/bin/uv"
|
|
13
|
+
SHIM_DIR="$HOME/.local/bin"
|
|
14
|
+
SHIM_PATH="$SHIM_DIR/gumloop"
|
|
15
|
+
|
|
16
|
+
if [ -t 1 ]; then
|
|
17
|
+
BOLD="$(printf '\033[1m')"
|
|
18
|
+
RED="$(printf '\033[31m')"
|
|
19
|
+
GREEN="$(printf '\033[32m')"
|
|
20
|
+
YELLOW="$(printf '\033[33m')"
|
|
21
|
+
RESET="$(printf '\033[0m')"
|
|
22
|
+
else
|
|
23
|
+
BOLD="" RED="" GREEN="" YELLOW="" RESET=""
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
step() {
|
|
27
|
+
printf '%s==>%s %s\n' "${BOLD}" "${RESET}" "$1"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
warn() {
|
|
31
|
+
printf '%swarning:%s %s\n' "${YELLOW}" "${RESET}" "$1" >&2
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fail() {
|
|
35
|
+
printf '%serror:%s %s\n' "${RED}" "${RESET}" "$1" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# [ -r /dev/tty ] passes in CI/containers where the open still fails.
|
|
40
|
+
has_tty() {
|
|
41
|
+
( : < /dev/tty ) 2>/dev/null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Prompts read /dev/tty, not stdin: under `curl | sh` stdin is the script.
|
|
45
|
+
confirm() {
|
|
46
|
+
if ! has_tty; then
|
|
47
|
+
return 1
|
|
48
|
+
fi
|
|
49
|
+
printf '%s [y/N] ' "$1"
|
|
50
|
+
answer=""
|
|
51
|
+
read -r answer < /dev/tty || { printf '\n'; return 1; }
|
|
52
|
+
case "$answer" in
|
|
53
|
+
y|Y|yes|YES|Yes) return 0 ;;
|
|
54
|
+
*) return 1 ;;
|
|
55
|
+
esac
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
confirm_yes() {
|
|
59
|
+
if ! has_tty; then
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
printf '%s [Y/n] ' "$1"
|
|
63
|
+
answer=""
|
|
64
|
+
read -r answer < /dev/tty || { printf '\n'; return 0; }
|
|
65
|
+
case "$answer" in
|
|
66
|
+
n|N|no|NO|No) return 1 ;;
|
|
67
|
+
*) return 0 ;;
|
|
68
|
+
esac
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
|
|
72
|
+
fail "do not run this installer with sudo. It installs into your home directory and needs no root access."
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
case "$(uname -s)" in
|
|
76
|
+
Darwin) os="macos" ;;
|
|
77
|
+
Linux) os="linux" ;;
|
|
78
|
+
*) fail "unsupported operating system: $(uname -s). The Gumloop CLI supports macOS and Linux (use WSL on Windows)." ;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
case "$(uname -m)" in
|
|
82
|
+
x86_64|amd64) arch="x86_64" ;;
|
|
83
|
+
arm64|aarch64) arch="aarch64" ;;
|
|
84
|
+
*) fail "unsupported architecture: $(uname -m)" ;;
|
|
85
|
+
esac
|
|
86
|
+
|
|
87
|
+
if [ "$os" = "macos" ] && [ "$arch" = "x86_64" ]; then
|
|
88
|
+
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = "1" ]; then
|
|
89
|
+
arch="aarch64"
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [ "$os" = "macos" ]; then
|
|
94
|
+
uv_target="${arch}-apple-darwin"
|
|
95
|
+
platform_label="macOS (${arch})"
|
|
96
|
+
else
|
|
97
|
+
libc="gnu"
|
|
98
|
+
if ldd /bin/ls 2>&1 | grep -q musl; then
|
|
99
|
+
libc="musl"
|
|
100
|
+
fi
|
|
101
|
+
uv_target="${arch}-unknown-linux-${libc}"
|
|
102
|
+
platform_label="Linux (${arch}, ${libc})"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
downloader=""
|
|
106
|
+
if command -v curl >/dev/null 2>&1; then
|
|
107
|
+
downloader="curl"
|
|
108
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
109
|
+
downloader="wget"
|
|
110
|
+
else
|
|
111
|
+
fail "curl or wget is required to install the Gumloop CLI."
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
download() {
|
|
115
|
+
url="$1"
|
|
116
|
+
output="$2"
|
|
117
|
+
if [ "$downloader" = "curl" ]; then
|
|
118
|
+
curl -fsSL -o "$output" "$url"
|
|
119
|
+
else
|
|
120
|
+
wget -q -O "$output" "$url"
|
|
121
|
+
fi
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
download_text() {
|
|
125
|
+
url="$1"
|
|
126
|
+
if [ "$downloader" = "curl" ]; then
|
|
127
|
+
curl -fsSL "$url"
|
|
128
|
+
else
|
|
129
|
+
wget -q -O - "$url"
|
|
130
|
+
fi
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
sha256_of() {
|
|
134
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
135
|
+
sha256sum "$1" | cut -d' ' -f1
|
|
136
|
+
else
|
|
137
|
+
shasum -a 256 "$1" | cut -d' ' -f1
|
|
138
|
+
fi
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if [ -x "$VENV_DIR/bin/gumloop" ]; then
|
|
142
|
+
install_mode="Updating"
|
|
143
|
+
else
|
|
144
|
+
install_mode="Installing"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
step "${install_mode} Gumloop CLI"
|
|
148
|
+
step "Detected platform: ${platform_label}"
|
|
149
|
+
|
|
150
|
+
if [ "$install_mode" = "Updating" ]; then
|
|
151
|
+
installed_version="$("$VENV_DIR/bin/gumloop" --version 2>/dev/null || true)"
|
|
152
|
+
if ! confirm_yes "Gumloop CLI${installed_version:+ ${installed_version}} is already installed. Reinstall?"; then
|
|
153
|
+
step "Keeping the existing installation"
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
tmp_dir="$(mktemp -d)"
|
|
159
|
+
trap 'rm -rf "$tmp_dir"' EXIT INT TERM
|
|
160
|
+
|
|
161
|
+
existing="$(command -v gumloop 2>/dev/null || true)"
|
|
162
|
+
if [ -n "$existing" ] && [ "$existing" != "$SHIM_PATH" ]; then
|
|
163
|
+
case "$(readlink "$existing" 2>/dev/null || echo "$existing")" in
|
|
164
|
+
"$GUMLOOP_HOME"/*) ;;
|
|
165
|
+
*)
|
|
166
|
+
method="pip"
|
|
167
|
+
case "$existing" in
|
|
168
|
+
*pipx*) method="pipx" ;;
|
|
169
|
+
*"/uv/tools/"*) method="uv tool" ;;
|
|
170
|
+
esac
|
|
171
|
+
if confirm "Found existing gumloop at ${existing} (installed via ${method}). Remove it and switch to the managed install?"; then
|
|
172
|
+
step "Removing ${method} install"
|
|
173
|
+
case "$method" in
|
|
174
|
+
pipx) pipx uninstall gumloop >/dev/null 2>&1 || warn "pipx uninstall failed; remove it manually with: pipx uninstall gumloop" ;;
|
|
175
|
+
"uv tool") uv tool uninstall gumloop >/dev/null 2>&1 || warn "uv tool uninstall failed; remove it manually with: uv tool uninstall gumloop" ;;
|
|
176
|
+
pip)
|
|
177
|
+
shebang_python="$(head -n 1 "$existing" 2>/dev/null | sed 's/^#!//')"
|
|
178
|
+
if [ -x "$shebang_python" ] && "$shebang_python" -m pip uninstall -y gumloop >/dev/null 2>&1; then
|
|
179
|
+
:
|
|
180
|
+
else
|
|
181
|
+
warn "could not uninstall automatically; remove it manually with: pip uninstall gumloop"
|
|
182
|
+
fi
|
|
183
|
+
;;
|
|
184
|
+
esac
|
|
185
|
+
else
|
|
186
|
+
warn "keeping the existing install; it may shadow the new 'gumloop' command on your PATH."
|
|
187
|
+
fi
|
|
188
|
+
;;
|
|
189
|
+
esac
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
uv_asset="uv-${uv_target}.tar.gz"
|
|
193
|
+
uv_url="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/${uv_asset}"
|
|
194
|
+
|
|
195
|
+
if [ -x "$UV_BIN" ] && [ "$("$UV_BIN" --version 2>/dev/null | cut -d' ' -f2)" = "$UV_VERSION" ]; then
|
|
196
|
+
:
|
|
197
|
+
else
|
|
198
|
+
step "Downloading uv ${UV_VERSION}"
|
|
199
|
+
download "$uv_url" "$tmp_dir/$uv_asset"
|
|
200
|
+
|
|
201
|
+
expected_sha="$(download_text "${uv_url}.sha256" | cut -d' ' -f1)"
|
|
202
|
+
actual_sha="$(sha256_of "$tmp_dir/$uv_asset")"
|
|
203
|
+
if [ -z "$expected_sha" ] || [ "$expected_sha" != "$actual_sha" ]; then
|
|
204
|
+
fail "checksum verification failed for ${uv_asset}"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
tar -xzf "$tmp_dir/$uv_asset" -C "$tmp_dir"
|
|
208
|
+
mkdir -p "$GUMLOOP_HOME/bin"
|
|
209
|
+
mv "$tmp_dir/uv-${uv_target}/uv" "$UV_BIN"
|
|
210
|
+
chmod 0755 "$UV_BIN"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
if [ -n "${GUMLOOP_VERSION:-}" ]; then
|
|
214
|
+
install_spec="gumloop==${GUMLOOP_VERSION}"
|
|
215
|
+
else
|
|
216
|
+
install_spec="gumloop"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
export UV_PYTHON_INSTALL_DIR="$GUMLOOP_HOME/python"
|
|
220
|
+
export UV_CACHE_DIR="$GUMLOOP_HOME/cache"
|
|
221
|
+
|
|
222
|
+
step "Installing Python ${PYTHON_VERSION} to ${GUMLOOP_HOME}"
|
|
223
|
+
rm -rf "$VENV_DIR"
|
|
224
|
+
# Without --managed-python uv silently reuses any system Python it finds.
|
|
225
|
+
"$UV_BIN" venv "$VENV_DIR" --python "$PYTHON_VERSION" --managed-python --quiet
|
|
226
|
+
|
|
227
|
+
step "Installing ${install_spec} from PyPI"
|
|
228
|
+
"$UV_BIN" pip install --python "$VENV_DIR/bin/python" --quiet "$install_spec"
|
|
229
|
+
|
|
230
|
+
version="$("$VENV_DIR/bin/python" -c 'from gumloop import __version__; print(__version__)')"
|
|
231
|
+
step "Resolved version: ${version}"
|
|
232
|
+
|
|
233
|
+
mkdir -p "$SHIM_DIR"
|
|
234
|
+
ln -sf "$VENV_DIR/bin/gumloop" "$SHIM_PATH"
|
|
235
|
+
|
|
236
|
+
cat > "$GUMLOOP_HOME/install.json" <<EOF
|
|
237
|
+
{
|
|
238
|
+
"install_method": "installer",
|
|
239
|
+
"installer_version": "${INSTALLER_VERSION}",
|
|
240
|
+
"version": "${version}",
|
|
241
|
+
"installed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
242
|
+
}
|
|
243
|
+
EOF
|
|
244
|
+
|
|
245
|
+
"$SHIM_PATH" --version >/dev/null 2>&1 || fail "installed CLI failed to run; try re-running the installer."
|
|
246
|
+
|
|
247
|
+
case ":$PATH:" in
|
|
248
|
+
*":$SHIM_DIR:"*)
|
|
249
|
+
step "${SHIM_DIR} is already on PATH"
|
|
250
|
+
;;
|
|
251
|
+
*)
|
|
252
|
+
warn "${SHIM_DIR} is not on your PATH."
|
|
253
|
+
printf '\nAdd it to your shell profile (e.g. ~/.zshrc or ~/.bashrc):\n'
|
|
254
|
+
printf ' export PATH="%s:$PATH"\n\n' "$SHIM_DIR"
|
|
255
|
+
;;
|
|
256
|
+
esac
|
|
257
|
+
|
|
258
|
+
printf '\n%s✓ Gumloop CLI %s installed successfully.%s\n' "${GREEN}" "${version}" "${RESET}"
|
|
259
|
+
printf 'Run %sgumloop --help%s to get started, %sgumloop update%s to update.\n\n' \
|
|
260
|
+
"${BOLD}" "${RESET}" "${BOLD}" "${RESET}"
|
|
261
|
+
|
|
262
|
+
login_state="$("$VENV_DIR/bin/python" - 2>/dev/null <<'EOF' || true
|
|
263
|
+
from gumloop.cli.credentials import is_keyring_available, load_credentials
|
|
264
|
+
|
|
265
|
+
if load_credentials().has_any:
|
|
266
|
+
print("logged_in")
|
|
267
|
+
elif is_keyring_available():
|
|
268
|
+
print("prompt")
|
|
269
|
+
else:
|
|
270
|
+
print("no_keyring")
|
|
271
|
+
EOF
|
|
272
|
+
)"
|
|
273
|
+
|
|
274
|
+
case "$login_state" in
|
|
275
|
+
logged_in) ;;
|
|
276
|
+
no_keyring)
|
|
277
|
+
printf 'No OS keychain was found, so `gumloop login` cannot store credentials on this machine.\n'
|
|
278
|
+
printf 'Authenticate with environment variables instead (GUMLOOP_API_KEY + GUMLOOP_USER_ID),\n'
|
|
279
|
+
printf 'or install a keychain (e.g. apt install gnome-keyring libsecret-1-0) and run `gumloop login`.\n\n'
|
|
280
|
+
;;
|
|
281
|
+
*)
|
|
282
|
+
if has_tty && confirm "Log in to Gumloop now?"; then
|
|
283
|
+
"$SHIM_PATH" login < /dev/tty || true
|
|
284
|
+
fi
|
|
285
|
+
;;
|
|
286
|
+
esac
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from gumloop import __version__
|
|
15
|
+
from gumloop.cli.console import console
|
|
16
|
+
from gumloop.cli.console import error_console
|
|
17
|
+
|
|
18
|
+
PYPI_JSON_URL = "https://pypi.org/pypi/gumloop/json"
|
|
19
|
+
UPDATE_CHECK_INTERVAL_SECONDS = 24 * 60 * 60
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def gumloop_home() -> Path:
|
|
23
|
+
override = os.environ.get("GUMLOOP_INSTALL_DIR")
|
|
24
|
+
return Path(override) if override else Path.home() / ".gumloop"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_managed_install() -> bool:
|
|
28
|
+
try:
|
|
29
|
+
prefix = Path(sys.prefix).resolve()
|
|
30
|
+
return prefix == (gumloop_home() / "venv").resolve()
|
|
31
|
+
except OSError:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fetch_latest_version(timeout: float = 10.0) -> str:
|
|
36
|
+
response = httpx.get(PYPI_JSON_URL, timeout=timeout, follow_redirects=True)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return response.json()["info"]["version"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _version_tuple(version: str) -> tuple[int, ...] | None:
|
|
42
|
+
parts: list[int] = []
|
|
43
|
+
for part in version.split("."):
|
|
44
|
+
digits = ""
|
|
45
|
+
for char in part:
|
|
46
|
+
if not char.isdigit():
|
|
47
|
+
break
|
|
48
|
+
digits += char
|
|
49
|
+
if not digits:
|
|
50
|
+
return None
|
|
51
|
+
parts.append(int(digits))
|
|
52
|
+
return tuple(parts) if parts else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_newer(latest: str, current: str) -> bool:
|
|
56
|
+
latest_t = _version_tuple(latest)
|
|
57
|
+
current_t = _version_tuple(current)
|
|
58
|
+
if latest_t is None or current_t is None:
|
|
59
|
+
return latest != current
|
|
60
|
+
return latest_t > current_t
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _non_managed_hint() -> str:
|
|
64
|
+
prefix = sys.prefix
|
|
65
|
+
if "pipx" in prefix:
|
|
66
|
+
return "pipx upgrade gumloop"
|
|
67
|
+
if f"{os.sep}uv{os.sep}tools{os.sep}" in prefix:
|
|
68
|
+
return "uv tool upgrade gumloop"
|
|
69
|
+
return "uv pip install --upgrade gumloop"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def update() -> None:
|
|
73
|
+
"""Update the Gumloop CLI to the latest version."""
|
|
74
|
+
try:
|
|
75
|
+
latest = fetch_latest_version()
|
|
76
|
+
except (httpx.HTTPError, KeyError, ValueError):
|
|
77
|
+
error_console.print("[red]Error:[/red] could not reach PyPI to check for updates.")
|
|
78
|
+
raise typer.Exit(1) from None
|
|
79
|
+
|
|
80
|
+
if not is_newer(latest, __version__):
|
|
81
|
+
console.print(f"gumloop {__version__} is already the latest version.")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if not is_managed_install():
|
|
85
|
+
console.print(
|
|
86
|
+
f"gumloop [bold]{latest}[/bold] is available (you have {__version__}).\n"
|
|
87
|
+
f"This install is not managed by the Gumloop installer; update it with:\n"
|
|
88
|
+
f" [bold]{_non_managed_hint()}[/bold]"
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
home = gumloop_home()
|
|
93
|
+
uv = home / "bin" / "uv"
|
|
94
|
+
python = home / "venv" / "bin" / "python"
|
|
95
|
+
if not uv.is_file() or not python.is_file():
|
|
96
|
+
error_console.print(
|
|
97
|
+
"[red]Error:[/red] managed install is missing its uv or Python. Repair it by re-running:\n"
|
|
98
|
+
" curl -fsSL https://gumloop.com/cli/install.sh | sh"
|
|
99
|
+
)
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
|
|
102
|
+
console.print(f"Updating gumloop {__version__} -> [bold]{latest}[/bold] ...")
|
|
103
|
+
env = os.environ.copy()
|
|
104
|
+
env["UV_PYTHON_INSTALL_DIR"] = str(home / "python")
|
|
105
|
+
env["UV_CACHE_DIR"] = str(home / "cache")
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
[str(uv), "pip", "install", "--python", str(python), "--quiet", f"gumloop=={latest}"],
|
|
108
|
+
env=env,
|
|
109
|
+
capture_output=True,
|
|
110
|
+
text=True,
|
|
111
|
+
)
|
|
112
|
+
if result.returncode != 0:
|
|
113
|
+
error_console.print(f"[red]Error:[/red] update failed:\n{result.stderr.strip()}")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
_write_check_state(latest)
|
|
117
|
+
console.print(f"[green]Updated to gumloop {latest}.[/green]")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _check_state_path() -> Path:
|
|
121
|
+
return gumloop_home() / "update_check.json"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _write_check_state(latest: str) -> None:
|
|
125
|
+
try:
|
|
126
|
+
path = _check_state_path()
|
|
127
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
path.write_text(json.dumps({"checked_at": time.time(), "latest": latest}))
|
|
129
|
+
except OSError:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _refresh_check_state() -> None:
|
|
134
|
+
try:
|
|
135
|
+
_write_check_state(fetch_latest_version(timeout=5.0))
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def maybe_notify_update() -> None:
|
|
141
|
+
"""Runs on every CLI invocation; must never block or raise."""
|
|
142
|
+
try:
|
|
143
|
+
if os.environ.get("GUMLOOP_NO_UPDATE_CHECK") or os.environ.get("CI"):
|
|
144
|
+
return
|
|
145
|
+
if not sys.stderr.isatty():
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
state: dict[str, object] = {}
|
|
149
|
+
try:
|
|
150
|
+
state = json.loads(_check_state_path().read_text())
|
|
151
|
+
except (OSError, json.JSONDecodeError):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
checked_at = state.get("checked_at")
|
|
155
|
+
stale = not isinstance(checked_at, (int, float)) or time.time() - checked_at > UPDATE_CHECK_INTERVAL_SECONDS
|
|
156
|
+
if stale:
|
|
157
|
+
threading.Thread(target=_refresh_check_state, daemon=True).start()
|
|
158
|
+
|
|
159
|
+
latest = state.get("latest")
|
|
160
|
+
if isinstance(latest, str) and is_newer(latest, __version__):
|
|
161
|
+
command = "gumloop update" if is_managed_install() else _non_managed_hint()
|
|
162
|
+
error_console.print(f"[dim]A new version of gumloop is available: {latest} (run `{command}`).[/dim]")
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
@@ -17,6 +17,8 @@ from gumloop.cli.commands.mcp import mcp_app
|
|
|
17
17
|
from gumloop.cli.commands.sessions import sessions_app
|
|
18
18
|
from gumloop.cli.commands.skills import skills_app
|
|
19
19
|
from gumloop.cli.commands.sync import sync_app
|
|
20
|
+
from gumloop.cli.commands.update import maybe_notify_update
|
|
21
|
+
from gumloop.cli.commands.update import update as update_command
|
|
20
22
|
from gumloop.cli.context import CliContext
|
|
21
23
|
from gumloop.cli.credentials import load_credentials
|
|
22
24
|
|
|
@@ -93,6 +95,7 @@ app.command("login", epilog="Examples:\n gumloop login\n gumloop login --api-k
|
|
|
93
95
|
login_command
|
|
94
96
|
)
|
|
95
97
|
app.command("logout")(logout_command)
|
|
98
|
+
app.command("update")(update_command)
|
|
96
99
|
app.add_typer(mcp_app, name="mcp")
|
|
97
100
|
app.add_typer(agents_app, name="agents")
|
|
98
101
|
app.add_typer(sessions_app, name="sessions")
|
|
@@ -119,6 +122,8 @@ def _require_supported_platform() -> None:
|
|
|
119
122
|
|
|
120
123
|
def main() -> None:
|
|
121
124
|
_require_supported_platform()
|
|
125
|
+
if sys.argv[1:2] != ["update"]:
|
|
126
|
+
maybe_notify_update()
|
|
122
127
|
app()
|
|
123
128
|
|
|
124
129
|
|
|
@@ -180,6 +180,14 @@ class MessagePayload(_Model):
|
|
|
180
180
|
parts: list[dict[str, Any]] | None = None
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
class SessionUsage(_Model):
|
|
184
|
+
credit_cost: float | None = None
|
|
185
|
+
tool_credit_cost: float | None = None
|
|
186
|
+
flow_credit_cost: float | None = None
|
|
187
|
+
input_tokens: int | None = None
|
|
188
|
+
output_tokens: int | None = None
|
|
189
|
+
|
|
190
|
+
|
|
183
191
|
class Session(_Model):
|
|
184
192
|
id: str
|
|
185
193
|
agent_id: str
|
|
@@ -195,6 +203,7 @@ class Session(_Model):
|
|
|
195
203
|
agent_tools: list[dict[str, Any]] = Field(default_factory=list)
|
|
196
204
|
participants: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
197
205
|
creator: CreatorPayload | None = None
|
|
206
|
+
usage: SessionUsage | None = None
|
|
198
207
|
|
|
199
208
|
|
|
200
209
|
class SessionResponse(_Model):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip
RENAMED
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip
RENAMED
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json
RENAMED
|
File without changes
|
|
File without changes
|
{gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|