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,61 @@
1
+ import { type ChatRole, type ControlMessage, type QuestionPromptQuestion, type ToolActivity } from "./protocol.js";
2
+ /** 履歴再生完了マーカーの streamId(iOS 側 `ChatLogModel` と対で解釈する)。 */
3
+ export declare const HISTORY_DONE_STREAM_ID = "pc:history-done";
4
+ /** 利用中モデル通知マーカーの streamId(iOS 側 `ChatLogModel` と対で解釈する)。 */
5
+ export declare const MODEL_STREAM_ID = "pc:model";
6
+ /** 現在コンテキストトークン数通知マーカーの streamId(iOS 側 `ChatLogModel` と対で解釈する)。 */
7
+ export declare const CONTEXT_STREAM_ID = "pc:context";
8
+ export interface TranscriptTailerOptions {
9
+ /** 追記ポーリング間隔(ms)。既定 50ms。 */
10
+ pollIntervalMs?: number;
11
+ /** tail 継続の最大 ms。null/未指定なら EOF で即終了(tail しない)。 */
12
+ tailDeadlineMs?: number | null;
13
+ /** EOF 後も abort まで無期限に tail するか。既定 false(本番 engine は true)。 */
14
+ tailIndefinitely?: boolean;
15
+ /** 初回 EOF で履歴再生完了マーカーを流すか。既定 false(セッション連動 tail のみ true)。 */
16
+ emitReplayDoneMarker?: boolean;
17
+ }
18
+ /** 抽出された 1 ターン。 */
19
+ interface Turn {
20
+ id: string | null;
21
+ role: ChatRole;
22
+ text: string;
23
+ toolActivities: ToolActivity[];
24
+ questionPrompts: {
25
+ id: string;
26
+ questions: QuestionPromptQuestion[];
27
+ }[];
28
+ toolResultIds: string[];
29
+ model: string | null;
30
+ contextTokens: number | null;
31
+ }
32
+ /** claude セッショントランスクリプト(JSONL)の tail 実装。 */
33
+ export declare class TranscriptTailer {
34
+ private readonly pollIntervalMs;
35
+ private readonly tailDeadlineMs;
36
+ private readonly tailIndefinitely;
37
+ private readonly emitReplayDoneMarker;
38
+ constructor(options?: TranscriptTailerOptions);
39
+ /** `path` の JSONL を頭から読み、assistant/user ターンを chat_output として流す。 */
40
+ streamTranscript(transcriptPath: string, signal?: AbortSignal): AsyncGenerator<ControlMessage, void, void>;
41
+ /**
42
+ * `projectDir`(`~/.claude/projects/<slug>/`)配下の対象 JSONL を解決して tail する。
43
+ * preferredSessionId 指定時は `<id>.jsonl` 優先、無指定は最新 mtime の *.jsonl。
44
+ * まだ無ければ出現をポーリングで待つ(無期限 tail は abort まで、上限 tail は deadline まで)。
45
+ */
46
+ streamProjectDir(dir: string, preferredSessionId: string | null, newerThanMs?: number | null, signal?: AbortSignal): AsyncGenerator<ControlMessage, void, void>;
47
+ /**
48
+ * `dir` 配下の対象 JSONL を解決する。
49
+ * preferredSessionId 指定時は「その会話 `<id>.jsonl` だけ」を対象にする(実在しなければ null で
50
+ * 出現待ちへフォールスルーし、mtime 最新の別会話へは決して吸着させない)。無指定時のみ最新 mtime の
51
+ * *.jsonl(newerThanMs 指定時は、それより後に更新されたものだけを候補にする)。無ければ null。
52
+ */
53
+ static resolveJsonl(dir: string, preferredSessionId: string | null, newerThanMs?: number | null): string | null;
54
+ /** 単一 JSONL ファイルを頭から読み、tail する共有ループ。 */
55
+ private runTail;
56
+ }
57
+ /** JSONL の 1 行から assistant/user ターンを寛容に抽出する(形式依存を 1 関数に集約)。 */
58
+ export declare function extractTurn(line: string): Turn | null;
59
+ /** content から tool_use ブロックを構造化表示データへ変換する。 */
60
+ export declare function extractToolActivities(content: unknown): ToolActivity[];
61
+ export {};
@@ -0,0 +1,606 @@
1
+ // transcriptTailer.ts
2
+ // tailii (TS host) — 会話出力キャプチャ(claude セッショントランスクリプト tail)
3
+ // Swift 版 TranscriptTailer.swift の移植。
4
+ // claude のセッショントランスクリプト(JSONL)を tail し、assistant/user ターンのテキストを
5
+ // `chat_output` デルタ(1 ターン = 1 streamId、確定で eof:true)として生成する。
6
+ // あわせて tool_use → tool_activity / AskUserQuestion → question_prompt / tool_result →
7
+ // question_dismiss / model 変化 → モデルマーカーを送出する。
8
+ // 抽出対象を text ブロックに限定し、秘密(接続鍵)を運ぶ経路を作らない(9.3)。
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { PROTOCOL_V1, } from "./protocol.js";
12
+ import { abortableSleep } from "./sleep.js";
13
+ /** 履歴再生完了マーカーの streamId(iOS 側 `ChatLogModel` と対で解釈する)。 */
14
+ export const HISTORY_DONE_STREAM_ID = "pc:history-done";
15
+ /** 利用中モデル通知マーカーの streamId(iOS 側 `ChatLogModel` と対で解釈する)。 */
16
+ export const MODEL_STREAM_ID = "pc:model";
17
+ /** 現在コンテキストトークン数通知マーカーの streamId(iOS 側 `ChatLogModel` と対で解釈する)。 */
18
+ export const CONTEXT_STREAM_ID = "pc:context";
19
+ const MAX_COMMAND_CHARACTERS = 8_000;
20
+ const MAX_DESCRIPTION_CHARACTERS = 2_000;
21
+ const MAX_DIFF_FIELD_CHARACTERS = 24_000;
22
+ /** TodoWrite チェックリストの最大項目数。 */
23
+ const MAX_TODO_ITEMS = 50;
24
+ /** TodoWrite 項目本文の最大文字数。 */
25
+ const MAX_TODO_CONTENT_CHARACTERS = 300;
26
+ /** claude セッショントランスクリプト(JSONL)の tail 実装。 */
27
+ export class TranscriptTailer {
28
+ pollIntervalMs;
29
+ tailDeadlineMs;
30
+ tailIndefinitely;
31
+ emitReplayDoneMarker;
32
+ constructor(options = {}) {
33
+ this.pollIntervalMs = options.pollIntervalMs ?? 50;
34
+ this.tailDeadlineMs = options.tailDeadlineMs ?? null;
35
+ this.tailIndefinitely = options.tailIndefinitely ?? false;
36
+ this.emitReplayDoneMarker = options.emitReplayDoneMarker ?? false;
37
+ }
38
+ /** `path` の JSONL を頭から読み、assistant/user ターンを chat_output として流す。 */
39
+ async *streamTranscript(transcriptPath, signal) {
40
+ yield* this.runTail(transcriptPath, signal);
41
+ }
42
+ /**
43
+ * `projectDir`(`~/.claude/projects/<slug>/`)配下の対象 JSONL を解決して tail する。
44
+ * preferredSessionId 指定時は `<id>.jsonl` 優先、無指定は最新 mtime の *.jsonl。
45
+ * まだ無ければ出現をポーリングで待つ(無期限 tail は abort まで、上限 tail は deadline まで)。
46
+ */
47
+ async *streamProjectDir(dir, preferredSessionId, newerThanMs = null, signal) {
48
+ const start = Date.now();
49
+ let resolved = null;
50
+ while (!signal?.aborted) {
51
+ resolved = TranscriptTailer.resolveJsonl(dir, preferredSessionId, newerThanMs);
52
+ if (resolved !== null)
53
+ break;
54
+ if (!this.tailIndefinitely) {
55
+ if (this.tailDeadlineMs === null || Date.now() - start >= this.tailDeadlineMs)
56
+ return;
57
+ }
58
+ await abortableSleep(this.pollIntervalMs, signal);
59
+ }
60
+ if (resolved !== null && !signal?.aborted) {
61
+ yield* this.runTail(resolved, signal);
62
+ }
63
+ }
64
+ /**
65
+ * `dir` 配下の対象 JSONL を解決する。
66
+ * preferredSessionId 指定時は「その会話 `<id>.jsonl` だけ」を対象にする(実在しなければ null で
67
+ * 出現待ちへフォールスルーし、mtime 最新の別会話へは決して吸着させない)。無指定時のみ最新 mtime の
68
+ * *.jsonl(newerThanMs 指定時は、それより後に更新されたものだけを候補にする)。無ければ null。
69
+ */
70
+ static resolveJsonl(dir, preferredSessionId, newerThanMs = null) {
71
+ // preferred はその会話に厳密束縛する。新規セッションは engine が生成した session-id を
72
+ // preferred として渡すため、同一 cwd に別の稼働会話があっても(その jsonl が mtime 最新でも)
73
+ // 取り違えない。未出現なら null を返し、呼び出し側のポーリングで自会話の出現を待つ。
74
+ if (preferredSessionId) {
75
+ const candidate = path.join(dir, `${preferredSessionId}.jsonl`);
76
+ return fs.existsSync(candidate) ? candidate : null;
77
+ }
78
+ let files;
79
+ try {
80
+ files = fs.readdirSync(dir);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ let best = null;
86
+ for (const file of files) {
87
+ if (!file.endsWith(".jsonl"))
88
+ continue;
89
+ const p = path.join(dir, file);
90
+ let mtimeMs;
91
+ try {
92
+ mtimeMs = fs.statSync(p).mtimeMs;
93
+ }
94
+ catch {
95
+ mtimeMs = 0;
96
+ }
97
+ if (newerThanMs !== null && mtimeMs <= newerThanMs)
98
+ continue;
99
+ if (best === null || mtimeMs > best.mtimeMs)
100
+ best = { path: p, mtimeMs };
101
+ }
102
+ return best?.path ?? null;
103
+ }
104
+ /** 単一 JSONL ファイルを頭から読み、tail する共有ループ。 */
105
+ async *runTail(transcriptPath, signal) {
106
+ let fd;
107
+ try {
108
+ fd = fs.openSync(transcriptPath, "r");
109
+ }
110
+ catch {
111
+ return;
112
+ }
113
+ try {
114
+ let position = 0;
115
+ let lineBuf = Buffer.alloc(0);
116
+ const state = {
117
+ seq: 0,
118
+ lastModel: null,
119
+ lastContextTokens: null,
120
+ activeQuestionIds: new Set(),
121
+ };
122
+ const start = Date.now();
123
+ let announcedReplayDone = false;
124
+ const chunk = Buffer.alloc(4096);
125
+ while (!signal?.aborted) {
126
+ let bytesRead = 0;
127
+ try {
128
+ bytesRead = fs.readSync(fd, chunk, 0, chunk.length, position);
129
+ }
130
+ catch {
131
+ bytesRead = 0;
132
+ }
133
+ if (bytesRead === 0) {
134
+ // 初回 EOF = 既存内容の再生完了。マーカー有効時は完了シグナルを 1 通流す。
135
+ if (this.emitReplayDoneMarker && !announcedReplayDone) {
136
+ announcedReplayDone = true;
137
+ if (lineBuf.length > 0) {
138
+ yield* emitLine(lineBuf, state);
139
+ lineBuf = Buffer.alloc(0);
140
+ }
141
+ yield {
142
+ type: "chat_output",
143
+ v: PROTOCOL_V1,
144
+ streamId: HISTORY_DONE_STREAM_ID,
145
+ role: "system",
146
+ text: "",
147
+ eof: true,
148
+ };
149
+ }
150
+ if (!this.tailIndefinitely) {
151
+ if (this.tailDeadlineMs === null || Date.now() - start >= this.tailDeadlineMs) {
152
+ if (lineBuf.length > 0)
153
+ yield* emitLine(lineBuf, state);
154
+ return;
155
+ }
156
+ }
157
+ await abortableSleep(this.pollIntervalMs, signal);
158
+ continue;
159
+ }
160
+ position += bytesRead;
161
+ lineBuf = Buffer.concat([lineBuf, chunk.subarray(0, bytesRead)]);
162
+ // 改行区切りで完結行を切り出して処理する。
163
+ let nl = lineBuf.indexOf(0x0a);
164
+ while (nl >= 0) {
165
+ const line = lineBuf.subarray(0, nl);
166
+ lineBuf = lineBuf.subarray(nl + 1);
167
+ yield* emitLine(line, state);
168
+ nl = lineBuf.indexOf(0x0a);
169
+ }
170
+ }
171
+ }
172
+ finally {
173
+ try {
174
+ fs.closeSync(fd);
175
+ }
176
+ catch {
177
+ // 二重 close 等は無視。
178
+ }
179
+ }
180
+ }
181
+ }
182
+ /** 1 行(JSONL)をパースし、生成メッセージを列挙する。解釈できない行はスキップ。 */
183
+ function* emitLine(line, state) {
184
+ const text = line.toString("utf8").replaceAll("\r", "");
185
+ if (!text)
186
+ return;
187
+ const turn = extractTurn(text);
188
+ if (turn === null)
189
+ return;
190
+ for (const prompt of turn.questionPrompts) {
191
+ if (state.activeQuestionIds.has(prompt.id))
192
+ continue;
193
+ state.activeQuestionIds.add(prompt.id);
194
+ yield { type: "question_prompt", v: PROTOCOL_V1, id: prompt.id, questions: prompt.questions };
195
+ }
196
+ for (const activity of turn.toolActivities) {
197
+ yield { type: "tool_activity", v: PROTOCOL_V1, activity };
198
+ }
199
+ for (const id of turn.toolResultIds) {
200
+ if (!state.activeQuestionIds.has(id))
201
+ continue;
202
+ state.activeQuestionIds.delete(id);
203
+ yield { type: "question_dismiss", v: PROTOCOL_V1, id };
204
+ }
205
+ // 利用中モデルの通知: assistant ターンの `message.model` が変わったらマーカーを 1 通流す。
206
+ // Claude Code が合成メッセージ(/remote-control 等の "No response requested.")へ記録する
207
+ // プレースホルダ "<synthetic>" は実利用モデルではないため通知しない。
208
+ if (turn.model !== null && !turn.model.startsWith("<") && turn.model !== state.lastModel) {
209
+ state.lastModel = turn.model;
210
+ yield {
211
+ type: "chat_output",
212
+ v: PROTOCOL_V1,
213
+ streamId: MODEL_STREAM_ID,
214
+ role: "system",
215
+ text: turn.model,
216
+ eof: true,
217
+ };
218
+ }
219
+ // 現在コンテキストトークン数の通知: assistant ターンの `message.usage` 合計が変わったらマーカーを 1 通流す。
220
+ if (turn.contextTokens !== null && turn.contextTokens !== state.lastContextTokens) {
221
+ state.lastContextTokens = turn.contextTokens;
222
+ yield {
223
+ type: "chat_output",
224
+ v: PROTOCOL_V1,
225
+ streamId: CONTEXT_STREAM_ID,
226
+ role: "system",
227
+ text: String(turn.contextTokens),
228
+ eof: true,
229
+ };
230
+ }
231
+ // テキストターン: 1 ターン = 1 chat_output(確定, eof:true)。
232
+ if (turn.text.length > 0) {
233
+ state.seq += 1;
234
+ const streamId = turn.id ?? `turn-${state.seq}`;
235
+ yield {
236
+ type: "chat_output",
237
+ v: PROTOCOL_V1,
238
+ streamId,
239
+ role: turn.role,
240
+ text: turn.text,
241
+ eof: true,
242
+ };
243
+ }
244
+ }
245
+ /** JSONL の 1 行から assistant/user ターンを寛容に抽出する(形式依存を 1 関数に集約)。 */
246
+ export function extractTurn(line) {
247
+ let obj;
248
+ try {
249
+ obj = JSON.parse(line);
250
+ }
251
+ catch {
252
+ return null;
253
+ }
254
+ if (typeof obj !== "object" || obj === null)
255
+ return null;
256
+ const rec = obj;
257
+ const message = typeof rec["message"] === "object" && rec["message"] !== null
258
+ ? rec["message"]
259
+ : null;
260
+ const roleStr = message?.["role"] ?? rec["type"];
261
+ let role;
262
+ if (roleStr === "assistant")
263
+ role = "assistant";
264
+ else if (roleStr === "user")
265
+ role = "user";
266
+ else
267
+ return null; // system / summary / 未知は対象外(text ターンに限定)
268
+ const id = (typeof rec["uuid"] === "string" ? rec["uuid"] : null) ??
269
+ (typeof rec["id"] === "string" ? rec["id"] : null);
270
+ const rawContent = message?.["content"] ?? rec["content"];
271
+ const text = extractText(rawContent);
272
+ const toolActivities = extractToolActivities(rawContent);
273
+ const questionPrompts = extractQuestionPrompts(rawContent, id);
274
+ const toolResultIds = extractToolResultIds(rawContent);
275
+ const model = typeof message?.["model"] === "string" ? message["model"] : null;
276
+ const contextTokens = role === "assistant" ? extractContextTokens(message?.["usage"]) : null;
277
+ return { id, role, text, toolActivities, questionPrompts, toolResultIds, model, contextTokens };
278
+ }
279
+ /** usage から現在コンテキスト相当のトークン数を合算する(未知フィールドは 0 扱い)。 */
280
+ function extractContextTokens(usage) {
281
+ if (typeof usage !== "object" || usage === null || Array.isArray(usage))
282
+ return null;
283
+ const rec = usage;
284
+ return (numberOrZero(rec["input_tokens"]) +
285
+ numberOrZero(rec["cache_read_input_tokens"]) +
286
+ numberOrZero(rec["cache_creation_input_tokens"]));
287
+ }
288
+ function numberOrZero(value) {
289
+ return typeof value === "number" ? value : 0;
290
+ }
291
+ /** content から text ブロックのみを連結する(tool_use/tool_result/image 等は無視)。 */
292
+ function extractText(content) {
293
+ if (typeof content === "string")
294
+ return content;
295
+ if (!Array.isArray(content))
296
+ return "";
297
+ const parts = [];
298
+ for (const block of content) {
299
+ if (typeof block !== "object" || block === null)
300
+ continue;
301
+ const rec = block;
302
+ if (rec["type"] !== "text")
303
+ continue;
304
+ if (typeof rec["text"] === "string")
305
+ parts.push(rec["text"]);
306
+ }
307
+ return parts.join("");
308
+ }
309
+ /** content から tool_use ブロックを構造化表示データへ変換する。 */
310
+ export function extractToolActivities(content) {
311
+ if (!Array.isArray(content))
312
+ return [];
313
+ const activities = [];
314
+ content.forEach((block, index) => {
315
+ if (typeof block !== "object" || block === null)
316
+ return;
317
+ const rec = block;
318
+ if (rec["type"] !== "tool_use")
319
+ return;
320
+ const name = rec["name"];
321
+ if (typeof name !== "string" || !name)
322
+ return;
323
+ const id = typeof rec["id"] === "string" ? rec["id"] : `tool-${index + 1}`;
324
+ const input = typeof rec["input"] === "object" && rec["input"] !== null && !Array.isArray(rec["input"])
325
+ ? rec["input"]
326
+ : {};
327
+ activities.push(makeToolActivity(id, name, input));
328
+ });
329
+ return activities;
330
+ }
331
+ function makeToolActivity(id, name, input) {
332
+ switch (name) {
333
+ case "Bash": {
334
+ const commandCap = cap(str(input["command"]), MAX_COMMAND_CHARACTERS);
335
+ const descriptionCap = cap(str(input["description"]), MAX_DESCRIPTION_CHARACTERS);
336
+ const summary = nonEmpty(descriptionCap.value) ?? commandPrefix(commandCap.value) ?? "Bash";
337
+ const activity = {
338
+ id,
339
+ name,
340
+ label: `実行済み ${summary}`,
341
+ commandTruncated: commandCap.truncated,
342
+ descriptionTruncated: descriptionCap.truncated,
343
+ };
344
+ if (commandCap.value !== null)
345
+ activity.command = commandCap.value;
346
+ if (descriptionCap.value !== null)
347
+ activity.description = descriptionCap.value;
348
+ return activity;
349
+ }
350
+ case "Edit":
351
+ case "MultiEdit": {
352
+ const file = filePath(input);
353
+ const oldCap = cap(str(input["old_string"]), MAX_DIFF_FIELD_CHARACTERS);
354
+ const newCap = cap(str(input["new_string"]), MAX_DIFF_FIELD_CHARACTERS);
355
+ const counts = lineDelta(str(input["old_string"]), str(input["new_string"]));
356
+ const diff = {
357
+ oldStringTruncated: oldCap.truncated,
358
+ newStringTruncated: newCap.truncated,
359
+ };
360
+ if (oldCap.value !== null)
361
+ diff.oldString = oldCap.value;
362
+ if (newCap.value !== null)
363
+ diff.newString = newCap.value;
364
+ const activity = {
365
+ id,
366
+ name,
367
+ label: `編集済み ${displayPath(file, name)}`,
368
+ addedLines: counts.added,
369
+ removedLines: counts.removed,
370
+ diff,
371
+ commandTruncated: false,
372
+ descriptionTruncated: false,
373
+ };
374
+ if (file !== null)
375
+ activity.file = file;
376
+ return activity;
377
+ }
378
+ case "Write":
379
+ case "NotebookEdit": {
380
+ const file = filePath(input);
381
+ const newText = str(input["content"]) ?? str(input["new_string"]);
382
+ const newCap = cap(newText, MAX_DIFF_FIELD_CHARACTERS);
383
+ const counts = lineDelta(null, newText);
384
+ const diff = { oldStringTruncated: false, newStringTruncated: newCap.truncated };
385
+ if (newCap.value !== null)
386
+ diff.newString = newCap.value;
387
+ const activity = {
388
+ id,
389
+ name,
390
+ label: `作成済み ${displayPath(file, name)}`,
391
+ addedLines: counts.added,
392
+ removedLines: counts.removed,
393
+ diff,
394
+ commandTruncated: false,
395
+ descriptionTruncated: false,
396
+ };
397
+ if (file !== null)
398
+ activity.file = file;
399
+ return activity;
400
+ }
401
+ case "Read": {
402
+ const file = filePath(input);
403
+ const activity = {
404
+ id,
405
+ name,
406
+ label: `既読 ${displayPath(file, name)}`,
407
+ commandTruncated: false,
408
+ descriptionTruncated: false,
409
+ };
410
+ if (file !== null)
411
+ activity.file = file;
412
+ return activity;
413
+ }
414
+ case "Grep":
415
+ case "Glob":
416
+ case "LS": {
417
+ const file = filePath(input);
418
+ const pattern = str(input["pattern"]) ?? str(input["path"]);
419
+ const summary = nonEmpty(pattern) ?? displayPath(file, name);
420
+ const activity = {
421
+ id,
422
+ name,
423
+ label: `検索済み ${summary}`,
424
+ commandTruncated: false,
425
+ descriptionTruncated: false,
426
+ };
427
+ if (file !== null)
428
+ activity.file = file;
429
+ return activity;
430
+ }
431
+ case "TodoWrite": {
432
+ const todos = extractTodos(input["todos"]);
433
+ const activity = {
434
+ id,
435
+ name,
436
+ label: "Todoを更新しました",
437
+ commandTruncated: false,
438
+ descriptionTruncated: false,
439
+ };
440
+ if (todos !== null)
441
+ activity.todos = todos;
442
+ return activity;
443
+ }
444
+ default: {
445
+ const file = filePath(input);
446
+ const activity = {
447
+ id,
448
+ name,
449
+ label: `実行済み ${displayPath(file, name)}`,
450
+ commandTruncated: false,
451
+ descriptionTruncated: false,
452
+ };
453
+ if (file !== null)
454
+ activity.file = file;
455
+ return activity;
456
+ }
457
+ }
458
+ }
459
+ function filePath(input) {
460
+ for (const key of ["file_path", "path", "notebook_path"]) {
461
+ const value = input[key];
462
+ if (typeof value === "string" && value.length > 0)
463
+ return value;
464
+ }
465
+ return null;
466
+ }
467
+ /** TodoWrite の todos 入力(`[{content, status, activeForm}]`)をチェックリストへ落とす。 */
468
+ function extractTodos(value) {
469
+ if (!Array.isArray(value) || value.length === 0)
470
+ return null;
471
+ const capped = [];
472
+ for (const item of value.slice(0, MAX_TODO_ITEMS)) {
473
+ if (typeof item !== "object" || item === null)
474
+ continue;
475
+ const rec = item;
476
+ const content = rec["content"];
477
+ if (typeof content !== "string" || !content)
478
+ continue;
479
+ const status = typeof rec["status"] === "string" ? rec["status"] : "pending";
480
+ capped.push({ content: cap(content, MAX_TODO_CONTENT_CHARACTERS).value ?? content, status });
481
+ }
482
+ return capped.length === 0 ? null : capped;
483
+ }
484
+ function displayPath(p, fallback) {
485
+ if (!p)
486
+ return fallback;
487
+ const base = path.basename(p);
488
+ return base.length === 0 ? p : base;
489
+ }
490
+ function nonEmpty(text) {
491
+ const trimmed = (text ?? "").trim();
492
+ return trimmed.length === 0 ? null : trimmed;
493
+ }
494
+ function commandPrefix(command) {
495
+ const trimmed = nonEmpty(command);
496
+ if (trimmed === null)
497
+ return null;
498
+ return trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed;
499
+ }
500
+ function cap(text, limit) {
501
+ if (text === null)
502
+ return { value: null, truncated: false };
503
+ if (text.length <= limit)
504
+ return { value: text, truncated: false };
505
+ return { value: text.slice(0, limit), truncated: true };
506
+ }
507
+ function lineDelta(oldText, newText) {
508
+ const oldLines = splitLines(oldText);
509
+ const newLines = splitLines(newText);
510
+ const oldCounts = new Map();
511
+ for (const line of oldLines)
512
+ oldCounts.set(line, (oldCounts.get(line) ?? 0) + 1);
513
+ let unchanged = 0;
514
+ for (const line of newLines) {
515
+ const count = oldCounts.get(line);
516
+ if (count !== undefined && count > 0) {
517
+ oldCounts.set(line, count - 1);
518
+ unchanged += 1;
519
+ }
520
+ }
521
+ return {
522
+ added: Math.max(0, newLines.length - unchanged),
523
+ removed: Math.max(0, oldLines.length - unchanged),
524
+ };
525
+ }
526
+ function splitLines(text) {
527
+ if (!text)
528
+ return [];
529
+ return text.split("\n");
530
+ }
531
+ function str(value) {
532
+ return typeof value === "string" ? value : null;
533
+ }
534
+ /** AskUserQuestion の tool_use 入力から、質問本文と選択肢だけを抽出する(他 tool の input は読まない)。 */
535
+ function extractQuestionPrompts(content, fallbackId) {
536
+ if (!Array.isArray(content))
537
+ return [];
538
+ const prompts = [];
539
+ for (const block of content) {
540
+ if (typeof block !== "object" || block === null)
541
+ continue;
542
+ const rec = block;
543
+ if (rec["type"] !== "tool_use" || rec["name"] !== "AskUserQuestion")
544
+ continue;
545
+ const input = rec["input"];
546
+ if (typeof input !== "object" || input === null)
547
+ continue;
548
+ const rawQuestions = input["questions"];
549
+ if (!Array.isArray(rawQuestions))
550
+ continue;
551
+ const questions = [];
552
+ for (const raw of rawQuestions) {
553
+ if (typeof raw !== "object" || raw === null)
554
+ continue;
555
+ const q = decodeQuestionPromptQuestion(raw);
556
+ if (q !== null)
557
+ questions.push(q);
558
+ }
559
+ if (questions.length === 0)
560
+ continue;
561
+ const id = (typeof rec["id"] === "string" ? rec["id"] : null) ??
562
+ fallbackId ??
563
+ `ask-user-question-${prompts.length + 1}`;
564
+ prompts.push({ id, questions });
565
+ }
566
+ return prompts;
567
+ }
568
+ function decodeQuestionPromptQuestion(raw) {
569
+ const question = raw["question"];
570
+ if (typeof question !== "string")
571
+ return null;
572
+ const header = typeof raw["header"] === "string" ? raw["header"] : "";
573
+ const multiSelect = raw["multiSelect"] === true;
574
+ const options = [];
575
+ if (Array.isArray(raw["options"])) {
576
+ for (const rawOption of raw["options"]) {
577
+ if (typeof rawOption !== "object" || rawOption === null)
578
+ continue;
579
+ const rec = rawOption;
580
+ const label = rec["label"];
581
+ if (typeof label !== "string")
582
+ continue;
583
+ options.push({
584
+ label,
585
+ description: typeof rec["description"] === "string" ? rec["description"] : "",
586
+ });
587
+ }
588
+ }
589
+ return { header, question, multiSelect, options };
590
+ }
591
+ function extractToolResultIds(content) {
592
+ if (!Array.isArray(content))
593
+ return [];
594
+ const ids = [];
595
+ for (const block of content) {
596
+ if (typeof block !== "object" || block === null)
597
+ continue;
598
+ const rec = block;
599
+ if (rec["type"] !== "tool_result")
600
+ continue;
601
+ if (typeof rec["tool_use_id"] === "string")
602
+ ids.push(rec["tool_use_id"]);
603
+ }
604
+ return ids;
605
+ }
606
+ //# sourceMappingURL=transcriptTailer.js.map