scootcli 0.14.1__tar.gz → 0.14.2__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 (119) hide show
  1. {scootcli-0.14.1/src/scootcli.egg-info → scootcli-0.14.2}/PKG-INFO +1 -1
  2. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/__init__.py +1 -1
  3. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/providers/base.py +35 -0
  4. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/providers/openai_chat.py +8 -2
  5. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/providers/openai_responses.py +4 -3
  6. {scootcli-0.14.1 → scootcli-0.14.2/src/scootcli.egg-info}/PKG-INFO +1 -1
  7. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_providers.py +49 -0
  8. {scootcli-0.14.1 → scootcli-0.14.2}/LICENSE +0 -0
  9. {scootcli-0.14.1 → scootcli-0.14.2}/README.md +0 -0
  10. {scootcli-0.14.1 → scootcli-0.14.2}/pyproject.toml +0 -0
  11. {scootcli-0.14.1 → scootcli-0.14.2}/setup.cfg +0 -0
  12. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/__main__.py +0 -0
  13. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/activity.py +0 -0
  14. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/agent.py +0 -0
  15. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/approvals.py +0 -0
  16. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/auth.py +0 -0
  17. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/cli.py +0 -0
  18. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/clipboard.py +0 -0
  19. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/__init__.py +0 -0
  20. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/approve.py +0 -0
  21. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/auth.py +0 -0
  22. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/base.py +0 -0
  23. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/compact.py +0 -0
  24. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/copy.py +0 -0
  25. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/exit.py +0 -0
  26. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/forget.py +0 -0
  27. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/help.py +0 -0
  28. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/hooks.py +0 -0
  29. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/init.py +0 -0
  30. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/logo.py +0 -0
  31. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/model.py +0 -0
  32. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/panel.py +0 -0
  33. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/reset.py +0 -0
  34. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/resume.py +0 -0
  35. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/route.py +0 -0
  36. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/save.py +0 -0
  37. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/scope.py +0 -0
  38. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/sessions.py +0 -0
  39. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/status.py +0 -0
  40. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/verbosity.py +0 -0
  41. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/worktree.py +0 -0
  42. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/commands/yolo.py +0 -0
  43. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/config.py +0 -0
  44. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/context.py +0 -0
  45. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/credentials.py +0 -0
  46. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/errors.py +0 -0
  47. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/headless.py +0 -0
  48. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/hooks.py +0 -0
  49. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/images.py +0 -0
  50. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/keys.py +0 -0
  51. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/lineeditor.py +0 -0
  52. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/logo.py +0 -0
  53. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/models.py +0 -0
  54. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/panel.py +0 -0
  55. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/preferences.py +0 -0
  56. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/presets.py +0 -0
  57. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/pricing.py +0 -0
  58. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/project.py +0 -0
  59. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/prompts.py +0 -0
  60. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/providers/__init__.py +0 -0
  61. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/providers/anthropic.py +0 -0
  62. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/providers/registry.py +0 -0
  63. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/providers/router.py +0 -0
  64. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/rendering.py +0 -0
  65. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/repl.py +0 -0
  66. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/resize.py +0 -0
  67. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/sessions.py +0 -0
  68. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/status.py +0 -0
  69. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/store.py +0 -0
  70. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/__init__.py +0 -0
  71. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/base.py +0 -0
  72. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/edit_file.py +0 -0
  73. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/list_dir.py +0 -0
  74. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/open_editor.py +0 -0
  75. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/read_file.py +0 -0
  76. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/run_shell.py +0 -0
  77. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/search.py +0 -0
  78. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/update_plan.py +0 -0
  79. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/tools/write_file.py +0 -0
  80. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/transport.py +0 -0
  81. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/updates.py +0 -0
  82. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/vision.py +0 -0
  83. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/workspace.py +0 -0
  84. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli/worktree.py +0 -0
  85. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli.egg-info/SOURCES.txt +0 -0
  86. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli.egg-info/dependency_links.txt +0 -0
  87. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli.egg-info/entry_points.txt +0 -0
  88. {scootcli-0.14.1 → scootcli-0.14.2}/src/scootcli.egg-info/top_level.txt +0 -0
  89. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_agent.py +0 -0
  90. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_anthropic.py +0 -0
  91. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_approvals.py +0 -0
  92. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_auth.py +0 -0
  93. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_clipboard.py +0 -0
  94. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_config.py +0 -0
  95. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_headless.py +0 -0
  96. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_hooks.py +0 -0
  97. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_images.py +0 -0
  98. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_lineeditor.py +0 -0
  99. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_logo.py +0 -0
  100. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_models.py +0 -0
  101. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_panel.py +0 -0
  102. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_preferences.py +0 -0
  103. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_readiness.py +0 -0
  104. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_rendering.py +0 -0
  105. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_repl.py +0 -0
  106. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_resilience.py +0 -0
  107. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_resize.py +0 -0
  108. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_router.py +0 -0
  109. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_scope.py +0 -0
  110. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_sessions.py +0 -0
  111. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_small_060.py +0 -0
  112. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_smoke.py +0 -0
  113. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_store.py +0 -0
  114. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_streaming.py +0 -0
  115. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_tools.py +0 -0
  116. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_transport_native.py +0 -0
  117. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_vision.py +0 -0
  118. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_workspace.py +0 -0
  119. {scootcli-0.14.1 → scootcli-0.14.2}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scootcli
