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.
Files changed (156) hide show
  1. gumloop-0.4.4/.github/workflows/release.yml +46 -0
  2. {gumloop-0.4.2 → gumloop-0.4.4}/PKG-INFO +20 -4
  3. {gumloop-0.4.2 → gumloop-0.4.4}/README.md +19 -3
  4. {gumloop-0.4.2 → gumloop-0.4.4}/pyproject.toml +4 -1
  5. gumloop-0.4.4/scripts/install.sh +286 -0
  6. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_version.py +1 -1
  7. gumloop-0.4.4/src/gumloop/cli/commands/update.py +164 -0
  8. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/main.py +5 -0
  9. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/types.py +9 -0
  10. {gumloop-0.4.2 → gumloop-0.4.4}/uv.lock +0 -1
  11. {gumloop-0.4.2 → gumloop-0.4.4}/.env.example +0 -0
  12. {gumloop-0.4.2 → gumloop-0.4.4}/.gitignore +0 -0
  13. {gumloop-0.4.2 → gumloop-0.4.4}/.pre-commit-config.yaml +0 -0
  14. {gumloop-0.4.2 → gumloop-0.4.4}/.python-version +0 -0
  15. {gumloop-0.4.2 → gumloop-0.4.4}/LICENSE +0 -0
  16. {gumloop-0.4.2 → gumloop-0.4.4}/scripts/check +0 -0
  17. {gumloop-0.4.2 → gumloop-0.4.4}/scripts/fmt +0 -0
  18. {gumloop-0.4.2 → gumloop-0.4.4}/scripts/sdk_examples.py +0 -0
  19. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/__init__.py +0 -0
  20. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_auth.py +0 -0
  21. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_client.py +0 -0
  22. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/_http.py +0 -0
  23. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/__init__.py +0 -0
  24. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/__init__.py +0 -0
  25. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/_args_input.py +0 -0
  26. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/_downloads.py +0 -0
  27. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/_sync_output.py +0 -0
  28. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/agents.py +0 -0
  29. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/artifacts.py +0 -0
  30. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/auth.py +0 -0
  31. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/brain.py +0 -0
  32. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/chat.py +0 -0
  33. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/mcp.py +0 -0
  34. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/sessions.py +0 -0
  35. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/skills.py +0 -0
  36. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/commands/sync.py +0 -0
  37. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/console.py +0 -0
  38. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/context.py +0 -0
  39. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/credentials.py +0 -0
  40. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/errors.py +0 -0
  41. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/cli/oauth.py +0 -0
  42. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/errors.py +0 -0
  43. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/oauth.py +0 -0
  44. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/py.typed +0 -0
  45. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/__init__.py +0 -0
  46. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/agents.py +0 -0
  47. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/artifacts.py +0 -0
  48. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/brain.py +0 -0
  49. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/chat.py +0 -0
  50. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/mcp.py +0 -0
  51. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/sessions.py +0 -0
  52. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/skills.py +0 -0
  53. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/sync.py +0 -0
  54. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/resources/teams.py +0 -0
  55. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/spec/__init__.py +0 -0
  56. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/spec/_compat.py +0 -0
  57. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/spec/_extensions.py +0 -0
  58. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/__init__.py +0 -0
  59. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/archive.py +0 -0
  60. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/bundle.py +0 -0
  61. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/bundle_validation.py +0 -0
  62. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/errors.py +0 -0
  63. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/hashing.py +0 -0
  64. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/installed.py +0 -0
  65. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/local_state.py +0 -0
  66. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/lock.py +0 -0
  67. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/markers.py +0 -0
  68. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/reconcile.py +0 -0
  69. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/reporting.py +0 -0
  70. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/results.py +0 -0
  71. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/run.py +0 -0
  72. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/scheduler.py +0 -0
  73. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/target_files.py +0 -0
  74. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/targets.py +0 -0
  75. {gumloop-0.4.2 → gumloop-0.4.4}/src/gumloop/sync/wire.py +0 -0
  76. {gumloop-0.4.2 → gumloop-0.4.4}/tests/__init__.py +0 -0
  77. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/__init__.py +0 -0
  78. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/conftest.py +0 -0
  79. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/sync_scenario_helpers.py +0 -0
  80. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/sync_test_fakes.py +0 -0
  81. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_agents.py +0 -0
  82. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_args_input.py +0 -0
  83. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_artifacts.py +0 -0
  84. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_brain.py +0 -0
  85. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_chat.py +0 -0
  86. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_context.py +0 -0
  87. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_credentials.py +0 -0
  88. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_home_isolation.py +0 -0
  89. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_login.py +0 -0
  90. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_main.py +0 -0
  91. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_mcp.py +0 -0
  92. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sessions.py +0 -0
  93. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_skills.py +0 -0
  94. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_bundle.py +0 -0
  95. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_command.py +0 -0
  96. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_command_safety.py +0 -0
  97. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_contract_fixtures.py +0 -0
  98. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_enrollment.py +0 -0
  99. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_fixture_corpus.py +0 -0
  100. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_hashing.py +0 -0
  101. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_installed.py +0 -0
  102. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_reconciliation_safety.py +0 -0
  103. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_scheduler_macos.py +0 -0
  104. {gumloop-0.4.2 → gumloop-0.4.4}/tests/cli/test_sync_targets.py +0 -0
  105. {gumloop-0.4.2 → gumloop-0.4.4}/tests/conftest.py +0 -0
  106. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/README.md +0 -0
  107. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/generated-bundle-transfer-limit.json +0 -0
  108. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip +0 -0
  109. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-bytes-per-file-limit.zip +0 -0
  110. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-device.zip +0 -0
  111. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip +0 -0
  112. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-fifo.zip +0 -0
  113. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-files-per-skill-limit.zip +0 -0
  114. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-nul-name.zip +0 -0
  115. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip +0 -0
  116. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip +0 -0
  117. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-socket.zip +0 -0
  118. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-symlink.zip +0 -0
  119. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-total-uncompressed-limit.zip +0 -0
  120. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/invalid-windows-drive-prefix.zip +0 -0
  121. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip +0 -0
  122. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip +0 -0
  123. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/bundles/newer-than-plan.zip +0 -0
  124. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/bundles/normal.zip +0 -0
  125. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/catalog.json +0 -0
  126. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/corpus.sha256 +0 -0
  127. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/hashes/content-v1.json +0 -0
  128. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/hashes/manifest-v1.json +0 -0
  129. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-identity.json +0 -0
  130. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-install-name.json +0 -0
  131. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json +0 -0
  132. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/newer-than-plan.json +0 -0
  133. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/manifests/normal-bundle.json +0 -0
  134. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/below-pro.json +0 -0
  135. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json +0 -0
  136. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/lost-membership.json +0 -0
  137. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json +0 -0
  138. {gumloop-0.4.2 → gumloop-0.4.4}/tests/fixtures/skill_sync/v1/responses/normal-plan.json +0 -0
  139. {gumloop-0.4.2 → gumloop-0.4.4}/tests/integration/conftest.py +0 -0
  140. {gumloop-0.4.2 → gumloop-0.4.4}/tests/integration/test_live.py +0 -0
  141. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/__init__.py +0 -0
  142. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/conftest.py +0 -0
  143. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/helpers.py +0 -0
  144. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_agents.py +0 -0
  145. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_artifacts.py +0 -0
  146. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_brain.py +0 -0
  147. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_chat.py +0 -0
  148. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_client.py +0 -0
  149. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_mcp.py +0 -0
  150. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_oauth.py +0 -0
  151. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_sessions.py +0 -0
  152. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_skills.py +0 -0
  153. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_sync.py +0 -0
  154. {gumloop-0.4.2 → gumloop-0.4.4}/tests/sdk/test_teams.py +0 -0
  155. {gumloop-0.4.2 → gumloop-0.4.4}/tests/skill_sync_fixtures.py +0 -0
  156. {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.2
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
- ## Installation
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
- pip install gumloop
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
- ## Installation
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
- pip install gumloop
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
- version = "0.4.2"
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
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.4.4"
@@ -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):
@@ -355,7 +355,6 @@ wheels = [
355
355
 
356
356
  [[package]]
357
357
  name = "gumloop"
358
- version = "0.4.2"
359
358
  source = { editable = "." }
360
359
  dependencies = [
361
360
  { name = "click" },
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