cinna-cli 0.2.4__tar.gz → 0.2.5__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 (86) hide show
  1. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/PKG-INFO +1 -1
  2. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/pyproject.toml +1 -1
  3. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/chat.py +51 -1
  4. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_chat.py +106 -0
  5. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/uv.lock +1 -1
  6. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/.claude/commands/cinna-cli.feature.doc.md +0 -0
  7. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/.github/workflows/publish.yml +0 -0
  8. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/.gitignore +0 -0
  9. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/LICENSE.md +0 -0
  10. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/README.md +0 -0
  11. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/README.md +0 -0
  12. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/account_workspace/account_workspace.md +0 -0
  13. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/account_workspace/account_workspace_acceptance.md +0 -0
  14. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/account_workspace/account_workspace_tech.md +0 -0
  15. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_api/agent_api.md +0 -0
  16. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_api/agent_api_acceptance.md +0 -0
  17. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_api/agent_api_tech.md +0 -0
  18. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_management/agent_management.md +0 -0
  19. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_management/agent_management_acceptance.md +0 -0
  20. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_management/agent_management_tech.md +0 -0
  21. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_schedules/agent_schedules.md +0 -0
  22. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_schedules/agent_schedules_acceptance.md +0 -0
  23. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/agent_schedules/agent_schedules_tech.md +0 -0
  24. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/bootstrap_onboarding/bootstrap_onboarding.md +0 -0
  25. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/bootstrap_onboarding/bootstrap_onboarding_acceptance.md +0 -0
  26. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/bootstrap_onboarding/bootstrap_onboarding_tech.md +0 -0
  27. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/doctor/doctor.md +0 -0
  28. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/doctor/doctor_acceptance.md +0 -0
  29. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/doctor/doctor_tech.md +0 -0
  30. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/git_versioning/git_versioning.md +0 -0
  31. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/git_versioning/git_versioning_acceptance.md +0 -0
  32. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/git_versioning/git_versioning_tech.md +0 -0
  33. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/live_sync/live_sync.md +0 -0
  34. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/live_sync/live_sync_acceptance.md +0 -0
  35. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/live_sync/live_sync_tech.md +0 -0
  36. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/mcp_integration/mcp_integration.md +0 -0
  37. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/mcp_integration/mcp_integration_acceptance.md +0 -0
  38. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/mcp_integration/mcp_integration_tech.md +0 -0
  39. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/remote_chat/remote_chat.md +0 -0
  40. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/remote_chat/remote_chat_acceptance.md +0 -0
  41. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/remote_chat/remote_chat_tech.md +0 -0
  42. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/remote_exec/remote_exec.md +0 -0
  43. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/remote_exec/remote_exec_acceptance.md +0 -0
  44. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/features/remote_exec/remote_exec_tech.md +0 -0
  45. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/interface.md +0 -0
  46. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/docs/mutagen_capabilities.md +0 -0
  47. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/scripts/check_docs_references.py +0 -0
  48. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/__init__.py +0 -0
  49. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/account.py +0 -0
  50. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/auth.py +0 -0
  51. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/bootstrap.py +0 -0
  52. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/client.py +0 -0
  53. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/config.py +0 -0
  54. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/console.py +0 -0
  55. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/context.py +0 -0
  56. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/doctor.py +0 -0
  57. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/errors.py +0 -0
  58. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/git_versioning.py +0 -0
  59. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/logging.py +0 -0
  60. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/main.py +0 -0
  61. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/mcp_proxy.py +0 -0
  62. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/mutagen_runtime.py +0 -0
  63. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/sync.py +0 -0
  64. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/sync_session.py +0 -0
  65. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/sync_ssh_shim.py +0 -0
  66. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/sync_tui.py +0 -0
  67. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/templates/ACCOUNT_CLAUDE.md.template +0 -0
  68. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/templates/CHAT_TESTING.md +0 -0
  69. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/templates/CLAUDE.md.template +0 -0
  70. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/templates/GIT_VERSIONING.md +0 -0
  71. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/src/cinna/templates/__init__.py +0 -0
  72. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/__init__.py +0 -0
  73. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/conftest.py +0 -0
  74. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_account.py +0 -0
  75. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_auth.py +0 -0
  76. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_bootstrap.py +0 -0
  77. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_client.py +0 -0
  78. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_config.py +0 -0
  79. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_context.py +0 -0
  80. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_doctor.py +0 -0
  81. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_git_versioning.py +0 -0
  82. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_main.py +0 -0
  83. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_mutagen_runtime.py +0 -0
  84. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_sync.py +0 -0
  85. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_sync_session.py +0 -0
  86. {cinna_cli-0.2.4 → cinna_cli-0.2.5}/tests/test_sync_ssh_shim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cinna-cli
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Local development CLI for Cinna Core agents
5
5
  Project-URL: Homepage, https://github.com/opencinna/cinna-cli
