Skip to content

Commit 49fac50

Browse files
authored
Merge pull request #6 from ComposioHQ/kj/plugin-upgrade-nudge
feat(hooks): nudge CLI upgrade when a newer stable release is cached
2 parents 9d2590c + df428bb commit 49fac50

3 files changed

Lines changed: 80 additions & 3 deletions

File tree

plugins/composio/.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "composio",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "Your agent decides what to do — Composio handles the rest. Use just-in-time tool calls across 1,000+ apps with managed auth through the hosted app or local CLI.",
55
"author": {
66
"name": "Composio",

plugins/composio/hooks/session-start.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ else
3232
'"authenticated"[[:space:]]*:[[:space:]]*false|not[[:space:]-]+logged[[:space:]-]+in'; then
3333
cli_status="The local Composio CLI is available and signed in."
3434
fi
35+
36+
# Upgrade nudge from the CLI's local release cache — never the network.
37+
# Stable X.Y.Z versions only; anything missing or malformed stays silent.
38+
update_cache="${HOME:-}/.composio/update-check.json"
39+
if [ -f "$update_cache" ]; then
40+
latest="$(sed -n 's/.*"latestVersion"[[:space:]]*:[[:space:]]*"\([0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\)".*/\1/p' "$update_cache" 2>/dev/null | head -n 1)"
41+
installed="$(composio version 2>/dev/null | tail -n 1 | tr -d '[:space:]')"
42+
if [ -n "$latest" ] && [ "$installed" != "$latest" ] \
43+
&& printf '%s' "$installed" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' \
44+
&& [ "$(printf '%s\n%s\n' "$installed" "$latest" \
45+
| sort -t. -k1,1n -k2,2n -k3,3n | head -n 1)" = "$installed" ]; then
46+
cli_status="${cli_status} A newer Composio CLI (${latest}) is available — run \`composio upgrade\`."
47+
fi
48+
fi
3549
fi
3650

3751
[ -n "$cache_pid" ] && wait "$cache_pid" 2>/dev/null

tests/test_package.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ def test_readme_documents_the_combined_local_flow(self):
169169

170170
class HookBehaviorTests(unittest.TestCase):
171171
def run_hook(self, script, payload, tmpdir, path=None):
172-
env = dict(os.environ, TMPDIR=tmpdir)
172+
# HOME is pinned so tests never read the real ~/.composio.
173+
env = dict(os.environ, TMPDIR=tmpdir, HOME=tmpdir)
173174
if path is not None:
174175
env["PATH"] = path
175176
return subprocess.run(
@@ -182,11 +183,20 @@ def run_hook(self, script, payload, tmpdir, path=None):
182183
check=False,
183184
)
184185

185-
def fake_composio(self, tmpdir, stdout="", exit_code=0, toolkits=None):
186+
def fake_composio(self, tmpdir, stdout="", exit_code=0, toolkits=None, version=None):
186187
bindir = pathlib.Path(tmpdir) / "bin"
187188
bindir.mkdir(exist_ok=True)
188189
script = bindir / "composio"
189190
lines = ["#!/usr/bin/env bash"]
191+
if version is not None:
192+
lines.extend(
193+
[
194+
'if [ "$1" = "version" ]; then',
195+
f" printf '%s\\n' {shlex.quote(version)}",
196+
" exit 0",
197+
"fi",
198+
]
199+
)
190200
if toolkits is not None:
191201
lines.extend(
192202
[
@@ -246,6 +256,59 @@ def test_session_start_offers_cli_install_in_terminal_context(self):
246256
self.assertNotIn("composio execute", context)
247257
self.assertNotIn("composio search", context)
248258

259+
NUDGE = "newer Composio CLI"
260+
261+
def write_update_cache(self, tmpdir, content):
262+
cache_dir = pathlib.Path(tmpdir) / ".composio"
263+
cache_dir.mkdir(exist_ok=True)
264+
(cache_dir / "update-check.json").write_text(content, encoding="utf-8")
265+
266+
def session_context(self, tmpdir, path):
267+
result = self.run_hook(
268+
"session-start.sh", {"hook_event_name": "SessionStart"}, tmpdir, path
269+
)
270+
self.assertEqual(0, result.returncode, result.stderr)
271+
return json.loads(result.stdout)["hookSpecificOutput"]["additionalContext"]
272+
273+
def test_session_start_nudges_upgrade_for_older_stable_cli(self):
274+
with tempfile.TemporaryDirectory() as tmpdir:
275+
self.write_update_cache(
276+
tmpdir,
277+
json.dumps(
278+
{"lastChecked": "2026-07-24T00:00:00.000Z", "latestVersion": "0.2.32"}
279+
),
280+
)
281+
path = self.fake_composio(
282+
tmpdir, stdout='{"authenticated": true}', version="0.2.30"
283+
)
284+
context = self.session_context(tmpdir, path)
285+
self.assertIn("local Composio CLI is available and signed in", context)
286+
self.assertIn("A newer Composio CLI (0.2.32) is available", context)
287+
self.assertIn("composio upgrade", context)
288+
289+
def test_session_start_upgrade_nudge_degrades_to_silence(self):
290+
cache = json.dumps(
291+
{"lastChecked": "2026-07-24T00:00:00.000Z", "latestVersion": "0.2.32"}
292+
)
293+
silent_cases = [
294+
("beta-installed", cache, "0.2.32-beta.289"),
295+
("up-to-date", cache, "0.2.32"),
296+
("installed-newer", cache, "0.3.0"),
297+
("prerelease-latest", cache.replace("0.2.32", "0.3.0-beta.1"), "0.2.31"),
298+
("malformed-cache", "not json at all {{{", "0.2.30"),
299+
("cache-missing", None, "0.2.30"),
300+
]
301+
for name, cache_content, installed in silent_cases:
302+
with self.subTest(name):
303+
with tempfile.TemporaryDirectory() as tmpdir:
304+
if cache_content is not None:
305+
self.write_update_cache(tmpdir, cache_content)
306+
path = self.fake_composio(
307+
tmpdir, stdout='{"authenticated": true}', version=installed
308+
)
309+
context = self.session_context(tmpdir, path)
310+
self.assertNotIn(self.NUDGE, context)
311+
249312
def test_prompt_hook_is_surface_aware_for_cached_toolkits(self):
250313
with tempfile.TemporaryDirectory() as tmpdir:
251314
cache = pathlib.Path(tmpdir) / "composio-plugin-toolkits.cache"

0 commit comments

Comments
 (0)