usage-cli 0.29.32__py3-none-any.whl

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 (109) hide show
  1. adapters/__init__.py +5 -0
  2. adapters/agy.py +68 -0
  3. adapters/claude.py +215 -0
  4. adapters/codex.py +209 -0
  5. adapters/rate_limits.py +76 -0
  6. adapters/registry.py +17 -0
  7. adapters/types.py +139 -0
  8. agy_disk_cache.py +135 -0
  9. agy_loader.py +416 -0
  10. agy_quota_probe.py +748 -0
  11. agy_window_keeper.py +185 -0
  12. analyzer/__init__.py +5 -0
  13. analyzer/aggregator.py +139 -0
  14. analyzer/blocks.py +80 -0
  15. analyzer/diagnoser.py +638 -0
  16. analyzer/insights.py +277 -0
  17. analyzer/persona_loader.py +199 -0
  18. analyzer/reporter.py +989 -0
  19. analyzer/subscription.py +108 -0
  20. burn_rate.py +75 -0
  21. cache_quarantine.py +50 -0
  22. codex_disk_cache.py +227 -0
  23. codex_events.py +136 -0
  24. codex_fork_replay.py +111 -0
  25. codex_loader.py +1426 -0
  26. codex_paths.py +20 -0
  27. critter_frames.py +26 -0
  28. discussion_bridge.py +1196 -0
  29. discussion_cli.py +844 -0
  30. discussion_session.py +622 -0
  31. discussion_usage.py +13 -0
  32. discussion_window.py +955 -0
  33. disk_cache_common.py +132 -0
  34. disk_cache_lifecycle.py +39 -0
  35. doctor.py +452 -0
  36. fsevents_watch.py +207 -0
  37. history_disk_cache.py +110 -0
  38. history_loader.py +416 -0
  39. i18n.py +88 -0
  40. jsonl_limits.py +17 -0
  41. jsonl_utils.py +40 -0
  42. login_item.py +154 -0
  43. main.py +387 -0
  44. menubar.py +1201 -0
  45. menubar_actions.py +204 -0
  46. menubar_agy.py +193 -0
  47. menubar_chrome.py +156 -0
  48. menubar_menu.py +169 -0
  49. menubar_notify.py +102 -0
  50. menubar_popover.py +233 -0
  51. menubar_prefs.py +118 -0
  52. menubar_refresh.py +285 -0
  53. menubar_state.py +1200 -0
  54. menubar_title.py +157 -0
  55. menubar_update.py +123 -0
  56. panel_window.py +78 -0
  57. panel_window_state.py +159 -0
  58. panels/__init__.py +186 -0
  59. panels/base.py +83 -0
  60. panels/dynamic_height.py +140 -0
  61. panels/payload.py +178 -0
  62. panels/web_panel.py +513 -0
  63. panels/window_drag.py +56 -0
  64. prefs.py +44 -0
  65. pricing.py +452 -0
  66. project_resolver.py +112 -0
  67. service_status.py +383 -0
  68. session_hooks.py +1154 -0
  69. setup_app.py +171 -0
  70. setup_hook.py +1011 -0
  71. statusline_settings.py +160 -0
  72. talent_market_bridge.py +243 -0
  73. time_utils.py +24 -0
  74. tui.py +288 -0
  75. tui_sprite.py +206 -0
  76. ui/__init__.py +5 -0
  77. ui/html_report.py +923 -0
  78. ui/report_scripts.py +251 -0
  79. ui/report_styles.py +370 -0
  80. ui/tables.py +888 -0
  81. update_checker.py +156 -0
  82. update_gate.py +66 -0
  83. update_release_notes.py +49 -0
  84. usage_cli-0.29.32.data/data/share/usage/i18n.json +2427 -0
  85. usage_cli-0.29.32.dist-info/METADATA +223 -0
  86. usage_cli-0.29.32.dist-info/RECORD +109 -0
  87. usage_cli-0.29.32.dist-info/WHEEL +5 -0
  88. usage_cli-0.29.32.dist-info/entry_points.txt +3 -0
  89. usage_cli-0.29.32.dist-info/licenses/LICENSE +663 -0
  90. usage_cli-0.29.32.dist-info/top_level.txt +80 -0
  91. usage_cli.py +827 -0
  92. usage_client.py +487 -0
  93. usage_diagnosis_snapshot.py +143 -0
  94. usage_dir_sweeper.py +100 -0
  95. usage_lang.py +79 -0
  96. usage_logging.py +75 -0
  97. usage_notifications.py +96 -0
  98. usage_rate.py +97 -0
  99. usage_session_resume.py +913 -0
  100. usage_statusline.py +810 -0
  101. usage_statusline_agy.py +397 -0
  102. usage_statusline_forwarder.py +88 -0
  103. usage_terse_mode.py +223 -0
  104. usage_terse_reminder.py +151 -0
  105. win_login_item.py +53 -0
  106. window_keeper.py +264 -0
  107. windows_watch.py +443 -0
  108. wintray.py +2014 -0
  109. wintray_menu.py +136 -0