3
- Version: 0.14.1
3
+ Version: 0.14.2
4
4
  Summary: A tiny coding agent that goes where you point it
5
5
  Author: Sergey Nes
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
1
  """scoot: a tiny coding agent that goes where you point it."""
2
2
 
3
- __version__ = "0.14.1"
3
+ __version__ = "0.14.2"
4
4
 
@@ -363,6 +363,41 @@ class BaseProvider:
363
363
  time.sleep(0.05)
364
364
 
365
365
 
366
+ def valid_tool_arguments(raw) -> str:
367
+ """A tool call's ``arguments`` as a JSON *object* string, safe to replay to a provider that
368
+ validates them. A missing, empty, or malformed value (a stream truncated mid-JSON on a length turn)
369
+ becomes ``"{}"``, so one bad tool call cannot poison the session on every later request. The call
370
+ is never dropped, so it still pairs with its matching ``tool`` result and history stays valid.
371
+ """
372
+ if isinstance(raw, dict):
373
+ try:
374
+ return json.dumps(raw)
375
+ except (TypeError, ValueError):
376
+ return "{}"
377
+ if not raw or not isinstance(raw, str):
378
+ return "{}"
379
+ try:
380
+ parsed = json.loads(raw)
381
+ except (json.JSONDecodeError, ValueError):
382
+ return "{}"
383
+ return raw if isinstance(parsed, dict) else "{}"
384
+
385
+
386
+ def sanitized_tool_calls(calls) -> list:
387
+ """Copy ``calls`` with each ``function.arguments`` replaced by a valid JSON-object string."""
388
+ out = []
389
+ for c in calls or []:
390
+ if not isinstance(c, dict):
391
+ out.append(c)
392
+ continue
393
+ c = dict(c)
394
+ fn = dict(c.get("function") or {})
395
+ fn["arguments"] = valid_tool_arguments(fn.get("arguments"))
396
+ c["function"] = fn
397
+ out.append(c)
398
+ return out
399
+
400
+
366
401
  def iter_sse_json(lines: Iterator[str], status_holder: dict, leftovers: List[str]) -> Iterator[dict]:
367
402
  """Walk raw SSE lines: yield each ``data:`` JSON object, record the status sentinel, collect
368
403
  anything that is not SSE (an error body) into ``leftovers``."""
@@ -18,17 +18,23 @@ from .base import (
18
18
  finish_stream,
19
19
  iter_sse_json,
20
20
  normalize_usage,
21
+ sanitized_tool_calls,
21
22
  )
22
23
 
23
24
  _PRIVATE_KEYS = ("provider_items",)
24
25
 
25
26
 
26
27
  def strip_private(messages: List[dict]) -> List[dict]:
27
- """Drop scoot-private keys the API would reject."""
28
+ """Prepare messages for the wire: drop scoot-private keys the API would reject, and rewrite any
29
+ tool-call arguments that are not valid JSON objects (a truncated length-turn leaves a malformed
30
+ string that a validating provider would reject on every later request). Copies, never mutates."""
28
31
  out = []
29
32
  for m in messages:
30
- if any(k in m for k in _PRIVATE_KEYS):
33
+ calls = m.get("tool_calls")
34
+ if any(k in m for k in _PRIVATE_KEYS) or calls:
31
35
  m = {k: v for k, v in m.items() if k not in _PRIVATE_KEYS}
36
+ if calls:
37
+ m["tool_calls"] = sanitized_tool_calls(calls)
32
38
  out.append(m)
33
39
  return out
34
40
 
