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.
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/PKG-INFO +1 -1
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/pyproject.toml +1 -1
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/client.py +45 -41
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/main.py +39 -26
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/main.py +23 -16
- kiwi_code-0.0.30/src/kiwi_tui/random_words.py +9 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/dashboard.py +15 -0
- kiwi_code-0.0.30/src/kiwi_tui/status_words.py +481 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/uv.lock +1 -1
- kiwi_code-0.0.28/src/kiwi_tui/random_words.py +0 -80
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.gitignore +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/.python-version +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/CLAUDE.md +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/Makefile +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/README.md +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/test_hello.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/__init__.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/conftest.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.28 → kiwi_code-0.0.30}/tests/test_tui_headless.py +0 -0
|
@@ -177,52 +177,56 @@ class AutobotsClientWrapper:
|
|
|
177
177
|
try:
|
|
178
178
|
logger.info("Attempting to refresh access token")
|
|
179
179
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
225
|
+
# Update client with new token
|
|
226
|
+
self.update_token(tokens.access_token)
|
|
215
227
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
248
|
-
"""Force a refresh (if refresh_token exists). Used after HTTP 401.
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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,
|
|
260
|
+
new_data = await _refresh_tokens(http_base_url, str(refresh_token))
|
|
256
261
|
_atomic_write_tokens(TOKENS_PATH, new_data)
|
|
257
|
-
return
|
|
262
|
+
return new_data
|
|
258
263
|
except Exception:
|
|
259
|
-
|
|
260
|
-
return
|
|
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
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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
|
-
|
|
2353
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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)
|
|
@@ -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
|
|
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
|