6
6
  Project-URL: Repository, https://github.com/opencinna/cinna-cli
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cinna-cli"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "Local development CLI for Cinna Core agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -104,6 +104,24 @@ class _Emitter:
104
104
  console.console.print(f" [dim]📎 {att.get('filename')} → {loc}[/dim]")
105
105
  elif kind == "status":
106
106
  console.console.print(f"[dim]· {event.get('state')}…[/dim]")
107
+ elif kind == "delta":
108
+ for ev in event.get("events", []):
109
+ etype = ev.get("type")
110
+ if etype == "assistant":
111
+ if ev.get("content"):
112
+ console.console.print(ev["content"], end="")
113
+ elif etype == "thinking":
114
+ console.console.print(
115
+ f" [magenta]🧠 thinking:[/magenta] {ev.get('content', '')}"
116
+ )
117
+ elif etype in ("tool", "tool_use"):
118
+ payload = ev.get("tool_input")
119
+ console.console.print(
120
+ f" [cyan]🔧 {ev.get('tool_name', 'tool')}[/cyan] "
121
+ f"[dim]{json.dumps(payload, ensure_ascii=False) if payload is not None else ''}[/dim]"
122
+ )
123
+ elif ev.get("content"):
124
+ console.console.print(f" [dim]{etype}: {ev['content']}[/dim]")
107
125
  elif kind == "done":