@@ -31,6 +31,7 @@ from .base import (
31
31
  iter_sse_json,
32
32
  normalize_usage,
33
33
  raise_for_status,
34
+ valid_tool_arguments,
34
35
  )
35
36
 
36
37
  # Models that accept the ``reasoning`` parameter (others reject it with a 400).
@@ -68,9 +69,9 @@ def _content_parts(content, role: str) -> object:
68
69
 
69
70
 
70
71
  def _arguments(raw) -> str:
71
- if isinstance(raw, str):
72
- return raw
73
- return json.dumps(raw or {})
72
+ # A valid JSON-object string, so a truncated/malformed tool call replayed to the Responses API
73
+ # cannot get the session stuck (mirrors the Chat wire and Anthropic's dict fallback).
74
+ return valid_tool_arguments(raw)
74
75
 
75
76
 
76
77
  def translate_messages(messages: List[dict], qualified_model: str) -> Tuple[str, List[dict]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scootcli
3
- Version: 0.14.1
3
+ Version: 0.14.2
4
4
  Summary: A tiny coding agent that goes where you point it
5
5
  Author: Sergey Nes
6
6
  License-Expression: MIT
@@ -256,3 +256,52 @@ def test_cli_models_caps_per_provider_unless_filtered(capsys):
256
256
  _cmd_models(_Pool(), as_json=False, provider="openai")
257
257
  out = capsys.readouterr().out
258
258
  assert "more ·" not in out # a single --provider shows all
259
+
260
+
261
+ # ── outbound tool-argument sanitizing: a malformed/truncated call cannot stick a session ──
262
+ def test_valid_tool_arguments_normalizes_to_a_json_object():
263
+ import json as _json
264
+ from scootcli.providers.base import valid_tool_arguments
265
+
266
+ assert valid_tool_arguments('{"path": "a.txt"}') == '{"path": "a.txt"}' # valid: unchanged
267
+ assert valid_tool_arguments("") == "{}" # empty
268
+ assert valid_tool_arguments(None) == "{}"
269
+ assert valid_tool_arguments('{"path": "a') == "{}" # truncated mid-JSON
270
+ assert valid_tool_arguments("[1, 2]") == "{}" # JSON but not an object
271
+ assert _json.loads(valid_tool_arguments({"x": 1})) == {"x": 1} # dict -> json string
272
+
273
+
274
+ def test_chat_wire_sanitizes_tool_args_and_keeps_pairing():
275
+ import json as _json
276
+ from scootcli.providers.openai_chat import strip_private
277
+
278
+ def call(cid, args):
279
+ return {"id": cid, "type": "function", "function": {"name": "write_file", "arguments": args}}
280
+
281
+ messages = [
282
+ {"role": "user", "content": "go"},
283
+ {"role": "assistant", "content": "", "tool_calls": [
284
+ call("c1", '{"ok": true}'), call("c2", ""), call("c3", '{"path": "x')]},
285
+ {"role": "tool", "tool_call_id": "c1", "content": "ok"},
286
+ {"role": "tool", "tool_call_id": "c2", "content": "ok"},
287
+ {"role": "tool", "tool_call_id": "c3", "content": "ok"},
288
+ ]
289
+ out = strip_private(messages)
290
+ calls = out[1]["tool_calls"]
291
+ assert len(calls) == 3 # no call dropped
292
+ assert [c["id"] for c in calls] == ["c1", "c2", "c3"]
293
+ for c in calls:
294
+ _json.loads(c["function"]["arguments"]) # every one is now valid JSON
295
+ assert [m.get("tool_call_id") for m in out if m["role"] == "tool"] == ["c1", "c2", "c3"] # pairing kept
296
+ assert messages[1]["tool_calls"][1]["function"]["arguments"] == "" # original history untouched (copied)
297
+
298
+
299
+ def test_responses_wire_sanitizes_reconstructed_tool_args():
300
+ import json as _json
301
+ from scootcli.providers.openai_responses import translate_messages
302
+
303
+ messages = [{"role": "assistant", "content": "", "tool_calls": [
304
+ {"id": "c1", "type": "function", "function": {"name": "read_file", "arguments": '{"path": "a'}}]}]
305
+ _instr, items = translate_messages(messages, "openai/gpt-5.3-codex")
306
+ fc = [it for it in items if it.get("type") == "function_call"]
307
+ assert len(fc) == 1 and _json.loads(fc[0]["arguments"]) == {} # truncated args replayed as {}
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