usage_terse_mode.py ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
4
+ #
5
+ # Part of "usage". Free software licensed under the GNU Affero General Public
6
+ # License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
7
+
8
+ """usage SessionStart hook — inject terse-mode instructions into a new Claude session.
9
+
10
+ Claude Code runs this on SessionStart (matcher ``startup|clear``) and pipes the
11
+ session JSON on stdin. Unlike ``usage_session_resume.py``, this hook does not
12
+ inspect transcripts or git state: if stdin parses as a JSON object at all, it
13
+ prints a fixed instruction telling Claude to keep replies terse while leaving
14
+ code, commands, file paths, and error messages untouched.
15
+
16
+ Stdlib-only and 3.9-safe — same constraint as ``usage_statusline.py`` and
17
+ ``usage_session_resume.py``: it may run under macOS's bundled
18
+ ``/usr/bin/python3`` (3.9), so no third-party imports, no ``datetime.UTC``, no
19
+ runtime ``X | Y`` types. The prompt wording lives in a sidecar written by
20
+ ``setup_hook``; if that file is missing, this script falls back to embedded
21
+ defaults. Any failure exits 0 with no output.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import sys
29
+ from pathlib import Path
30
+ from typing import Any, cast
31
+
32
+ __version__ = "1.0"
33
+
34
+
35
+ def _read_stdin_utf8() -> str:
36
+ buffer = getattr(sys.stdin, "buffer", None)
37
+ if buffer is None:
38
+ return sys.stdin.read()
39
+ return cast(bytes, buffer.read()).decode("utf-8", "replace")
40
+
41
+
42
+ PROMPT_SIDECAR = Path(os.path.expanduser("~/.claude/usage-terse-prompt.json"))
43
+
44
+ _DEFAULT_INSTRUCTION: dict[str, str] = {
45
+ "zh-TW": (
46
+ "(這個對話已開啟「精簡模式」:請在這次對話第一則回覆的最前面提一下這件事——如果"
47
+ "你同時收到其他要打招呼的指示(例如進度交接),就把「精簡模式已開啟」自然併入那句"
48
+ "招呼裡就好,不要另外多開一句;如果沒有其他招呼可以搭,就自己說一行「🐾 已開啟精簡"
49
+ "模式,回覆會盡量簡短,繼續吧!」。從現在起,直到這個對話結束為止,每一則回覆都要"
50
+ "遵守這條規則——不會因為對話變長、話題變多就淡忘或恢復正常語氣。允許用短句、片語"
51
+ "甚至不成句的斷句表達,不必湊成完整句子;去掉虛詞贅字、客套語、重複鋪陳與不必要的"
52
+ "過渡句;用詞挑簡短的(例如「修」不要「針對這個問題實作解決方案」)。不用裝飾性"
53
+ "表格或表情符號,也不要旁白工具呼叫的過程。不要自創縮寫(例如「設定」別縮成「設」、"
54
+ "「函式」別縮成「函」)——這類縮寫斷詞長度跟完整詞一樣,省不到字數,反而讓讀者要"
55
+ "多想一下,直接用完整詞更省事也更清楚。程式碼、指令、檔案路徑、錯誤訊息一個字都"
56
+ "不能省略或改寫。遇到安全警示、不可逆操作的確認、或多步驟中省略連接詞會有誤讀風險"
57
+ "的情況,這幾種要先恢復完整、講清楚,講完再切回精簡語氣。如果使用者明確要求詳細"
58
+ "解說、逐步教學,或情境需要完整推理,仍以使用者當下的要求為準,不要因為這個模式而"
59
+ "省略關鍵資訊。)"
60
+ ),
61
+ "en": (
62
+ "(Terse mode is on for this entire conversation. Mention this at the very "
63
+ "start of your first reply — if you're already leading with another greeting "
64
+ "(e.g. a resume handoff), fold \"terse mode is on\" into that same line instead "
65
+ "of adding a separate one; if there's no other greeting to fold into, say your "
66
+ "own line: \"🐾 Terse mode is on — keeping replies short, let's go!\" This "
67
+ "applies starting now and lasting until the conversation ends — apply it to "
68
+ "every single reply, no matter how long the conversation gets; don't let it "
69
+ "fade or drift back to normal verbosity partway through. Drop articles "
70
+ "(a/an/the), filler (just/really/basically/actually), pleasantries (sure/"
71
+ "certainly/happy to), and hedging. Fragments are fine. Prefer short synonyms "
72
+ "(big, not extensive; fix, not \"implement a solution for\"). No decorative "
73
+ "tables, emoji, or tool-call narration. Never invent abbreviations (cfg/impl/"
74
+ "req/res) — the tokenizer splits them the same as the full word, so nothing "
75
+ "is saved and the reader still has to decode it; use the full word instead. "
76
+ "Code, commands, file paths, and error messages must stay byte-exact, never "
77
+ "trimmed or rewritten. Drop terseness for security warnings, irreversible-"
78
+ "action confirmations, and multi-step instructions where a fragment or dropped "
79
+ "conjunction risks being misread — write those out in full, then resume terse "
80
+ "mode after. If the user explicitly asks for a detailed walkthrough, step-by-"
81
+ "step teaching, or the situation needs full reasoning, follow that instead — "
82
+ "don't drop essential information just to stay terse.)"
83
+ ),
84
+ "zh-CN": (
85
+ "(这个对话已开启「精简模式」:请在这次对话第一则回复的最前面提一下这件事——如果"
86
+ "你同时收到其他要打招呼的指示(例如进度交接),就把「精简模式已开启」自然并入那句"
87
+ "招呼里就好,不要另外多开一句;如果没有其他招呼可以搭,就自己说一行「🐾 已开启精简"
88
+ "模式,回复会尽量简短,继续吧!」。从现在起,直到这个对话结束为止,每一则回复都要"
89
+ "遵守这条规则——不会因为对话变长、话题变多就淡忘或恢复正常语气。允许用短句、短语"
90
+ "甚至不成句的断句表达,不必凑成完整句子;去掉虚词赘字、客套语、重复铺陈和不必要的"
91
+ "过渡句;用词挑简短的(例如「修」不要「针对这个问题实现解决方案」)。不用装饰性表格"
92
+ "或表情符号,也不要旁白工具调用的过程。不要自创缩写(例如「配置」别缩成「配」、「函数」"
93
+ "别缩成「函」)——这类缩写分词长度跟完整词一样,省不到字数,反而让读者要多想一下,"
94
+ "直接用完整词更省事也更清楚。代码、指令、文件路径、错误信息一个字都不能省略或改写。"
95
+ "遇到安全警示、不可逆操作的确认、或多步骤中省略连接词会有误读风险的情况,这几种要"
96
+ "先恢复完整、讲清楚,讲完再切回精简语气。如果用户明确要求详细讲解、逐步教学,或"
97
+ "情境需要完整推理,仍以用户当下的要求为准,不要因为这个模式而省略关键信息。)"
98
+ ),
99
+ "ja": (
100
+ "(この会話では「簡潔モード」が有効です。最初の返信の冒頭でこのことに触れてくださ"
101
+ "い——すでに他の挨拶(進捗の引き継ぎなど)を述べる予定がある場合は、「簡潔モードが"
102
+ "有効」であることをその挨拶に自然に組み込み、別の行を追加しないでください。組み込"
103
+ "める挨拶がない場合は、自分で一行「🐾 簡潔モードが有効になりました。返答は短くして"
104
+ "いきます!」と述べてください。今この瞬間から会話が終わるまで、すべての返信でこの"
105
+ "ルールを守ってください——会話が長くなったり話題が増えたりしても、薄れたり通常の"
106
+ "口調に戻ったりしないこと。体言止めや断片的な言い方でも構わないので、無理に完全な"
107
+ "文にしないでください。前置き、丁寧すぎる言い回し、重複した表現、不必要なつなぎ文を"
108
+ "省き、短文や箇条書きで済むなら長い段落に広げないでください。言葉は短い方を選んで"
109
+ "ください(例:「修正」であって「この問題に対する解決策を実装する」ではない)。装飾的"
110
+ "な表や絵文字、ツール呼び出しの実況も不要です。独自の省略語は作らないでください"
111
+ "(例:「設定」を「設」に略すなど)——トークナイザー上は完全な語と同じ長さになり、"
112
+ "何も節約にならず読み手が余計に解読する手間が増えるだけなので、完全な語のままの方が"
113
+ "得です。コード、コマンド、ファイルパス、エラーメッセージは一文字たりとも省略・"
114
+ "書き換えしないこと。セキュリティ警告、不可逆操作の確認、接続詞を省くと誤読の恐れが"
115
+ "ある複数手順の説明では、簡潔さより先に完全な文で明確に伝え、伝え終えたら簡潔モード"
116
+ "に戻ってください。ユーザーが明確に詳しい解説や段階的な説明を求めた場合、または"
117
+ "状況的に十分な推論が必要な場合は、その要求を優先し、このモードのために重要な情報を"
118
+ "落とさないでください。)"
119
+ ),
120
+ "ko": (
121
+ "(이 대화에는 '간결 모드'가 켜져 있습니다. 첫 답변 맨 앞에서 이 사실을 언급하세요 — "
122
+ "이미 다른 인사(예: 진행 상황 인수인계)를 할 예정이라면, '간결 모드가 켜졌다'는 내용을 "
123
+ "그 인사에 자연스럽게 합쳐서 한 줄로 말하고 따로 추가하지 마세요. 합칠 인사가 없다면 "
124
+ "직접 한 줄로 \"🐾 간결 모드를 켰어요. 답변을 짧게 이어갈게요!\"라고 말하세요. 지금부터 "
125
+ "이 대화가 끝날 때까지 모든 답변에 이 규칙을 적용하세요 — 대화가 길어지거나 주제가 "
126
+ "늘어나도 흐려지거나 원래 말투로 돌아가지 마세요. 완전한 문장으로 억지로 맞추지 "
127
+ "말고 짧은 구나 단편적인 표현도 괜찮습니다. 빈말, 지나친 격식, 반복되는 서두, "
128
+ "불필요한 연결 문장은 덜어내고, 짧은 문장이나 목록으로 충분하면 긴 단락으로 늘이지 "
129
+ "마세요. 단어는 짧은 쪽을 고르세요 (예: \"이 문제에 대한 해결책을 구현하다\" 대신 "
130
+ "\"고치다\"). 장식용 표나 이모지, 도구 호출 중계는 넣지 마세요. 임의로 줄임말을 "
131
+ "만들지 마세요 (예: \"설정\"을 \"설\"로 줄이는 식) — 토크나이저 상으로는 완전한 단어와 "
132
+ "길이가 같아서 절약되는 게 없고 읽는 사람만 더 해석해야 하니, 완전한 단어를 쓰는 "
133
+ "편이 더 낫습니다. 코드, 명령어, 파일 경로, 오류 메시지는 한 글자도 생략하거나 "
134
+ "바꾸면 안 됩니다. 보안 경고, 되돌릴 수 없는 작업의 확인, 접속사를 생략하면 오독 "
135
+ "위험이 있는 다단계 설명에서는 간결함보다 먼저 완전하고 명확하게 전달한 뒤, 전달이 "
136
+ "끝나면 다시 간결 모드로 돌아가세요. 사용자가 자세한 설명이나 단계별 안내를 명확히 "
137
+ "요청했거나, 상황상 충분한 추론이 꼭 필요하다면 그 요구를 우선하고, 이 모드 때문에 "
138
+ "핵심 정보를 빼먹지 마세요.)"
139
+ ),
140
+ }
141
+
142
+
143
+ def _windows_system_lang() -> str:
144
+ if os.name != "nt":
145
+ return ""
146
+ try:
147
+ import ctypes
148
+ import locale as _locale
149
+
150
+ windll = getattr(ctypes, "windll", None)
151
+ if windll is None:
152
+ return ""
153
+ lang_id = int(windll.kernel32.GetUserDefaultUILanguage())
154
+ return _locale.windows_locale.get(lang_id, "") or ""
155
+ except Exception:
156
+ return ""
157
+
158
+
159
+ def _detect_lang() -> str:
160
+ # Windows 上的 LANG 多半是 Git Bash / MSYS 帶進來的,不代表使用者的系統語言。
161
+ keys = (
162
+ ("USAGE_LANG", "TT_LANG") if sys.platform == "win32" else ("USAGE_LANG", "TT_LANG", "LANG")
163
+ )
164
+ for key in keys:
165
+ value = os.environ.get(key, "").strip()
166
+ if value:
167
+ return _normalize_lang(value)
168
+ return _normalize_lang(_windows_system_lang())
169
+
170
+
171
+ def _normalize_lang(code: str) -> str:
172
+ normalized = code.split(".")[0].strip().lower().replace("_", "-")
173
+ if normalized in {"zh-tw", "zh-hk", "zh-hant"} or normalized.startswith(("zh-tw-", "zh-hant")):
174
+ return "zh-TW"
175
+ if normalized in {"zh-cn", "zh-sg", "zh-hans", "zh"} or normalized.startswith(
176
+ ("zh-cn-", "zh-hans")
177
+ ):
178
+ return "zh-CN"
179
+ if normalized.startswith("ja"):
180
+ return "ja"
181
+ if normalized.startswith("ko"):
182
+ return "ko"
183
+ return "en"
184
+
185
+
186
+ def _load_instruction(lang: str) -> str:
187
+ try:
188
+ raw = json.loads(PROMPT_SIDECAR.read_text(encoding="utf-8"))
189
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
190
+ raw = None
191
+ if isinstance(raw, dict):
192
+ table = raw.get(lang)
193
+ if isinstance(table, dict):
194
+ instruction = table.get("instruction")
195
+ if isinstance(instruction, str) and instruction:
196
+ return instruction
197
+ table = raw.get("en")
198
+ if isinstance(table, dict):
199
+ instruction = table.get("instruction")
200
+ if isinstance(instruction, str) and instruction:
201
+ return instruction
202
+ return _DEFAULT_INSTRUCTION.get(lang, _DEFAULT_INSTRUCTION["en"])
203
+
204
+
205
+ def main() -> int:
206
+ try:
207
+ payload = json.loads(_read_stdin_utf8() or "{}")
208
+ except (OSError, ValueError, TypeError):
209
+ return 0
210
+ if not isinstance(payload, dict):
211
+ return 0
212
+ output: dict[str, Any] = {
213
+ "hookSpecificOutput": {
214
+ "hookEventName": "SessionStart",
215
+ "additionalContext": _load_instruction(_detect_lang()),
216
+ }
217
+ }
218
+ print(json.dumps(output, ensure_ascii=False))
219
+ return 0
220
+
221
+
222
+ if __name__ == "__main__":
223
+ raise SystemExit(main())
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
4
+ #
5
+ # Part of "usage". Free software licensed under the GNU Affero General Public
6
+ # License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
7
+
8
+ """usage UserPromptSubmit hook — re-inject a terse-mode reminder on each user message.
9
+
10
+ Claude Code runs this every time the user submits a prompt and pipes the session
11
+ JSON on stdin. ``usage_terse_mode.py`` only fires once at SessionStart; over a long
12
+ conversation the terse style drifts back to verbose. This hook appends a one-line
13
+ bracketed nudge — not a request the model must answer — so terseness holds across
14
+ turns. Code, commands, paths, and error messages are explicitly left byte-exact.
15
+
16
+ Stdlib-only and 3.9-safe — same constraint as ``usage_statusline.py`` and
17
+ ``usage_terse_mode.py``: it may run under macOS's bundled ``/usr/bin/python3``
18
+ (3.9), so no third-party imports, no ``datetime.UTC``, no runtime ``X | Y``
19
+ types. The reminder wording lives in the same sidecar written by ``setup_hook``
20
+ (``~/.claude/usage-terse-prompt.json``, the ``reminder`` field per language); if
21
+ that file or field is missing, this script falls back to embedded defaults. Any
22
+ failure exits 0 with no output.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import os
29
+ import sys
30
+ from pathlib import Path
31
+ from typing import Any, cast
32
+
33
+ __version__ = "1.0"
34
+
35
+
36
+ def _read_stdin_utf8() -> str:
37
+ buffer = getattr(sys.stdin, "buffer", None)
38
+ if buffer is None:
39
+ return sys.stdin.read()
40
+ return cast(bytes, buffer.read()).decode("utf-8", "replace")
41
+
42
+
43
+ PROMPT_SIDECAR = Path(os.path.expanduser("~/.claude/usage-terse-prompt.json"))
44
+
45
+ _DEFAULT_REMINDER: dict[str, str] = {
46
+ "zh-TW": (
47
+ "[精簡模式仍生效:這則回覆保持精簡;程式碼、指令、路徑、錯誤訊息照舊一字不改;"
48
+ "安全警示與不可逆操作確認仍要講完整。]"
49
+ ),
50
+ "en": (
51
+ "[Terse mode is still on: keep this reply brief; code, commands, paths, and "
52
+ "error messages stay byte-exact; security warnings and irreversible-action "
53
+ "confirmations must still be written out in full.]"
54
+ ),
55
+ "zh-CN": (
56
+ "[精简模式仍生效:这则回复保持精简;代码、指令、路径、错误信息照旧一字不改;"
57
+ "安全警示与不可逆操作确认仍要讲完整。]"
58
+ ),
59
+ "ja": (
60
+ "[簡潔モードは引き続き有効:この返信は簡潔に;コード、コマンド、パス、エラー"
61
+ "メッセージは一字そのまま;セキュリティ警告と不可逆操作の確認は引き続き完全に"
62
+ "記述すること。]"
63
+ ),
64
+ "ko": (
65
+ "[간결 모드 계속 적용 중: 이 답변은 짧게; 코드, 명령어, 경로, 오류 메시지는 한 "
66
+ "글자 그대로; 보안 경고와 되돌릴 수 없는 작업 확인은 여전히 완전히 적을 것.]"
67
+ ),
68
+ }
69
+
70
+
71
+ def _windows_system_lang() -> str:
72
+ if os.name != "nt":
73
+ return ""
74
+ try:
75
+ import ctypes
76
+ import locale as _locale
77
+
78
+ windll = getattr(ctypes, "windll", None)
79
+ if windll is None:
80
+ return ""
81
+ lang_id = int(windll.kernel32.GetUserDefaultUILanguage())
82
+ return _locale.windows_locale.get(lang_id, "") or ""
83
+ except Exception:
84
+ return ""
85
+
86
+
87
+ def _detect_lang() -> str:
88
+ # Windows 上的 LANG 多半是 Git Bash / MSYS 帶進來的,不代表使用者的系統語言。
89
+ keys = (
90
+ ("USAGE_LANG", "TT_LANG") if sys.platform == "win32" else ("USAGE_LANG", "TT_LANG", "LANG")
91
+ )
92
+ for key in keys:
93
+ value = os.environ.get(key, "").strip()
94
+ if value:
95
+ return _normalize_lang(value)
96
+ return _normalize_lang(_windows_system_lang())
97
+
98
+
99
+ def _normalize_lang(code: str) -> str:
100
+ normalized = code.split(".")[0].strip().lower().replace("_", "-")
101
+ if normalized in {"zh-tw", "zh-hk", "zh-hant"} or normalized.startswith(("zh-tw-", "zh-hant")):
102
+ return "zh-TW"
103
+ if normalized in {"zh-cn", "zh-sg", "zh-hans", "zh"} or normalized.startswith(
104
+ ("zh-cn-", "zh-hans")
105
+ ):
106
+ return "zh-CN"
107
+ if normalized.startswith("ja"):
108
+ return "ja"
109
+ if normalized.startswith("ko"):
110
+ return "ko"
111
+ return "en"
112
+
113
+
114
+ def _load_reminder(lang: str) -> str:
115
+ try:
116
+ raw = json.loads(PROMPT_SIDECAR.read_text(encoding="utf-8"))
117
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
118
+ raw = None
119
+ if isinstance(raw, dict):
120
+ table = raw.get(lang)
121
+ if isinstance(table, dict):
122
+ reminder = table.get("reminder")
123
+ if isinstance(reminder, str) and reminder:
124
+ return reminder
125
+ table = raw.get("en")
126
+ if isinstance(table, dict):
127
+ reminder = table.get("reminder")
128
+ if isinstance(reminder, str) and reminder:
129
+ return reminder
130
+ return _DEFAULT_REMINDER.get(lang, _DEFAULT_REMINDER["en"])
131
+
132
+
133
+ def main() -> int:
134
+ try:
135
+ payload = json.loads(_read_stdin_utf8() or "{}")
136
+ except (OSError, ValueError, TypeError):
137
+ return 0
138
+ if not isinstance(payload, dict):
139
+ return 0
140
+ output: dict[str, Any] = {
141
+ "hookSpecificOutput": {
142
+ "hookEventName": "UserPromptSubmit",
143
+ "additionalContext": _load_reminder(_detect_lang()),
144
+ }
145
+ }
146
+ print(json.dumps(output, ensure_ascii=False))
147
+ return 0
148
+
149
+
150
+ if __name__ == "__main__":
151
+ raise SystemExit(main())
win_login_item.py ADDED
@@ -0,0 +1,53 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
11
+ VALUE_NAME = "usage"
12
+
13
+
14
+ def _winreg() -> Any:
15
+ import winreg
16
+
17
+ return winreg
18
+
19
+
20
+ def _command() -> str:
21
+ if getattr(sys, "frozen", False):
22
+ return f'"{Path(sys.executable)}"'
23
+ return f'"{Path(sys.executable)}" "{Path(__file__).resolve().with_name("main.py")}"'
24
+
25
+
26
+ def is_enabled() -> bool:
27
+ winreg = _winreg()
28
+ try:
29
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY) as key:
30
+ value, _value_type = winreg.QueryValueEx(key, VALUE_NAME)
31
+ except FileNotFoundError:
32
+ return False
33
+ return bool(value == _command())
34
+
35
+
36
+ def enable() -> None:
37
+ winreg = _winreg()
38
+ with winreg.CreateKey(winreg.HKEY_CURRENT_USER, RUN_KEY) as key:
39
+ winreg.SetValueEx(key, VALUE_NAME, 0, winreg.REG_SZ, _command())
40
+
41
+
42
+ def disable() -> None:
43
+ winreg = _winreg()
44
+ try:
45
+ with winreg.OpenKey(
46
+ winreg.HKEY_CURRENT_USER,
47
+ RUN_KEY,
48
+ 0,
49
+ winreg.KEY_SET_VALUE,
50
+ ) as key:
51
+ winreg.DeleteValue(key, VALUE_NAME)
52
+ except FileNotFoundError:
53
+ return