108
126
  console.console.print(
109
127
  f"[dim]done — result: {event.get('result_state')}[/dim]"
@@ -274,6 +292,12 @@ def _poll_turn(
274
292
  start_deadline = started + START_GRACE_SECONDS
275
293
  turn_started = not expect_turn
276
294
  flagged_in_progress: set[str] = set()
295
+ # How much of an in-progress message's trace we've already surfaced —
296
+ # the row is periodically re-flushed server-side with its *full*
297
+ # accumulated ``streaming_events`` so far, so this is what lets us emit
298
+ # only what's new since the last poll instead of going silent until the
299
+ # whole (possibly many-minutes-long) message finalizes.
300
+ shown_event_count: dict[str, int] = {}
277
301
 
278
302
  while True:
279
303
  # Drain newly-finalized messages; stop at the first in-progress one
@@ -285,16 +309,19 @@ def _poll_turn(
285
309
  advanced = False
286
310
  for m in batch:
287
311
  meta = m.get("message_metadata") or {}
312
+ mid = m.get("id")
288
313
  if meta.get("streaming_in_progress"):
289
314
  in_progress = True
290
- mid = m.get("id")
291
315
  if mid not in flagged_in_progress:
292
316
  flagged_in_progress.add(mid)
293
317
  emit.emit(
294
318
  {"event": "status", "state": "working", "message_id": mid}
295
319
  )
320
+ if include_events:
321
+ _emit_delta(emit, m, shown_event_count)
296
322
  break
297
323
  _emit_message(client, emit, m, dl_dir, include_events)
324
+ shown_event_count.pop(mid, None)
298
325
  consumed += 1
299
326
  advanced = True
300
327
  if m.get("role") and m["role"] != "user":
@@ -328,6 +355,29 @@ def _poll_turn(
328
355
  _emit_done(client, emit, session_id)
329
356
 
330
357
 
358
+ def _emit_delta(
359
+ emit: _Emitter,
360
+ m: dict,
361
+ shown_event_count: dict[str, int],
362
+ ) -> None:
363
+ """Emit trace events newly appended to a still-in-progress message.
364
+
365
+ ``streaming_events`` (thinking blocks, tool calls, tool results, and
366
+ individual assistant-text chunks) accumulates on the row as the turn
367
+ progresses and is what the server periodically flushes; this diffs
368
+ against what's already been shown for this message id so a long-running
369
+ turn surfaces gradual output instead of nothing until it finalizes.
370
+ """
371
+ mid = m.get("id")
372
+ events = _extract_events(m)
373
+ prev_count = shown_event_count.get(mid, 0)
374
+ new_events = events[prev_count:]
375
+ if not new_events:
376
+ return
377
+ emit.emit({"event": "delta", "message_id": mid, "role": m.get("role"), "events": new_events})
378
+ shown_event_count[mid] = len(events)
379
+
380
+
331
381
  def _emit_message(
332
382
  client: AccountClient,
333
383
  emit: _Emitter,
@@ -165,6 +165,81 @@ class FakeClient:
165
165
  return {}
166
166
 
167
167
 
168
+ class FakeStreamingClient(FakeClient):
169
+ """Scripts a turn whose agent message streams in two chunks before it
170
+ finalizes — models a long-running turn so progressive delta emission can
171
+ be exercised (see test_chat_streams_progressive_deltas_while_in_progress)."""
172
+
173
+ def __init__(self):
174
+ super().__init__()
175
+ self.poll_count = 0
176
+
177
+ def _user_msg(self):
178
+ return {
179
+ "id": "m1",
180
+ "role": "user",
181
+ "sequence_number": 1,
182
+ "timestamp": "2026-06-22T00:00:00Z",
183
+ "content": self.last_content,
184
+ "message_metadata": {},
185
+ "files": [],
186
+ }
187
+
188
+ def _agent_msg(self):
189
+ thinking = {"type": "thinking", "event_seq": 1, "content": "Step one."}
190
+ tool = {
191
+ "type": "tool",
192
+ "event_seq": 2,
193
+ "tool_name": "calculator",
194
+ "content": "calculator",
195
+ "metadata": {"tool_id": "tool-1", "tool_input": {"expression": "2+2"}},
196
+ }
197
+ assistant = {"type": "assistant", "event_seq": 3, "content": "Here you go"}
198
+ base = {
199
+ "id": "m2",
200
+ "role": "agent",
201
+ "sequence_number": 2,
202
+ "timestamp": "2026-06-22T00:00:01Z",
203
+ "files": [],
204
+ }
205
+ if self.poll_count == 1:
206
+ return {
207
+ **base,
208
+ "content": "Agent is responding...",
209
+ "message_metadata": {
210
+ "streaming_in_progress": True,
211
+ "streaming_events": [thinking],
212
+ },
213
+ }
214
+ if self.poll_count == 2:
215
+ return {
216
+ **base,
217
+ "content": "Agent is responding...",
218
+ "message_metadata": {
219
+ "streaming_in_progress": True,
220
+ "streaming_events": [thinking, tool],
221
+ },
222
+ }
223
+ return {
224
+ **base,
225
+ "content": "Here you go",
226
+ "message_metadata": {"streaming_events": [thinking, tool, assistant]},
227
+ }
228
+
229
+ def get_messages(self, session_id, limit=100, offset=0):
230
+ if not self.sent:
231
+ return {"data": [], "count": 0}
232
+ self.poll_count += 1
233
+ if offset == 0:
234
+ return {"data": [self._user_msg(), self._agent_msg()], "count": 2}
235
+ if offset == 1:
236
+ return {"data": [self._agent_msg()], "count": 1}
237
+ return {"data": [], "count": 0}
238
+
239
+ def get_streaming_status(self, session_id):
240
+ return {"is_streaming": self.poll_count < 3}
241
+
242
+
168
243
  def _ndjson(output: str) -> list[dict]:
169
244
  return [json.loads(line) for line in output.splitlines() if line.strip()]
170
245
 
@@ -247,6 +322,37 @@ def test_chat_no_events_flag_omits_trace(runner, account_root, monkeypatch):
247
322
  assert agent_msg["content"] == "Here you go"
248
323
 
249
324
 
325
+ def test_chat_streams_progressive_deltas_while_in_progress(
326
+ runner, account_root, monkeypatch
327
+ ):
328
+ """A long-running turn (agent message stays `streaming_in_progress` across
329
+ several polls) surfaces its trace incrementally via `delta` events instead
330
+ of going silent until the whole message finalizes."""
331
+ monkeypatch.chdir(account_root)
332
+ fake = FakeStreamingClient()
333
+ monkeypatch.setattr("cinna.chat.AccountClient", lambda cfg: fake)
334
+
335
+ result = runner.invoke(
336
+ cli, ["chat", "--agent", "CRM Agent", "--interval", "0", "2+2"]
337
+ )
338
+ assert result.exit_code == 0, result.output
339
+
340
+ events = _ndjson(result.output)
341
+ deltas = [e for e in events if e["event"] == "delta"]
342
+ assert len(deltas) == 2
343
+ assert [ev["type"] for ev in deltas[0]["events"]] == ["thinking"]
344
+ # Second delta carries only the newly-appended event, not a repeat of
345
+ # what was already shown.
346
+ assert [ev["type"] for ev in deltas[1]["events"]] == ["tool"]
347
+
348
+ agent_msg = [
349
+ e
350
+ for e in events
351
+ if e["event"] == "message" and e["role"] == "agent"
352
+ ][0]
353
+ assert agent_msg["content"] == "Here you go"
354
+
355
+
250
356
  def test_chat_downloads_agent_attachment(runner, account_root, monkeypatch):
251
357
  monkeypatch.chdir(account_root)
252
358
  fake = FakeClient()
@@ -136,7 +136,7 @@ wheels = [
136
136
 
137
137
  [[package]]
138
138
  name = "cinna-cli"
139
- version = "0.2.4"
139
+ version = "0.2.5"
140
140
  source = { editable = "." }
141
141
  dependencies = [
142
142
  { 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