kiwi-code 0.0.28__tar.gz → 0.0.30__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 (53) hide show
  1. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/client.py +45 -41
  4. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/main.py +39 -26
  5. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/main.py +23 -16
  6. kiwi_code-0.0.30/src/kiwi_tui/random_words.py +9 -0
  7. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/dashboard.py +15 -0
  8. kiwi_code-0.0.30/src/kiwi_tui/status_words.py +481 -0
  9. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/uv.lock +1 -1
  10. kiwi_code-0.0.28/src/kiwi_tui/random_words.py +0 -80
  11. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.github/workflows/publish.yml +0 -0
  12. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.github/workflows/test.yml +0 -0
  13. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.gitignore +0 -0
  14. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.python-version +0 -0
  15. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/CLAUDE.md +0 -0
  16. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/Makefile +0 -0
  17. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/README.md +0 -0
  18. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/__init__.py +0 -0
  19. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/auth.py +0 -0
  20. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/cli.py +0 -0
  21. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/commands.py +0 -0
  22. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/logger.py +0 -0
  23. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/models.py +0 -0
  24. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/runtime_manager.py +0 -0
  25. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/server.py +0 -0
  26. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/__init__.py +0 -0
  27. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/__main__.py +0 -0
  28. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/snake_game/.gitignore +0 -0
  29. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
  30. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/__init__.py +0 -0
  31. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/inline_file_picker.py +0 -0
  32. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/runtime_agent.py +0 -0
  33. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/__init__.py +0 -0
  34. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/attach_content.py +0 -0
  35. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/command_result.py +0 -0
  36. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/file_browser.py +0 -0
  37. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/help.py +0 -0
  38. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/id_picker.py +0 -0
  39. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/login.py +0 -0
  40. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  41. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  42. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/slash_picker.py +0 -0
  43. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/slash_commands.py +0 -0
  44. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/widgets.py +0 -0
  45. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/test_hello.py +0 -0
  46. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/__init__.py +0 -0
  47. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/conftest.py +0 -0
  48. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_cli_help.py +0 -0
  49. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_imports.py +0 -0
  50. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_reexec_kiwi.py +0 -0
  51. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_runtime_log_trimming.py +0 -0
  52. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_tokens.py +0 -0
  53. {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_tui_headless.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.28
3
+ Version: 0.0.30
4
4
  Summary: A textual-based terminal user interface application
5
5
  Project-URL: Homepage, https://meetkiwi.ai
6
6
  Project-URL: Repository, https://github.com/jetoslabs/kiwi-code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.28"
3
+ version = "0.0.30"
4
4
  description = "A textual-based terminal user interface application"
5
5
  readme = {file = "README.md", content-type = "text/markdown"}
6
6
  requires-python = ">=3.11,<4.0"
@@ -177,52 +177,56 @@ class AutobotsClientWrapper:
177
177
  try:
178
178
  logger.info("Attempting to refresh access token")
179
179
 
180
- # Need authenticated client for refresh
181
- if not isinstance(self.client, AuthenticatedClient):
182
- temp_client = AuthenticatedClient(
183
- base_url=self.base_url,
184
- token=refresh_token,
185
- raise_on_unexpected_status=False,
186
- )
187
- else:
188
- temp_client = self.client
189
-
190
- response = refresh_password_email_v1_auth_session_refresh_post.sync_detailed(
191
- client=temp_client,
192
- refresh_token=refresh_token,
193
- )
194
-
195
- if response.status_code == 200 and response.parsed:
196
- auth_response = response.parsed
180
+ # NOTE: Don't use the OpenAPI AuthenticatedClient for refresh.
181
+ # It always injects an Authorization header, and depending on backend
182
+ # behavior, an invalid/expired bearer token can cause refresh to fail
183
+ # even if `refresh_token` is provided as a query param.
184
+ url = f"{self.base_url.rstrip('/')}/v1/auth/session/refresh"
185
+ with httpx.Client(timeout=30) as client:
186
+ resp = client.post(url, params={"refresh_token": refresh_token})
187
+
188
+ if resp.status_code != 200:
189
+ detail = (resp.text or "").strip()
190
+ if len(detail) > 200:
191
+ detail = detail[:200] + "..."
192
+ error_msg = f"Token refresh failed with status {resp.status_code}"
193
+ if detail:
194
+ error_msg += f": {detail}"
195
+ logger.error(error_msg)
196
+ return False, None, error_msg
197
197
 
198
- if auth_response.session:
199
- session = auth_response.session
198
+ body = resp.json() if resp.content else {}
199
+ session = (body.get("session") or {}) if isinstance(body, dict) else {}
200
+ access = session.get("access_token") or (body.get("access_token") if isinstance(body, dict) else None)
201
+ new_refresh = session.get("refresh_token") or refresh_token
202
+ token_type = session.get("token_type") or "Bearer"
203
+ expires_in = session.get("expires_in")
200
204
 
201
- # Calculate expiry time
202
- expires_at = None
203
- if hasattr(session, 'expires_in') and session.expires_in:
204
- expires_at = datetime.now() + timedelta(seconds=session.expires_in)
205
+ if not access:
206
+ error_msg = f"Token refresh response missing access_token: {body}"
207
+ logger.error(error_msg)
208
+ return False, None, error_msg
205
209
 
206
- tokens = AuthTokens(
207
- access_token=session.access_token,
208
- refresh_token=session.refresh_token,
209
- token_type=session.token_type,
210
- expires_at=expires_at,
211
- )
210
+ # Calculate expiry time when provided by the backend.
211
+ expires_at = None
212
+ try:
213
+ if isinstance(expires_in, (int, float)) and expires_in:
214
+ expires_at = datetime.now() + timedelta(seconds=int(expires_in))
215
+ except Exception:
216
+ expires_at = None
217
+
218
+ tokens = AuthTokens(
219
+ access_token=str(access),
220
+ refresh_token=str(new_refresh),
221
+ token_type=str(token_type),
222
+ expires_at=expires_at,
223
+ )
212
224
 
213
- # Update client with new token
214
- self.update_token(tokens.access_token)
225
+ # Update client with new token
226
+ self.update_token(tokens.access_token)
215
227
 
216
- logger.info("Token refresh successful")
217
- return True, tokens, "Token refreshed successfully"
218
- else:
219
- error_msg = "No session in refresh response"
220
- logger.error(error_msg)
221
- return False, None, error_msg
222
- else:
223
- error_msg = f"Token refresh failed with status {response.status_code}"
224
- logger.error(error_msg)
225
- return False, None, error_msg
228
+ logger.info("Token refresh successful")
229
+ return True, tokens, "Token refreshed successfully"
226
230
 
227
231
  except Exception as e:
228
232
  error_msg = f"Token refresh error: {str(e)}"
@@ -244,24 +244,25 @@ async def _get_valid_access_token(http_base_url: str, fallback_token: str) -> st
244
244
  return str(data3.get("access_token") or access2 or fallback_token)
245
245
 
246
246
 
247
- async def _force_refresh_access_token(http_base_url: str, fallback_token: str) -> str:
248
- """Force a refresh (if refresh_token exists). Used after HTTP 401."""
249
- data = _load_tokens_file(TOKENS_PATH)
250
- refresh = None
247
+ async def _force_refresh_access_token(http_base_url: str) -> dict[str, Any] | None:
248
+ """Force a refresh (if refresh_token exists). Used after HTTP 401/403.
249
+
250
+ Returns:
251
+ tokens.json-compatible dict on success, otherwise None.
252
+ """
253
+ data = _load_tokens_file(TOKENS_PATH) or {}
251
254
  with _TokensLock():
252
255
  data2 = _load_tokens_file(TOKENS_PATH) or data or {}
253
- refresh2 = data2.get("refresh_token") or refresh
256
+ refresh_token = data2.get("refresh_token")
257
+ if not refresh_token:
258
+ return None
254
259
  try:
255
- new_data = await _refresh_tokens(http_base_url, refresh2)
260
+ new_data = await _refresh_tokens(http_base_url, str(refresh_token))
256
261
  _atomic_write_tokens(TOKENS_PATH, new_data)
257
- return str(new_data.get("access_token"))
262
+ return new_data
258
263
  except Exception:
259
- data3 = _load_tokens_file(TOKENS_PATH) or data2
260
- return str(data3.get("access_token") or fallback_token)
261
- refresh2 = data2.get("refresh_token") or refresh
262
- new_data = await _refresh_tokens(http_base_url, refresh2)
263
- _atomic_write_tokens(TOKENS_PATH, new_data)
264
- return str(new_data.get("access_token"))
264
+ # If refresh failed, keep existing auth state and let the caller decide.
265
+ return None
265
266
 
266
267
  _chunked_writes_lock = threading.Lock()
267
268
  MAX_OUTPUT_BYTES = 50 * 1024 # 50KB cap on command output
@@ -2329,17 +2330,23 @@ async def handle_read_files(
2329
2330
  # If the access token expired mid-session, try once more after a refresh.
2330
2331
  if resp.status_code in (401, 403):
2331
2332
  try:
2332
- upload_token2 = await _force_refresh_access_token(http_base_url, upload_token)
2333
- for fh in open_handles:
2334
- try:
2335
- fh.seek(0)
2336
- except Exception:
2337
- pass
2338
- resp = await client.post(
2339
- upload_url,
2340
- files=file_tuples,
2341
- headers={"Authorization": f"Bearer {upload_token2}"},
2333
+ new_data = await _force_refresh_access_token(http_base_url)
2334
+ upload_token2 = (
2335
+ str(new_data.get("access_token"))
2336
+ if isinstance(new_data, dict) and new_data.get("access_token")
2337
+ else ""
2342
2338
  )
2339
+ if upload_token2:
2340
+ for fh in open_handles:
2341
+ try:
2342
+ fh.seek(0)
2343
+ except Exception:
2344
+ pass
2345
+ resp = await client.post(
2346
+ upload_url,
2347
+ files=file_tuples,
2348
+ headers={"Authorization": f"Bearer {upload_token2}"},
2349
+ )
2343
2350
  except Exception:
2344
2351
  # Fall through to error handling below.
2345
2352
  pass
@@ -2349,9 +2356,15 @@ async def handle_read_files(
2349
2356
  for meta in file_metas:
2350
2357
  urls.append(str(meta["url"]))
2351
2358
  else:
2352
- errors.append(
2353
- f"Upload failed: HTTP {resp.status_code} — {resp.text[:200]}"
2354
- )
2359
+ if resp.status_code in (401, 403):
2360
+ errors.append(
2361
+ f"Upload failed: authentication expired (HTTP {resp.status_code}). "
2362
+ "Please login again in Kiwi Code (or run `kiwi login`) to refresh tokens.",
2363
+ )
2364
+ else:
2365
+ errors.append(
2366
+ f"Upload failed: HTTP {resp.status_code} — {resp.text[:200]}"
2367
+ )
2355
2368
  except Exception as e:
2356
2369
  errors.append(f"Upload error: {str(e)}")
2357
2370
  finally:
@@ -192,7 +192,10 @@ class AutobotsTUI(App):
192
192
  access_token = new_tokens.access_token
193
193
  else:
194
194
  logger.warning(f"Token refresh failed: {message}")
195
- self.token_manager.clear_tokens()
195
+ # Keep tokens on disk so other processes (e.g. kiwi-runtime)
196
+ # can still attempt refresh / recover. We'll prompt the user
197
+ # to login again if needed.
198
+ access_token = None
196
199
  else:
197
200
  logger.info("Valid access token found")
198
201
  access_token = tokens.access_token
@@ -585,22 +588,28 @@ class AutobotsTUI(App):
585
588
  # if tokens and tokens.refresh_token:
586
589
  # runtime_manager.save_refresh_token(tokens.refresh_token)
587
590
 
588
- def _refresh_token_if_needed(self) -> None:
589
- """Check token expiry and refresh if needed."""
591
+ def _refresh_token_if_needed(self, force: bool = False) -> bool:
592
+ """Check token expiry and refresh if needed.
593
+
594
+ Args:
595
+ force: If True, attempt refresh even if the token doesn't look expired.
596
+ """
590
597
  with self.token_manager.file_lock():
591
598
  tokens = self.token_manager.load_tokens()
592
599
  if not tokens:
593
600
  logger.debug("No tokens to refresh")
594
- return
601
+ return False
595
602
 
596
- if not tokens.is_expired():
603
+ if (not force) and (not tokens.is_expired()):
597
604
  logger.debug("Token still valid, skipping refresh")
598
- return
605
+ return True
599
606
 
600
- logger.info("Token expired or near expiry, refreshing...")
601
- success, new_tokens, message = self.autobots_client.refresh_token(
602
- tokens.refresh_token
603
- )
607
+ if not getattr(tokens, "refresh_token", None):
608
+ logger.warning("No refresh token available; cannot refresh access token")
609
+ return False
610
+
611
+ logger.info("Refreshing access token...")
612
+ success, new_tokens, message = self.autobots_client.refresh_token(tokens.refresh_token)
604
613
 
605
614
  if success and new_tokens:
606
615
  logger.info("Token refreshed successfully")
@@ -610,12 +619,10 @@ class AutobotsTUI(App):
610
619
  self.autobots_client.update_token(new_tokens.access_token)
611
620
  # Update shared state
612
621
  self._kiwi_token = new_tokens.access_token
613
- # Write to runtime directory so the runtime process can pick it up
614
- # runtime_manager.save_token(new_tokens.access_token)
615
- # if new_tokens.refresh_token:
616
- # runtime_manager.save_refresh_token(new_tokens.refresh_token)
617
- else:
618
- logger.error(f"Token refresh failed: {message}")
622
+ return True
623
+
624
+ logger.error(f"Token refresh failed: {message}")
625
+ return False
619
626
 
620
627
  # ------------------------------------------------------------------
621
628
 
@@ -0,0 +1,9 @@
1
+ from kiwi_tui.status_words import next_status_word
2
+ def random_verb(*, default: str = "Thinking") -> str:
3
+ """Return a curated "-ing" status word for display (e.g. "Searching")."""
4
+
5
+ return next_status_word(default=default)
6
+
7
+
8
+ def random_adjective(*, default: str = "Curious") -> str: # pragma: no cover
9
+ return default
@@ -2075,7 +2075,22 @@ class DashboardScreen(Screen):
2075
2075
  return
2076
2076
  names = [Path(p).name for p in file_paths]
2077
2077
  self.add_message(f"> Uploading: {', '.join(names)}", "user")
2078
+ # Ensure the token is fresh before upload (important for long-running sessions).
2079
+ try:
2080
+ self.app._refresh_token_if_needed()
2081
+ except Exception:
2082
+ pass
2083
+
2078
2084
  success, urls, message = self.app.autobots_client.upload_files(file_paths)
2085
+ if (not success) and ("HTTP 401" in message or "HTTP 403" in message):
2086
+ # Token may have expired or been rotated; force a refresh and retry once.
2087
+ refreshed = False
2088
+ try:
2089
+ refreshed = bool(self.app._refresh_token_if_needed(force=True))
2090
+ except Exception:
2091
+ refreshed = False
2092
+ if refreshed:
2093
+ success, urls, message = self.app.autobots_client.upload_files(file_paths)
2079
2094
  if success:
2080
2095
  self._pending_urls.extend(urls)
2081
2096
  uploaded_names = [Path(u.rsplit("/", 1)[-1]).name for u in urls]
@@ -0,0 +1,481 @@
1
+ from collections import deque
2
+ from dataclasses import dataclass, field
3
+ from functools import lru_cache
4
+ import random
5
+ import threading
6
+ import time
7
+ from typing import Deque, Iterable
8
+
9
+
10
+ _STATUS_WORDS_RAW: tuple[str, ...] = (
11
+ "Thinking",
12
+ "Working",
13
+ "Responding",
14
+ "Reasoning",
15
+ "Reflecting",
16
+ "Considering",
17
+ "Pondering",
18
+ "Deliberating",
19
+ "Planning",
20
+ "Prioritizing",
21
+ "Organizing",
22
+ "Outlining",
23
+ "Strategizing",
24
+ "Brainstorming",
25
+ "Ideating",
26
+ "Envisioning",
27
+ "Imagining",
28
+ "Inferring",
29
+ "Deducing",
30
+ "Concluding",
31
+ "Deciding",
32
+ "Choosing",
33
+ "Reconsidering",
34
+ "Reassessing",
35
+ "Evaluating",
36
+ "Estimating",
37
+ "Calculating",
38
+ "Computing",
39
+ "Measuring",
40
+ "Counting",
41
+ "Timing",
42
+ "Comparing",
43
+ "Contrasting",
44
+ "Weighing",
45
+ "Balancing",
46
+ "Aligning",
47
+ "Centering",
48
+ "Spacing",
49
+ "Arranging",
50
+ "Resizing",
51
+ "Wrapping",
52
+ "Rendering",
53
+ "Styling",
54
+ "Theming",
55
+ "Polishing",
56
+ "Refining",
57
+ "Improving",
58
+ "Optimizing",
59
+ "Tuning",
60
+ "Simplifying",
61
+ "Streamlining",
62
+ "Modernizing",
63
+ "Upgrading",
64
+ "Updating",
65
+ "Reworking",
66
+ "Rewriting",
67
+ "Editing",
68
+ "Proofreading",
69
+ "Formatting",
70
+ "Drafting",
71
+ "Writing",
72
+ "Summarizing",
73
+ "Condensing",
74
+ "Clarifying",
75
+ "Explaining",
76
+ "Describing",
77
+ "Documenting",
78
+ "Annotating",
79
+ "Commenting",
80
+ "Noting",
81
+ "Labeling",
82
+ "Tagging",
83
+ "Indexing",
84
+ "Cataloging",
85
+ "Listing",
86
+ "Sorting",
87
+ "Grouping",
88
+ "Filtering",
89
+ "Selecting",
90
+ "Collecting",
91
+ "Gathering",
92
+ "Compiling",
93
+ "Assembling",
94
+ "Combining",
95
+ "Merging",
96
+ "Splitting",
97
+ "Separating",
98
+ "Isolating",
99
+ "Narrowing",
100
+ "Pinpointing",
101
+ "Investigating",
102
+ "Inspecting",
103
+ "Reviewing",
104
+ "Rereading",
105
+ "Reading",
106
+ "Scanning",
107
+ "Browsing",
108
+ "Searching",
109
+ "Exploring",
110
+ "Discovering",
111
+ "Observing",
112
+ "Monitoring",
113
+ "Tracking",
114
+ "Tracing",
115
+ "Logging",
116
+ "Instrumenting",
117
+ "Profiling",
118
+ "Benchmarking",
119
+ "Testing",
120
+ "Retesting",
121
+ "Validating",
122
+ "Verifying",
123
+ "Confirming",
124
+ "Checking",
125
+ "Crosschecking",
126
+ "Doublechecking",
127
+ "Rechecking",
128
+ "Auditing",
129
+ "Diagnosing",
130
+ "Debugging",
131
+ "Reproducing",
132
+ "Triaging",
133
+ "Resolving",
134
+ "Fixing",
135
+ "Repairing",
136
+ "Mitigating",
137
+ "Preventing",
138
+ "Safeguarding",
139
+ "Securing",
140
+ "Hardening",
141
+ "Sanitizing",
142
+ "Normalizing",
143
+ "Cleaning",
144
+ "Tidying",
145
+ "Pruning",
146
+ "Trimming",
147
+ "Archiving",
148
+ "Restoring",
149
+ "Saving",
150
+ "Autosaving",
151
+ "Persisting",
152
+ "Storing",
153
+ "Caching",
154
+ "Preloading",
155
+ "Loading",
156
+ "Fetching",
157
+ "Retrieving",
158
+ "Downloading",
159
+ "Uploading",
160
+ "Syncing",
161
+ "Synchronizing",
162
+ "Refreshing",
163
+ "Connecting",
164
+ "Reconnecting",
165
+ "Handshaking",
166
+ "Authenticating",
167
+ "Authorizing",
168
+ "Negotiating",
169
+ "Provisioning",
170
+ "Initializing",
171
+ "Booting",
172
+ "Starting",
173
+ "Restarting",
174
+ "Resuming",
175
+ "Pausing",
176
+ "Waiting",
177
+ "Queueing",
178
+ "Retrying",
179
+ "Recovering",
180
+ "Stabilizing",
181
+ "Configuring",
182
+ "Reconfiguring",
183
+ "Customizing",
184
+ "Parameterizing",
185
+ "Templating",
186
+ "Generating",
187
+ "Synthesizing",
188
+ "Deriving",
189
+ "Transforming",
190
+ "Parsing",
191
+ "Tokenizing",
192
+ "Serializing",
193
+ "Deserializing",
194
+ "Encoding",
195
+ "Decoding",
196
+ "Compressing",
197
+ "Decompressing",
198
+ "Encrypting",
199
+ "Decrypting",
200
+ "Migrating",
201
+ "Seeding",
202
+ "Backfilling",
203
+ "Importing",
204
+ "Exporting",
205
+ "Consolidating",
206
+ "Aggregating",
207
+ "Reconciling",
208
+ "Harmonizing",
209
+ "Coordinating",
210
+ "Collaborating",
211
+ "Communicating",
212
+ "Listening",
213
+ "Asking",
214
+ "Answering",
215
+ "Advising",
216
+ "Suggesting",
217
+ "Recommending",
218
+ "Guiding",
219
+ "Coaching",
220
+ "Helping",
221
+ "Assisting",
222
+ "Supporting",
223
+ "Sharing",
224
+ "Notifying",
225
+ "Reporting",
226
+ "Presenting",
227
+ "Approving",
228
+ "Finalizing",
229
+ "Launching",
230
+ "Deploying",
231
+ "Building",
232
+ "Bundling",
233
+ "Packaging",
234
+ "Releasing",
235
+ "Shipping",
236
+ "Installing",
237
+ "Uninstalling",
238
+ "Executing",
239
+ "Running",
240
+ "Scheduling",
241
+ "Orchestrating",
242
+ "Automating",
243
+ "Scripting",
244
+ "Branching",
245
+ "Committing",
246
+ "Rebasing",
247
+ "Pushing",
248
+ "Pulling",
249
+ "Cloning",
250
+ "Forking",
251
+ "Versioning",
252
+ "Linting",
253
+ "Refactoring",
254
+ "Modularizing",
255
+ "Encapsulating",
256
+ "Integrating",
257
+ "Interfacing",
258
+ "Adapting",
259
+ "Abstracting",
260
+ "Decomposing",
261
+ "Recomposing",
262
+ "Prototyping",
263
+ "Sketching",
264
+ "Tinkering",
265
+ "Noodling",
266
+ "Spelunking",
267
+ "Diagramming",
268
+ "Visualizing",
269
+ "Simulating",
270
+ "Emulating",
271
+ "Staging",
272
+ "Navigating",
273
+ "Traversing",
274
+ "Examining",
275
+ "Disambiguating",
276
+ "Canonicalizing",
277
+ "Contextualizing",
278
+ "Operationalizing",
279
+ "Instrumenting",
280
+ "Triangulating",
281
+ "Deobfuscating",
282
+ "Deinterleaving",
283
+ "Reparameterizing",
284
+ "Recontextualizing",
285
+ "Disentangling",
286
+ "Deconvoluting",
287
+ "Corroborating",
288
+ "Extrapolating",
289
+ "Interpolating",
290
+ "Reconceptualizing",
291
+ "Rationalizing",
292
+ "Formalizing",
293
+ "Generalizing",
294
+ "Specializing",
295
+ "Particularizing",
296
+ "Virtualizing",
297
+ "Containerizing",
298
+ "Deprovisioning",
299
+ "Sandboxing",
300
+ "Backporting",
301
+ "Forwardporting",
302
+ "Snapshotting",
303
+ "Diffing",
304
+ "Grepping",
305
+ "Vendoring",
306
+ "Reindexing",
307
+ "Rerendering",
308
+ "Revalidating",
309
+ "Recalibrating",
310
+ "Reassociating",
311
+ "Enumerating",
312
+ "Assimilating",
313
+ "Ameliorating",
314
+ "Interrogating",
315
+ "Disseminating",
316
+ "Discretizing",
317
+ "Denormalizing",
318
+ "Deprioritizing",
319
+ "Underprovisioning",
320
+ "Assimilating",
321
+ "Scaffolding",
322
+ "Excavating",
323
+ "Unearthing",
324
+ "Deciphering",
325
+ "Synthesizing",
326
+ "Converging",
327
+ "Diverging",
328
+ "Reticulating",
329
+ "Transpiling",
330
+ "Hydrating",
331
+ "Materializing",
332
+ "Manifesting",
333
+ "Crystallizing",
334
+ "Coalescing",
335
+ "Distilling",
336
+ "Ferreting",
337
+ "Foraging",
338
+ "Sifting",
339
+ "Winnowing",
340
+ "Untangling",
341
+ "Unraveling",
342
+ "Threading",
343
+ "Stitching",
344
+ "Splicing",
345
+ "Grafting",
346
+ "Weaving",
347
+ "Braiding",
348
+ "Layering",
349
+ "Etching",
350
+ "Carving",
351
+ "Chiseling",
352
+ "Sculpting",
353
+ "Forging",
354
+ "Tempering",
355
+ "Annealing",
356
+ "Calibrating",
357
+ "Synchronizing",
358
+ "Sequencing",
359
+ "Interleaving",
360
+ "Compacting",
361
+ "Densifying",
362
+ "Canonicalizing",
363
+ "Curating",
364
+ "Shepherding",
365
+ "Marshalling",
366
+ "Funneling",
367
+ "Grounding",
368
+ "Anchoring",
369
+ "Illuminating",
370
+ "Conjuring",
371
+ "Juggling",
372
+ "Wrangling",
373
+ "Untangling",
374
+ "Unsnarling",
375
+ "Puzzling",
376
+ "Scheming",
377
+ "Machinating",
378
+ "Whispering",
379
+ "Summoning",
380
+ "Brewing",
381
+ "Steeping",
382
+ "Percolating",
383
+ "Marinating",
384
+ "Fermenting",
385
+ "Incubating",
386
+ "Hatching",
387
+ "Gobbling",
388
+ "Nibbling",
389
+ "Munching",
390
+ "Chewing",
391
+ "Digesting",
392
+ "Rummaging",
393
+ "Scavenging",
394
+ "Foraging",
395
+ "Poking",
396
+ "Prodding",
397
+ "Tickling",
398
+ "Massaging",
399
+ "Jiggling",
400
+ "Shimmying",
401
+ "Scooting",
402
+ "Zipping",
403
+ "Zooming",
404
+ "Whizzing",
405
+ "Bleeping",
406
+ "Blooping",
407
+ "Beeping",
408
+ "Booping",
409
+ "Tinkering",
410
+ "Fiddling",
411
+ "Doodling",
412
+ "Noodling",
413
+ "Goblinizing",
414
+ "Wizarding",
415
+ "Enchanting",
416
+ "Spellcasting",
417
+ "Alchemizing",
418
+ "Treasure-hunting",
419
+ "Dragon-wrangling",
420
+ )
421
+
422
+
423
+ def _clean_words(items: Iterable[str]) -> tuple[str, ...]:
424
+
425
+ seen: set[str] = set()
426
+ out: list[str] = []
427
+ for it in items:
428
+ w = str(it or "").strip()
429
+ if not w:
430
+ continue
431
+ if " " in w:
432
+ continue
433
+ if not w.isalpha():
434
+ continue
435
+ if not w.lower().endswith("ing"):
436
+ continue
437
+ w = w[0].upper() + w[1:]
438
+ if w in seen:
439
+ continue
440
+ seen.add(w)
441
+ out.append(w)
442
+ return tuple(out)
443
+
444
+
445
+ STATUS_WORDS: tuple[str, ...] = _clean_words(_STATUS_WORDS_RAW)
446
+
447
+ if len(STATUS_WORDS) < 50: # pragma: no cover
448
+ STATUS_WORDS = ("Thinking", "Working", "Responding")
449
+
450
+
451
+ @dataclass
452
+ class _DeckRotator:
453
+ words: tuple[str, ...]
454
+ rng: random.Random
455
+ lock: threading.Lock = field(default_factory=threading.Lock)
456
+ deck: Deque[str] = field(default_factory=deque)
457
+
458
+ def next(self, *, default: str = "Thinking") -> str:
459
+ if not self.words:
460
+ return default
461
+
462
+ with self.lock:
463
+ if not self.deck:
464
+ items = list(self.words)
465
+ self.rng.shuffle(items)
466
+ self.deck.extend(items)
467
+
468
+ try:
469
+ return self.deck.popleft()
470
+ except Exception:
471
+ return default
472
+
473
+
474
+ @lru_cache(maxsize=1)
475
+ def _rotator() -> _DeckRotator:
476
+ seed = time.time_ns() & 0xFFFFFFFF
477
+ return _DeckRotator(words=STATUS_WORDS, rng=random.Random(seed))
478
+
479
+
480
+ def next_status_word(*, default: str = "Thinking") -> str:
481
+ return _rotator().next(default=default)
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.28"
400
+ version = "0.0.30"
401
401
  source = { editable = "." }
402
402
  dependencies = [
403
403
  { name = "autobots-client" },
@@ -1,80 +0,0 @@
1
- from functools import lru_cache
2
-
3
-
4
- @lru_cache(maxsize=1)
5
- def _ww() -> object | None:
6
- try:
7
- from wonderwords import RandomWord # type: ignore
8
-
9
- return RandomWord()
10
- except Exception:
11
- return None
12
-
13
-
14
- def _to_present_participle(verb: str) -> str:
15
- v = (verb or "").strip()
16
- if not v:
17
- return ""
18
- w = v.lower()
19
-
20
- # Common irregulars / spelling rules that a simple suffix approach misses.
21
- irregular = {
22
- "be": "being",
23
- "see": "seeing",
24
- "flee": "fleeing",
25
- "knee": "kneeing",
26
- "die": "dying",
27
- "tie": "tying",
28
- "lie": "lying",
29
- }
30
- if w in irregular:
31
- return irregular[w].capitalize()
32
-
33
- # ie -> ying (die -> dying).
34
- if w.endswith("ie") and len(w) > 2:
35
- return (w[:-2] + "ying").capitalize()
36
-
37
- # Drop trailing e (make -> making), but keep ee/ye/oe (see -> seeing).
38
- if w.endswith("e") and not w.endswith(("ee", "ye", "oe")) and len(w) > 1:
39
- return (w[:-1] + "ing").capitalize()
40
-
41
- # Verbs ending in 'ic' often add 'k' (panic -> panicking).
42
- if w.endswith("ic") and len(w) > 2:
43
- return (w + "king").capitalize()
44
-
45
- vowels = set("aeiou")
46
- # Double final consonant for short CVC words with a/i/o/u vowel (run -> running).
47
- if (
48
- 3 <= len(w) <= 4
49
- and w[-1] not in vowels
50
- and w[-1] not in "wxy"
51
- and w[-2] in vowels
52
- and w[-2] != "e"
53
- and w[-3] not in vowels
54
- ):
55
- return (w + w[-1] + "ing").capitalize()
56
-
57
- return (w + "ing").capitalize()
58
-
59
-
60
- def random_verb(*, default: str = "Thinking") -> str:
61
- """Return a random present-participle verb for display (e.g. "Searching")."""
62
- rw = _ww()
63
- if rw is None:
64
- return default
65
-
66
- try:
67
- # wonderwords supports parts_of_speech filtering.
68
- word = rw.word(include_parts_of_speech=["verbs"]) # type: ignore[attr-defined]
69
- word = str(word or "").strip()
70
- if not word:
71
- return default
72
- ing = _to_present_participle(word)
73
- return ing if ing else default
74
- except Exception:
75
- return default
76
-
77
-
78
- # Backward-compat alias (older code used adjectives).
79
- def random_adjective(*, default: str = "Curious") -> str: # pragma: no cover
80
- return default
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes