tailii-host 0.1.0

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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/apnsConfigStore.d.ts +22 -0
  4. package/dist/apnsConfigStore.js +50 -0
  5. package/dist/apnsConfigStore.js.map +1 -0
  6. package/dist/apnsJwtProvider.d.ts +50 -0
  7. package/dist/apnsJwtProvider.js +158 -0
  8. package/dist/apnsJwtProvider.js.map +1 -0
  9. package/dist/apnsSender.d.ts +69 -0
  10. package/dist/apnsSender.js +155 -0
  11. package/dist/apnsSender.js.map +1 -0
  12. package/dist/approvalPushNotifier.d.ts +100 -0
  13. package/dist/approvalPushNotifier.js +236 -0
  14. package/dist/approvalPushNotifier.js.map +1 -0
  15. package/dist/broker.d.ts +24 -0
  16. package/dist/broker.js +197 -0
  17. package/dist/broker.js.map +1 -0
  18. package/dist/chatTailController.d.ts +87 -0
  19. package/dist/chatTailController.js +292 -0
  20. package/dist/chatTailController.js.map +1 -0
  21. package/dist/claudeSessionStore.d.ts +15 -0
  22. package/dist/claudeSessionStore.js +201 -0
  23. package/dist/claudeSessionStore.js.map +1 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +53 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/codexRolloutTailer.d.ts +56 -0
  28. package/dist/codexRolloutTailer.js +303 -0
  29. package/dist/codexRolloutTailer.js.map +1 -0
  30. package/dist/codexSessionStore.d.ts +23 -0
  31. package/dist/codexSessionStore.js +274 -0
  32. package/dist/codexSessionStore.js.map +1 -0
  33. package/dist/codexUsage.d.ts +16 -0
  34. package/dist/codexUsage.js +144 -0
  35. package/dist/codexUsage.js.map +1 -0
  36. package/dist/deviceTokenStore.d.ts +38 -0
  37. package/dist/deviceTokenStore.js +73 -0
  38. package/dist/deviceTokenStore.js.map +1 -0
  39. package/dist/dirLister.d.ts +9 -0
  40. package/dist/dirLister.js +98 -0
  41. package/dist/dirLister.js.map +1 -0
  42. package/dist/doctor.d.ts +36 -0
  43. package/dist/doctor.js +162 -0
  44. package/dist/doctor.js.map +1 -0
  45. package/dist/engine.d.ts +85 -0
  46. package/dist/engine.js +962 -0
  47. package/dist/engine.js.map +1 -0
  48. package/dist/hook.d.ts +67 -0
  49. package/dist/hook.js +511 -0
  50. package/dist/hook.js.map +1 -0
  51. package/dist/hookSettings.d.ts +40 -0
  52. package/dist/hookSettings.js +128 -0
  53. package/dist/hookSettings.js.map +1 -0
  54. package/dist/idleReaper.d.ts +13 -0
  55. package/dist/idleReaper.js +30 -0
  56. package/dist/idleReaper.js.map +1 -0
  57. package/dist/imageService.d.ts +53 -0
  58. package/dist/imageService.js +280 -0
  59. package/dist/imageService.js.map +1 -0
  60. package/dist/kick.d.ts +8 -0
  61. package/dist/kick.js +60 -0
  62. package/dist/kick.js.map +1 -0
  63. package/dist/launch.d.ts +107 -0
  64. package/dist/launch.js +396 -0
  65. package/dist/launch.js.map +1 -0
  66. package/dist/legacyHomeMigration.d.ts +1 -0
  67. package/dist/legacyHomeMigration.js +23 -0
  68. package/dist/legacyHomeMigration.js.map +1 -0
  69. package/dist/lineWriter.d.ts +9 -0
  70. package/dist/lineWriter.js +19 -0
  71. package/dist/lineWriter.js.map +1 -0
  72. package/dist/mdnsAdvertiser.d.ts +46 -0
  73. package/dist/mdnsAdvertiser.js +268 -0
  74. package/dist/mdnsAdvertiser.js.map +1 -0
  75. package/dist/observationLog.d.ts +54 -0
  76. package/dist/observationLog.js +108 -0
  77. package/dist/observationLog.js.map +1 -0
  78. package/dist/pairingCode.d.ts +60 -0
  79. package/dist/pairingCode.js +282 -0
  80. package/dist/pairingCode.js.map +1 -0
  81. package/dist/paths.d.ts +13 -0
  82. package/dist/paths.js +46 -0
  83. package/dist/paths.js.map +1 -0
  84. package/dist/permissionMode.d.ts +8 -0
  85. package/dist/permissionMode.js +32 -0
  86. package/dist/permissionMode.js.map +1 -0
  87. package/dist/planUsageFetcher.d.ts +35 -0
  88. package/dist/planUsageFetcher.js +172 -0
  89. package/dist/planUsageFetcher.js.map +1 -0
  90. package/dist/protocol.d.ts +329 -0
  91. package/dist/protocol.js +544 -0
  92. package/dist/protocol.js.map +1 -0
  93. package/dist/pushTokenCommand.d.ts +9 -0
  94. package/dist/pushTokenCommand.js +85 -0
  95. package/dist/pushTokenCommand.js.map +1 -0
  96. package/dist/pushTypes.d.ts +41 -0
  97. package/dist/pushTypes.js +68 -0
  98. package/dist/pushTypes.js.map +1 -0
  99. package/dist/sendLog.d.ts +14 -0
  100. package/dist/sendLog.js +64 -0
  101. package/dist/sendLog.js.map +1 -0
  102. package/dist/sessionActivityProvider.d.ts +7 -0
  103. package/dist/sessionActivityProvider.js +44 -0
  104. package/dist/sessionActivityProvider.js.map +1 -0
  105. package/dist/sessionIdleTracker.d.ts +19 -0
  106. package/dist/sessionIdleTracker.js +42 -0
  107. package/dist/sessionIdleTracker.js.map +1 -0
  108. package/dist/sessionListService.d.ts +27 -0
  109. package/dist/sessionListService.js +85 -0
  110. package/dist/sessionListService.js.map +1 -0
  111. package/dist/sessionMetadataStore.d.ts +33 -0
  112. package/dist/sessionMetadataStore.js +122 -0
  113. package/dist/sessionMetadataStore.js.map +1 -0
  114. package/dist/sessionSearch.d.ts +29 -0
  115. package/dist/sessionSearch.js +139 -0
  116. package/dist/sessionSearch.js.map +1 -0
  117. package/dist/setup.d.ts +83 -0
  118. package/dist/setup.js +412 -0
  119. package/dist/setup.js.map +1 -0
  120. package/dist/sleep.d.ts +4 -0
  121. package/dist/sleep.js +25 -0
  122. package/dist/sleep.js.map +1 -0
  123. package/dist/socketPath.d.ts +12 -0
  124. package/dist/socketPath.js +51 -0
  125. package/dist/socketPath.js.map +1 -0
  126. package/dist/subagentTailer.d.ts +15 -0
  127. package/dist/subagentTailer.js +399 -0
  128. package/dist/subagentTailer.js.map +1 -0
  129. package/dist/tmux.d.ts +62 -0
  130. package/dist/tmux.js +163 -0
  131. package/dist/tmux.js.map +1 -0
  132. package/dist/transcriptTailer.d.ts +61 -0
  133. package/dist/transcriptTailer.js +606 -0
  134. package/dist/transcriptTailer.js.map +1 -0
  135. package/dist/usageAggregator.d.ts +12 -0
  136. package/dist/usageAggregator.js +55 -0
  137. package/dist/usageAggregator.js.map +1 -0
  138. package/dist/version.d.ts +10 -0
  139. package/dist/version.js +45 -0
  140. package/dist/version.js.map +1 -0
  141. package/package.json +50 -0
@@ -0,0 +1,274 @@
1
+ // codexSessionStore.ts
2
+ // tailii (TS host) — codex(OpenAI Codex CLI)のマシン内会話一覧の導出(agent-tag / codex-sessions)
3
+ //
4
+ // claude の ClaudeSessionStore に対応する codex 版。`ClaudeSessionStore` が
5
+ // `~/.claude/projects/**.jsonl` を列挙するのに対し、codex は 2 ソースを突合する:
6
+ // 1. `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` の先頭 `session_meta` 行 → { id, cwd, timestamp }
7
+ // 2. `~/.codex/session_index.jsonl` の各行 → { id, thread_name(=タイトル), updated_at }
8
+ // session UUID(id) をキーに結合し、`ClaudeSessionInfo`(agent:"codex")へ落として updatedAt 降順で返す。
9
+ //
10
+ // タイトルは session_index の thread_name を優先。無ければ rollout 先頭付近の最初の実ユーザー
11
+ // メッセージ(event_msg/user_message の message)から導出する(codex アプリのスレッドタイトルと
12
+ // 同じ源。session_index は生成が遅延/欠落することがあり、id 先頭 8 字だとアプリと食い違うため)。
13
+ // どちらも取れなければ id 先頭 8 字へフォールバックする。cwd は rollout 側にのみ在る。
14
+ // 更新時刻は session_index の updated_at(無ければ rollout ファイル mtime)を Unix 秒で用いる。
15
+ import * as fs from "node:fs";
16
+ import * as os from "node:os";
17
+ import * as path from "node:path";
18
+ import { isInsideBase } from "./paths.js";
19
+ /** session_meta 先頭行を読む最大バイト数(先頭 1 行のみ使用。base_instructions を含み得るため広め)。 */
20
+ const META_READ_LIMIT_BYTES = 512 * 1024;
21
+ /** タイトルの最大長(claude と揃える)。 */
22
+ const TITLE_MAX_LENGTH = 60;
23
+ /** 走査する rollout の上限(mtime 新しい順)。大量セッション時の I/O 保護。 */
24
+ const DEFAULT_MAX_SESSIONS = 200;
25
+ /** 既定の codex ホーム(`~/.codex`)。 */
26
+ export function defaultCodexHome() {
27
+ return path.join(os.homedir(), ".codex");
28
+ }
29
+ /** codex のマシン内会話一覧を導出する値型サービス(agent-tag)。 */
30
+ export class CodexSessionStore {
31
+ sessionsRoot;
32
+ indexPath;
33
+ maxSessions;
34
+ /**
35
+ * @param home codex ホーム(既定 `~/.codex`)。テストは一時 dir を注入する。
36
+ * @param maxSessions 走査上限(既定 200、mtime 新しい順)。
37
+ */
38
+ constructor(home, maxSessions = DEFAULT_MAX_SESSIONS) {
39
+ const base = home ?? defaultCodexHome();
40
+ this.sessionsRoot = path.join(base, "sessions");
41
+ this.indexPath = path.join(base, "session_index.jsonl");
42
+ this.maxSessions = maxSessions;
43
+ }
44
+ /**
45
+ * codex 会話一覧を updatedAt 降順で返す(同値は sessionId 昇順で安定化)。
46
+ * `baseDir` 指定時は cwd が baseDir 自身/配下の会話のみに絞る(engine は無指定で呼ぶ)。
47
+ */
48
+ list(baseDir) {
49
+ const index = this.readIndex();
50
+ const files = this.listRollouts()
51
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
52
+ .slice(0, this.maxSessions);
53
+ let result = [];
54
+ const seen = new Set();
55
+ for (const file of files) {
56
+ const meta = readSessionMeta(file.path);
57
+ if (meta === null || meta.cwd === null)
58
+ continue;
59
+ const sessionId = meta.id ?? path.basename(file.path);
60
+ if (seen.has(sessionId))
61
+ continue;
62
+ seen.add(sessionId);
63
+ const idx = meta.id !== null ? index.get(meta.id) : undefined;
64
+ // thread_name が無ければ最初の実ユーザー発話からタイトルを作る(codex アプリ準拠)。
65
+ // それも取れなければ id 先頭 8 字。index にある会話は毎回のファイル走査を避けて thread_name を使う。
66
+ const title = idx?.title ?? readFirstUserMessage(file.path) ?? sessionId.slice(0, 8);
67
+ const updatedAt = idx?.updatedAt ?? (file.mtimeMs > 0 ? Math.floor(file.mtimeMs / 1000) : undefined);
68
+ const info = {
69
+ sessionId,
70
+ cwd: meta.cwd,
71
+ title,
72
+ agent: "codex",
73
+ };
74
+ if (updatedAt !== undefined)
75
+ info.updatedAt = updatedAt;
76
+ result.push(info);
77
+ }
78
+ if (baseDir) {
79
+ result = result.filter((info) => isInsideBase(info.cwd, baseDir));
80
+ }
81
+ return result.sort((lhs, rhs) => {
82
+ const l = lhs.updatedAt ?? Number.MIN_SAFE_INTEGER;
83
+ const r = rhs.updatedAt ?? Number.MIN_SAFE_INTEGER;
84
+ if (l !== r)
85
+ return r - l;
86
+ return lhs.sessionId < rhs.sessionId ? -1 : lhs.sessionId > rhs.sessionId ? 1 : 0;
87
+ });
88
+ }
89
+ /** `session_index.jsonl` を id → { title, updatedAt(秒) } に読む(無ければ空マップ)。 */
90
+ readIndex() {
91
+ const map = new Map();
92
+ let content;
93
+ try {
94
+ content = fs.readFileSync(this.indexPath, "utf8");
95
+ }
96
+ catch {
97
+ return map;
98
+ }
99
+ for (const line of content.split("\n")) {
100
+ if (!line)
101
+ continue;
102
+ let obj;
103
+ try {
104
+ obj = JSON.parse(line);
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ if (typeof obj !== "object" || obj === null)
110
+ continue;
111
+ const rec = obj;
112
+ const id = rec["id"];
113
+ if (typeof id !== "string" || id.length === 0)
114
+ continue;
115
+ let title = null;
116
+ if (typeof rec["thread_name"] === "string" && rec["thread_name"].length > 0) {
117
+ title = normalizeTitle(rec["thread_name"]);
118
+ }
119
+ let updatedAt = null;
120
+ if (typeof rec["updated_at"] === "string") {
121
+ const ms = Date.parse(rec["updated_at"]);
122
+ if (!Number.isNaN(ms))
123
+ updatedAt = Math.floor(ms / 1000);
124
+ }
125
+ map.set(id, { title, updatedAt });
126
+ }
127
+ return map;
128
+ }
129
+ /** rollout ファイル(`rollout-*.jsonl`)を再帰列挙する(path + mtimeMs)。 */
130
+ listRollouts() {
131
+ const out = [];
132
+ const walk = (dir) => {
133
+ let entries;
134
+ try {
135
+ entries = fs.readdirSync(dir, { withFileTypes: true });
136
+ }
137
+ catch {
138
+ return;
139
+ }
140
+ for (const entry of entries) {
141
+ const p = path.join(dir, entry.name);
142
+ if (entry.isDirectory()) {
143
+ walk(p);
144
+ }
145
+ else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.startsWith("rollout-")) {
146
+ let mtimeMs = 0;
147
+ try {
148
+ mtimeMs = fs.statSync(p).mtimeMs;
149
+ }
150
+ catch {
151
+ mtimeMs = 0;
152
+ }
153
+ out.push({ path: p, mtimeMs });
154
+ }
155
+ }
156
+ };
157
+ walk(this.sessionsRoot);
158
+ return out;
159
+ }
160
+ }
161
+ /** rollout の先頭 `session_meta` 行から { id, cwd, timestamp } を読む。読めなければ null。 */
162
+ function readSessionMeta(rolloutPath) {
163
+ let fd;
164
+ try {
165
+ fd = fs.openSync(rolloutPath, "r");
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ try {
171
+ const buf = Buffer.alloc(META_READ_LIMIT_BYTES);
172
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
173
+ if (bytesRead === 0)
174
+ return null;
175
+ const slice = buf.subarray(0, bytesRead);
176
+ const nl = slice.indexOf(0x0a);
177
+ const firstLine = (nl >= 0 ? slice.subarray(0, nl) : slice).toString("utf8");
178
+ const parsed = JSON.parse(firstLine);
179
+ if (typeof parsed !== "object" ||
180
+ parsed === null ||
181
+ parsed.type !== "session_meta") {
182
+ return null;
183
+ }
184
+ const payload = parsed.payload;
185
+ if (typeof payload !== "object" || payload === null)
186
+ return null;
187
+ const p = payload;
188
+ const id = typeof p["id"] === "string" && p["id"].length > 0 ? p["id"] : null;
189
+ const cwd = typeof p["cwd"] === "string" && p["cwd"].length > 0 ? p["cwd"] : null;
190
+ const timestamp = typeof p["timestamp"] === "string" ? p["timestamp"] : null;
191
+ return { id, cwd, timestamp };
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ finally {
197
+ try {
198
+ fs.closeSync(fd);
199
+ }
200
+ catch {
201
+ // close 失敗は無視。
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * rollout 先頭付近から最初の `event_msg` / `user_message` の message を読み、タイトル整形して返す。
207
+ *
208
+ * codex アプリはスレッドタイトルを最初のユーザー発話から作る。`user_message` イベントは
209
+ * 環境コンテキスト/AGENTS.md 等の注入を含まない「素のユーザー入力」なので、これをそのまま使える
210
+ * (`response_item/message/user` は注入込みなので使わない)。先頭 512KB のみ走査し、見つからなければ null。
211
+ */
212
+ function readFirstUserMessage(rolloutPath) {
213
+ let fd;
214
+ try {
215
+ fd = fs.openSync(rolloutPath, "r");
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ try {
221
+ const buf = Buffer.alloc(META_READ_LIMIT_BYTES);
222
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
223
+ if (bytesRead === 0)
224
+ return null;
225
+ const text = buf.subarray(0, bytesRead).toString("utf8");
226
+ for (const line of text.split("\n")) {
227
+ // 早期スキップ(大半の行は user_message を含まない)。末尾行は途中で切れ得るが
228
+ // その場合 JSON.parse が失敗して安全にスキップされる。
229
+ if (!line || !line.includes("user_message"))
230
+ continue;
231
+ let obj;
232
+ try {
233
+ obj = JSON.parse(line);
234
+ }
235
+ catch {
236
+ continue;
237
+ }
238
+ if (typeof obj !== "object" || obj === null)
239
+ continue;
240
+ if (obj.type !== "event_msg")
241
+ continue;
242
+ const payload = obj.payload;
243
+ if (typeof payload !== "object" || payload === null)
244
+ continue;
245
+ const pl = payload;
246
+ if (pl["type"] !== "user_message")
247
+ continue;
248
+ const message = pl["message"];
249
+ if (typeof message === "string" && message.trim().length > 0) {
250
+ return normalizeTitle(message);
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+ catch {
256
+ return null;
257
+ }
258
+ finally {
259
+ try {
260
+ fs.closeSync(fd);
261
+ }
262
+ catch {
263
+ // close 失敗は無視。
264
+ }
265
+ }
266
+ }
267
+ /** タイトルを 1 行・先頭 ~60 字へ整形する。 */
268
+ function normalizeTitle(raw) {
269
+ let text = raw.replaceAll("\n", " ").replaceAll("\r", " ").trim();
270
+ if (text.length > TITLE_MAX_LENGTH)
271
+ text = text.slice(0, TITLE_MAX_LENGTH);
272
+ return text;
273
+ }
274
+ //# sourceMappingURL=codexSessionStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexSessionStore.js","sourceRoot":"","sources":["../src/codexSessionStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type UsageTotals } from "./usageAggregator.js";
2
+ /** token_count から読める使用量 + レート制限(plan 相当)。 */
3
+ export interface CodexUsage extends UsageTotals {
4
+ /** モデルのコンテキスト窓(token)。未取得は undefined。 */
5
+ contextWindow?: number;
6
+ fiveHourUtilization?: number;
7
+ /** リセット時刻(ISO8601 文字列。usage_response は string で運ぶ)。 */
8
+ fiveHourResetsAt?: string;
9
+ sevenDayUtilization?: number;
10
+ sevenDayResetsAt?: string;
11
+ }
12
+ /**
13
+ * rollout の末尾から直近の `token_count` を 1 件読み、使用量へ落とす。
14
+ * 読めない/未出現なら空集計(全 0)を返す。
15
+ */
16
+ export declare function aggregateCodexUsage(rolloutPath: string): CodexUsage;
@@ -0,0 +1,144 @@
1
+ // codexUsage.ts
2
+ // tailii (TS host) — codex rollout の使用量集計(agent-tag / codex-input)
3
+ //
4
+ // codex は claude の transcript usage パーサと非互換。rollout の `event_msg/token_count` に
5
+ // 完全な使用量が載る(`info.total_token_usage`, `info.model_context_window`, `rate_limits`)。
6
+ // 直近(末尾)の token_count を 1 件読み、usage_response 相当の集計へ落とす。
7
+ //
8
+ // マップ:
9
+ // - inputTokens = total_token_usage.input_tokens
10
+ // - outputTokens = total_token_usage.output_tokens
11
+ // - cacheReadTokens = total_token_usage.cached_input_tokens
12
+ // - cacheCreationTokens= 0(codex は区別を持たない)
13
+ // - turns = 0(codex は assistant ターン数を token_count に持たない)
14
+ // - fiveHour*(5h窓=primary) / sevenDay*(週窓=secondary) の used_percent(0–100)・resets_at(Unix秒)
15
+ import * as fs from "node:fs";
16
+ import { emptyUsageTotals } from "./usageAggregator.js";
17
+ /** 末尾から読むバイト数上限(直近の token_count は末尾付近にある)。 */
18
+ const TAIL_READ_BYTES = 512 * 1024;
19
+ /**
20
+ * rollout の末尾から直近の `token_count` を 1 件読み、使用量へ落とす。
21
+ * 読めない/未出現なら空集計(全 0)を返す。
22
+ */
23
+ export function aggregateCodexUsage(rolloutPath) {
24
+ const line = readLastTokenCountLine(rolloutPath);
25
+ if (line === null)
26
+ return { ...emptyUsageTotals() };
27
+ let obj;
28
+ try {
29
+ obj = JSON.parse(line);
30
+ }
31
+ catch {
32
+ return { ...emptyUsageTotals() };
33
+ }
34
+ if (typeof obj !== "object" || obj === null)
35
+ return { ...emptyUsageTotals() };
36
+ const payload = obj.payload;
37
+ if (typeof payload !== "object" || payload === null)
38
+ return { ...emptyUsageTotals() };
39
+ const p = payload;
40
+ const result = { ...emptyUsageTotals() };
41
+ const info = p["info"];
42
+ if (typeof info === "object" && info !== null) {
43
+ const i = info;
44
+ const total = i["total_token_usage"];
45
+ if (typeof total === "object" && total !== null) {
46
+ const t = total;
47
+ result.inputTokens = numberOrZero(t["input_tokens"]);
48
+ result.outputTokens = numberOrZero(t["output_tokens"]);
49
+ result.cacheReadTokens = numberOrZero(t["cached_input_tokens"]);
50
+ }
51
+ const window = i["model_context_window"];
52
+ if (typeof window === "number" && window > 0)
53
+ result.contextWindow = window;
54
+ }
55
+ const rate = p["rate_limits"];
56
+ if (typeof rate === "object" && rate !== null) {
57
+ const r = rate;
58
+ applyWindow(r["primary"], (util, resets) => {
59
+ if (util !== undefined)
60
+ result.fiveHourUtilization = util;
61
+ if (resets !== undefined)
62
+ result.fiveHourResetsAt = resets;
63
+ });
64
+ applyWindow(r["secondary"], (util, resets) => {
65
+ if (util !== undefined)
66
+ result.sevenDayUtilization = util;
67
+ if (resets !== undefined)
68
+ result.sevenDayResetsAt = resets;
69
+ });
70
+ }
71
+ return result;
72
+ }
73
+ /** rate_limits の 1 窓(primary/secondary)から used_percent(丸め) と resets_at(ISO) を取り出す。 */
74
+ function applyWindow(raw, set) {
75
+ if (typeof raw !== "object" || raw === null)
76
+ return;
77
+ const w = raw;
78
+ const used = w["used_percent"];
79
+ const resets = w["resets_at"];
80
+ set(typeof used === "number" ? Math.round(used) : undefined,
81
+ // resets_at は Unix 秒。usage_response は ISO 文字列で運ぶため変換する。
82
+ typeof resets === "number" ? new Date(Math.floor(resets) * 1000).toISOString() : undefined);
83
+ }
84
+ /** ファイル末尾チャンクを読み、最後の `type=="event_msg"` かつ `payload.type=="token_count"` 行を返す。 */
85
+ function readLastTokenCountLine(rolloutPath) {
86
+ let fd;
87
+ let size;
88
+ try {
89
+ size = fs.statSync(rolloutPath).size;
90
+ fd = fs.openSync(rolloutPath, "r");
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ try {
96
+ const readLen = Math.min(size, TAIL_READ_BYTES);
97
+ const start = size - readLen;
98
+ const buf = Buffer.alloc(readLen);
99
+ const bytesRead = fs.readSync(fd, buf, 0, readLen, start);
100
+ const text = buf.subarray(0, bytesRead).toString("utf8");
101
+ const lines = text.split("\n");
102
+ for (let idx = lines.length - 1; idx >= 0; idx -= 1) {
103
+ const line = lines[idx];
104
+ if (line === undefined || line.length === 0)
105
+ continue;
106
+ // 末尾チャンクの先頭行は途中で切れ得るが、その行が token_count でパース可能なら採用、
107
+ // 不可なら次(より内側)へ進むため安全。
108
+ if (!line.includes("token_count"))
109
+ continue;
110
+ try {
111
+ const obj = JSON.parse(line);
112
+ if (typeof obj === "object" &&
113
+ obj !== null &&
114
+ obj.type === "event_msg") {
115
+ const payload = obj.payload;
116
+ if (typeof payload === "object" &&
117
+ payload !== null &&
118
+ payload.type === "token_count") {
119
+ return line;
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ finally {
133
+ try {
134
+ fs.closeSync(fd);
135
+ }
136
+ catch {
137
+ // close 失敗は無視。
138
+ }
139
+ }
140
+ }
141
+ function numberOrZero(v) {
142
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
143
+ }
144
+ //# sourceMappingURL=codexUsage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexUsage.js","sourceRoot":"","sources":["../src/codexUsage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { type ApnsHost } from "./pushTypes.js";
2
+ /** 保存する device token レコード(device-token.json の中身)。 */
3
+ export interface DeviceTokenRecord {
4
+ /** 宛先 device token(hex)。 */
5
+ token: string;
6
+ /** 登録環境(production / sandbox)。送信ホスト選択に使う。 */
7
+ environment: ApnsHost;
8
+ /** bundle id(`apns-topic`)。 */
9
+ bundleId: string;
10
+ /** 更新時刻(epoch 秒)。 */
11
+ updatedAt: number;
12
+ }
13
+ /**
14
+ * device token の永続化契約(load / save / delete)。
15
+ * APNs 送信側は load() で宛先を得て、`Unregistered` 受領時に delete() で宛先を除外する
16
+ * (3.3/3.4/3.5/6.3)。テストは実体を差し替え可能にするため interface 化。
17
+ */
18
+ export interface DeviceTokenStoring {
19
+ load(): DeviceTokenRecord | null;
20
+ save(record: DeviceTokenRecord): void;
21
+ delete(): void;
22
+ }
23
+ /**
24
+ * `~/.tailii/apns/device-token.json` へ device token を保存するストア。
25
+ * ディレクトリは 0700、ファイルは 0600 で保存する(Requirement 6.5)。
26
+ * テスト容易性のため base を注入できる。
27
+ */
28
+ export declare class DeviceTokenStore implements DeviceTokenStoring {
29
+ private readonly base;
30
+ constructor(base?: string);
31
+ private get fileURL();
32
+ /** 保存済み device token を読み込む。未保存・不正 JSON なら `null`(3.5)。 */
33
+ load(): DeviceTokenRecord | null;
34
+ /** device token を 0600 で保存する(同一パス上書き=token 置換, 3.4)。 */
35
+ save(record: DeviceTokenRecord): void;
36
+ /** 保存済み device token を削除する(宛先除外, 6.3)。未保存でも成功(冪等)。 */
37
+ delete(): void;
38
+ }
@@ -0,0 +1,73 @@
1
+ // deviceTokenStore.ts
2
+ // tailii (TS host) — device token の安全保存
3
+ // Swift 版 DeviceTokenStore.swift の移植。
4
+ //
5
+ // `~/.tailii/apns/device-token.json` に device token を保存 / 読込 / 削除する。
6
+ // token が変化したら上書き(3.4)、`Unregistered` 受領時は delete() で宛先を除外(6.3)。
7
+ // 未保存なら load() は null(送信スキップ判別, 3.5)。ファイルは 0600、ディレクトリは 0700。
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { defaultApnsBase, ensureApnsDirectory, isApnsHost } from "./pushTypes.js";
11
+ /**
12
+ * `~/.tailii/apns/device-token.json` へ device token を保存するストア。
13
+ * ディレクトリは 0700、ファイルは 0600 で保存する(Requirement 6.5)。
14
+ * テスト容易性のため base を注入できる。
15
+ */
16
+ export class DeviceTokenStore {
17
+ base;
18
+ constructor(base) {
19
+ this.base = base ?? defaultApnsBase();
20
+ }
21
+ get fileURL() {
22
+ return path.join(this.base, "device-token.json");
23
+ }
24
+ /** 保存済み device token を読み込む。未保存・不正 JSON なら `null`(3.5)。 */
25
+ load() {
26
+ let raw;
27
+ try {
28
+ raw = JSON.parse(fs.readFileSync(this.fileURL, "utf8"));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
34
+ return null;
35
+ const obj = raw;
36
+ if (typeof obj["token"] !== "string" ||
37
+ !isApnsHost(obj["environment"]) ||
38
+ typeof obj["bundleId"] !== "string" ||
39
+ typeof obj["updatedAt"] !== "number") {
40
+ return null;
41
+ }
42
+ return {
43
+ token: obj["token"],
44
+ environment: obj["environment"],
45
+ bundleId: obj["bundleId"],
46
+ updatedAt: obj["updatedAt"],
47
+ };
48
+ }
49
+ /** device token を 0600 で保存する(同一パス上書き=token 置換, 3.4)。 */
50
+ save(record) {
51
+ ensureApnsDirectory(this.base);
52
+ // キー順の非決定性を避けるため辞書順で符号化(Swift 版 .sortedKeys と同一)。
53
+ const payload = JSON.stringify({
54
+ bundleId: record.bundleId,
55
+ environment: record.environment,
56
+ token: record.token,
57
+ updatedAt: record.updatedAt,
58
+ });
59
+ fs.writeFileSync(this.fileURL, payload, { mode: 0o600 });
60
+ // 既存ファイルは writeFileSync の mode が効かないため明示的に 0600 を保証する。
61
+ fs.chmodSync(this.fileURL, 0o600);
62
+ }
63
+ /** 保存済み device token を削除する(宛先除外, 6.3)。未保存でも成功(冪等)。 */
64
+ delete() {
65
+ try {
66
+ fs.rmSync(this.fileURL);
67
+ }
68
+ catch {
69
+ // 未保存 = 何もしない(冪等)。他の削除失敗も送信停止判断を阻害しない。
70
+ }
71
+ }
72
+ }
73
+ //# sourceMappingURL=deviceTokenStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deviceTokenStore.js","sourceRoot":"","sources":["../src/deviceTokenStore.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,wCAAwC;AACxC,sCAAsC;AACtC,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,8DAA8D;AAE9D,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAiB,eAAe,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAyBjG;;;;GAIG;AACH,MAAM,OAAO,gBAAgB;IACV,IAAI,CAAS;IAE9B,YAAY,IAAa;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,eAAe,EAAE,CAAC;IACxC,CAAC;IAED,IAAY,OAAO;QACjB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;IACnD,CAAC;IAED,0DAA0D;IAC1D,IAAI;QACF,IAAI,GAAY,CAAC;QACjB,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAC/E,MAAM,GAAG,GAAG,GAA8B,CAAC;QAC3C,IACE,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ;YAChC,CAAC,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC/B,OAAO,GAAG,CAAC,UAAU,CAAC,KAAK,QAAQ;YACnC,OAAO,GAAG,CAAC,WAAW,CAAC,KAAK,QAAQ,EACpC,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC;YACnB,WAAW,EAAE,GAAG,CAAC,aAAa,CAAC;YAC/B,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC;YACzB,SAAS,EAAE,GAAG,CAAC,WAAW,CAAC;SAC5B,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,IAAI,CAAC,MAAyB;QAC5B,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,kDAAkD;QAClD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;YAC7B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC,CAAC;QACH,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACzD,uDAAuD;QACvD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,sDAAsD;IACtD,MAAM;QACJ,IAAI,CAAC;YACH,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,uCAAuC;QACzC,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ /** `baseDir/<partial の親>` 直下のサブディレクトリ名を prefix 一致で返す(base 外・不正は空)。 */
2
+ export declare function dirList(baseDir: string, partial: string): string[];
3
+ /** `baseDir` 配下限定で `relative` ディレクトリを作成する(base 外・不正・失敗は ok=false)。 */
4
+ export declare function dirCreate(baseDir: string, relative: string): {
5
+ path: string;
6
+ ok: boolean;
7
+ };
8
+ /** 絶対パス直下のサブディレクトリ名を非限定で列挙する(隠し dir とファイルは除外、ソート済み)。 */
9
+ export declare function dirChildren(absolutePath: string): string[];