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,85 @@
1
+ // pushTokenCommand.ts
2
+ // tailii (TS host) — push-token サブコマンド実装
3
+ // Swift 版 PushTokenCommand.swift の移植。
4
+ //
5
+ // iOS が APNs 登録で得た device token を SSH exec 経由で Mac に受け渡す
6
+ // (`tailii push-token`)。stdin から1行 JSON `{token, environment, bundleId, updatedAt}`
7
+ // を読み、検証(token=hex / environment=production|sandbox / 必須フィールド存在)後に
8
+ // DeviceTokenStore.save で `~/.tailii/apns/device-token.json` に永続化する(3.2/3.3)。
9
+ //
10
+ // 妥当 JSON → exit 0、不正入力 → 非0 exit + stderr メッセージ、書き込みは行わない。
11
+ // token 変化時は上書きで置換される(3.4)。device token は秘匿値として stdout に出さない。
12
+ import { DeviceTokenStore } from "./deviceTokenStore.js";
13
+ import { isApnsHost } from "./pushTypes.js";
14
+ /**
15
+ * push-token の純ロジック。DeviceTokenStore を注入できるためテスト可能。
16
+ * stdin バイト列を JSON として解釈し、token(非空 hex) / environment / bundleId / updatedAt を
17
+ * 検証してから store.save する。いずれか不正なら書き込まず errorSink にメッセージを出して非0。
18
+ */
19
+ export function pushTokenCore(stdinData, store, errorSink = (m) => process.stderr.write(m)) {
20
+ // --- JSON を辞書として解釈 ---
21
+ let obj;
22
+ try {
23
+ obj = JSON.parse(stdinData.toString("utf8"));
24
+ }
25
+ catch {
26
+ errorSink("tailii push-token: stdin が有効な JSON オブジェクトではありません\n");
27
+ return 1;
28
+ }
29
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
30
+ errorSink("tailii push-token: stdin が有効な JSON オブジェクトではありません\n");
31
+ return 1;
32
+ }
33
+ const dict = obj;
34
+ // --- token: 非空 hex 文字列 ---
35
+ const token = dict["token"];
36
+ if (typeof token !== "string" || token.length === 0) {
37
+ errorSink("tailii push-token: 'token' が欠落または空です\n");
38
+ return 1;
39
+ }
40
+ if (!/^[0-9a-fA-F]+$/.test(token)) {
41
+ errorSink("tailii push-token: 'token' は hex 文字列である必要があります\n");
42
+ return 1;
43
+ }
44
+ // --- environment: production | sandbox ---
45
+ const environment = dict["environment"];
46
+ if (typeof environment !== "string") {
47
+ errorSink("tailii push-token: 'environment' が欠落しています\n");
48
+ return 1;
49
+ }
50
+ if (!isApnsHost(environment)) {
51
+ errorSink("tailii push-token: 'environment' は production|sandbox のいずれかである必要があります\n");
52
+ return 1;
53
+ }
54
+ // --- bundleId: 非空文字列 ---
55
+ const bundleId = dict["bundleId"];
56
+ if (typeof bundleId !== "string" || bundleId.length === 0) {
57
+ errorSink("tailii push-token: 'bundleId' が欠落または空です\n");
58
+ return 1;
59
+ }
60
+ // --- updatedAt: 整数(epoch 秒) ---
61
+ const updatedAtRaw = dict["updatedAt"];
62
+ if (typeof updatedAtRaw !== "number" || !Number.isFinite(updatedAtRaw)) {
63
+ errorSink("tailii push-token: 'updatedAt' が欠落または数値ではありません\n");
64
+ return 1;
65
+ }
66
+ const updatedAt = Math.trunc(updatedAtRaw);
67
+ // --- 保存(同一パス上書き=token 置換, 3.4)。token は stdout に出さない(6.5 パリティ) ---
68
+ try {
69
+ store.save({ token, environment, bundleId, updatedAt });
70
+ }
71
+ catch (error) {
72
+ errorSink(`tailii push-token: device token の保存に失敗しました: ${String(error)}\n`);
73
+ return 1;
74
+ }
75
+ return 0;
76
+ }
77
+ /** push-token サブコマンドの CLI エントリポイント。stdin を全読みして pushTokenCore へ渡す。 */
78
+ export async function runPushTokenCommand(_args) {
79
+ const chunks = [];
80
+ for await (const chunk of process.stdin) {
81
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
82
+ }
83
+ return pushTokenCore(Buffer.concat(chunks), new DeviceTokenStore());
84
+ }
85
+ //# sourceMappingURL=pushTokenCommand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pushTokenCommand.js","sourceRoot":"","sources":["../src/pushTokenCommand.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,yCAAyC;AACzC,sCAAsC;AACtC,EAAE;AACF,yDAAyD;AACzD,oFAAoF;AACpF,mEAAmE;AACnE,8EAA8E;AAC9E,EAAE;AACF,4DAA4D;AAC5D,8DAA8D;AAE9D,OAAO,EAAE,gBAAgB,EAA2B,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,SAAiB,EACjB,KAAyB,EACzB,YAAuC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAErE,wBAAwB;IACxB,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,SAAS,CAAC,oDAAoD,CAAC,CAAC;QAChE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,SAAS,CAAC,oDAAoD,CAAC,CAAC;QAChE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,IAAI,GAAG,GAA8B,CAAC;IAE5C,4BAA4B;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,SAAS,CAAC,wCAAwC,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAClC,SAAS,CAAC,kDAAkD,CAAC,CAAC;QAC9D,OAAO,CAAC,CAAC;IACX,CAAC;IAED,4CAA4C;IAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC;IACxC,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpC,SAAS,CAAC,6CAA6C,CAAC,CAAC;QACzD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,SAAS,CACP,yEAAyE,CAC1E,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,0BAA0B;IAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;IAClC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1D,SAAS,CAAC,2CAA2C,CAAC,CAAC;QACvD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,iCAAiC;IACjC,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IACvC,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACvE,SAAS,CAAC,kDAAkD,CAAC,CAAC;QAC9D,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAE3C,mEAAmE;IACnE,IAAI,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;IAC1D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,SAAS,CAAC,+CAA+C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5E,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,CAAC;AACX,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAAe;IACvD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,gBAAgB,EAAE,CAAC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * APNs 送信先の環境。device token の登録環境(DEBUG=sandbox / RELEASE=production)に
3
+ * 由来し、送信ホストの選択に用いる。JSON 上は `"production"` / `"sandbox"` の文字列。
4
+ */
5
+ export type ApnsHost = "production" | "sandbox";
6
+ /** 当該環境の APNs ホスト名(token の environment から送信ホストを写像)。 */
7
+ export declare function apnsSendHost(environment: ApnsHost): string;
8
+ /** 文字列が有効な `ApnsHost` か判定する。 */
9
+ export declare function isApnsHost(value: unknown): value is ApnsHost;
10
+ /**
11
+ * APNs 送信のエラー種別(APNs レスポンスの reason を分類して写像する)。
12
+ * 呼び出し側はこれをもとに再試行・宛先除外を判断する(Requirement 6.2, 6.3)。
13
+ */
14
+ export type PushError = {
15
+ kind: "expiredProviderToken";
16
+ } | {
17
+ kind: "unregistered";
18
+ } | {
19
+ kind: "tooManyProviderTokenUpdates";
20
+ } | {
21
+ kind: "transport";
22
+ detail: string;
23
+ } | {
24
+ kind: "http";
25
+ status: number;
26
+ reason: string;
27
+ };
28
+ /** 2 つの `PushError` が同値か判定する(テスト・比較用)。 */
29
+ export declare function pushErrorEquals(a: PushError, b: PushError): boolean;
30
+ /** デフォルトの APNs ディレクトリ(`~/.tailii/apns`)。 */
31
+ export declare function defaultApnsBase(): string;
32
+ /** APNs ディレクトリを `0700` で用意する(冪等、非ディレクトリ衝突は例外)。 */
33
+ export declare function ensureApnsDirectory(base: string): void;
34
+ /**
35
+ * APNs リクエスト本文(最小 aps + カスタムキー)を JSON エンコードして返す。
36
+ *
37
+ * payload には承認判断へ導く最小情報(tool 名・セッション・approvalId)のみを含め、
38
+ * 承認内容の詳細本文・diff・cwd 全文・秘密は載せない(Requirement 2.3 / 6.5)。
39
+ * キー順の非決定性を避けるため辞書順で符号化する(Swift 版 .sortedKeys と同一)。
40
+ */
41
+ export declare function pushPayloadBody(approvalId: string, tool: string, session: string): Buffer;
@@ -0,0 +1,68 @@
1
+ // pushTypes.ts
2
+ // tailii (TS host) — Push レイヤ共通型・保存レイアウト
3
+ // Swift 版 PushPayload.swift(ApnsHost / PushError / PushPayload / ApnsStorage)の移植。
4
+ //
5
+ // 秘密(.p8/token)・diff・cwd 本文を payload/ログに載せない(Requirement 6.5, 2.3)。
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import { ensureDirectory0700 } from "./paths.js";
9
+ /** 当該環境の APNs ホスト名(token の environment から送信ホストを写像)。 */
10
+ export function apnsSendHost(environment) {
11
+ return environment === "production" ? "api.push.apple.com" : "api.sandbox.push.apple.com";
12
+ }
13
+ /** 文字列が有効な `ApnsHost` か判定する。 */
14
+ export function isApnsHost(value) {
15
+ return value === "production" || value === "sandbox";
16
+ }
17
+ /** 2 つの `PushError` が同値か判定する(テスト・比較用)。 */
18
+ export function pushErrorEquals(a, b) {
19
+ if (a.kind !== b.kind)
20
+ return false;
21
+ if (a.kind === "transport" && b.kind === "transport")
22
+ return a.detail === b.detail;
23
+ if (a.kind === "http" && b.kind === "http")
24
+ return a.status === b.status && a.reason === b.reason;
25
+ return true;
26
+ }
27
+ // MARK: - ApnsStorage(保存ディレクトリ規約)
28
+ /** デフォルトの APNs ディレクトリ(`~/.tailii/apns`)。 */
29
+ export function defaultApnsBase() {
30
+ return path.join(os.homedir(), ".tailii", "apns");
31
+ }
32
+ /** APNs ディレクトリを `0700` で用意する(冪等、非ディレクトリ衝突は例外)。 */
33
+ export function ensureApnsDirectory(base) {
34
+ ensureDirectory0700(base);
35
+ }
36
+ // MARK: - PushPayload
37
+ /**
38
+ * APNs リクエスト本文(最小 aps + カスタムキー)を JSON エンコードして返す。
39
+ *
40
+ * payload には承認判断へ導く最小情報(tool 名・セッション・approvalId)のみを含め、
41
+ * 承認内容の詳細本文・diff・cwd 全文・秘密は載せない(Requirement 2.3 / 6.5)。
42
+ * キー順の非決定性を避けるため辞書順で符号化する(Swift 版 .sortedKeys と同一)。
43
+ */
44
+ export function pushPayloadBody(approvalId, tool, session) {
45
+ const alert = { body: `${tool} · ${session}`, title: "承認待ち" };
46
+ const aps = {
47
+ alert,
48
+ "interruption-level": "time-sensitive",
49
+ sound: "default",
50
+ "thread-id": session,
51
+ };
52
+ const root = { approvalId, aps, session, tool };
53
+ return Buffer.from(JSON.stringify(sortDeep(root)), "utf8");
54
+ }
55
+ /** ネストした plain object のキーを辞書順に並べ替える(.sortedKeys 相当)。 */
56
+ function sortDeep(value) {
57
+ if (Array.isArray(value))
58
+ return value.map(sortDeep);
59
+ if (value !== null && typeof value === "object") {
60
+ const obj = value;
61
+ const sorted = {};
62
+ for (const key of Object.keys(obj).sort())
63
+ sorted[key] = sortDeep(obj[key]);
64
+ return sorted;
65
+ }
66
+ return value;
67
+ }
68
+ //# sourceMappingURL=pushTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pushTypes.js","sourceRoot":"","sources":["../src/pushTypes.ts"],"names":[],"mappings":"AAAA,eAAe;AACf,yCAAyC;AACzC,kFAAkF;AAClF,EAAE;AACF,oEAAoE;AAEpE,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAUjD,uDAAuD;AACvD,MAAM,UAAU,YAAY,CAAC,WAAqB;IAChD,OAAO,WAAW,KAAK,YAAY,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,4BAA4B,CAAC;AAC5F,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,KAAK,KAAK,YAAY,IAAI,KAAK,KAAK,SAAS,CAAC;AACvD,CAAC;AAeD,0CAA0C;AAC1C,MAAM,UAAU,eAAe,CAAC,CAAY,EAAE,CAAY;IACxD,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW;QAAE,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC;IACnF,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC;IAClG,OAAO,IAAI,CAAC;AACd,CAAC;AAED,kCAAkC;AAElC,4CAA4C;AAC5C,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,mBAAmB,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,sBAAsB;AAEtB;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,IAAY,EAAE,OAAe;IAC/E,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,MAAM,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC9D,MAAM,GAAG,GAAG;QACV,KAAK;QACL,oBAAoB,EAAE,gBAAgB;QACtC,KAAK,EAAE,SAAS;QAChB,WAAW,EAAE,OAAO;KACrB,CAAC;IACF,MAAM,IAAI,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAChD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED,wDAAwD;AACxD,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5E,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,14 @@
1
+ /** セッション別の最終送信時刻を永続する(バースト抑制の最小間隔判定, 7.2)。 */
2
+ export declare class SendLog {
3
+ private readonly base;
4
+ constructor(base?: string);
5
+ private get fileURL();
6
+ /** 当該セッションの最終送信時刻(Unix 秒)を返す。記録が無ければ `null`。 */
7
+ lastSent(session: string): number | null;
8
+ /** 当該セッションの最終送信時刻を更新する(送信成功時に呼ぶ)。 */
9
+ record(session: string, at: number): void;
10
+ /** send-log.json を読み込む。欠落・不正 JSON なら空辞書。 */
11
+ private load;
12
+ /** send-log.json を 0600 で保存する(ディレクトリは 0700)。保存失敗は握り潰す。 */
13
+ private save;
14
+ }
@@ -0,0 +1,64 @@
1
+ // sendLog.ts
2
+ // tailii (TS host) — バースト抑制のセッション別レート制限
3
+ // Swift 版 SendLog.swift の移植。
4
+ //
5
+ // `~/.tailii/apns/send-log.json`(`{ "<session>": lastSentEpoch }`)を読み書きする。
6
+ // hook プロセスは短命かつ複数起動しうるため、直近送信時刻はファイルで横断共有する(7.2)。
7
+ // 読み書き失敗は握り潰し、判定は「送信を許可する側(=抑制しない)」に倒す(best-effort)。
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { defaultApnsBase, ensureApnsDirectory } from "./pushTypes.js";
11
+ /** セッション別の最終送信時刻を永続する(バースト抑制の最小間隔判定, 7.2)。 */
12
+ export class SendLog {
13
+ base;
14
+ constructor(base) {
15
+ this.base = base ?? defaultApnsBase();
16
+ }
17
+ get fileURL() {
18
+ return path.join(this.base, "send-log.json");
19
+ }
20
+ /** 当該セッションの最終送信時刻(Unix 秒)を返す。記録が無ければ `null`。 */
21
+ lastSent(session) {
22
+ const value = this.load()[session];
23
+ return typeof value === "number" ? value : null;
24
+ }
25
+ /** 当該セッションの最終送信時刻を更新する(送信成功時に呼ぶ)。 */
26
+ record(session, at) {
27
+ const map = this.load();
28
+ map[session] = at;
29
+ this.save(map);
30
+ }
31
+ /** send-log.json を読み込む。欠落・不正 JSON なら空辞書。 */
32
+ load() {
33
+ let raw;
34
+ try {
35
+ raw = JSON.parse(fs.readFileSync(this.fileURL, "utf8"));
36
+ }
37
+ catch {
38
+ return {};
39
+ }
40
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
41
+ return {};
42
+ const result = {};
43
+ for (const [key, value] of Object.entries(raw)) {
44
+ if (typeof value === "number")
45
+ result[key] = value;
46
+ }
47
+ return result;
48
+ }
49
+ /** send-log.json を 0600 で保存する(ディレクトリは 0700)。保存失敗は握り潰す。 */
50
+ save(map) {
51
+ try {
52
+ ensureApnsDirectory(this.base);
53
+ const sorted = {};
54
+ for (const key of Object.keys(map).sort())
55
+ sorted[key] = map[key];
56
+ fs.writeFileSync(this.fileURL, JSON.stringify(sorted), { mode: 0o600 });
57
+ fs.chmodSync(this.fileURL, 0o600);
58
+ }
59
+ catch {
60
+ // 保存失敗は致命ではない(次回も送信できる)。
61
+ }
62
+ }
63
+ }
64
+ //# sourceMappingURL=sendLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sendLog.js","sourceRoot":"","sources":["../src/sendLog.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,wCAAwC;AACxC,6BAA6B;AAC7B,EAAE;AACF,2EAA2E;AAC3E,mDAAmD;AACnD,qDAAqD;AAErD,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAEtE,8CAA8C;AAC9C,MAAM,OAAO,OAAO;IACD,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,eAAe,CAAC,CAAC;IAC/C,CAAC;IAED,gDAAgD;IAChD,QAAQ,CAAC,OAAe;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC;QACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAClD,CAAC;IAED,qCAAqC;IACrC,MAAM,CAAC,OAAe,EAAE,EAAU;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjB,CAAC;IAED,4CAA4C;IACpC,IAAI;QACV,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,EAAE,CAAC;QACZ,CAAC;QACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QAC7E,MAAM,MAAM,GAA2B,EAAE,CAAC;QAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAA8B,CAAC,EAAE,CAAC;YAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACrD,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,0DAA0D;IAClD,IAAI,CAAC,GAA2B;QACtC,IAAI,CAAC;YACH,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,MAAM,GAA2B,EAAE,CAAC;YAC1C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE;gBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAE,CAAC;YACnE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACxE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import type { SessionInfo } from "./protocol.js";
2
+ /** セッションの更新時刻(Unix 秒)を解決する注入可能な抽象。解決不能は null。 */
3
+ export type SessionActivityProvider = (session: SessionInfo) => number | null;
4
+ /** cwd を Claude Code projects の slug へ写す(`/` と `.` を `-` に置換)。 */
5
+ export declare function activitySlugForCwd(cwd: string): string;
6
+ /** Claude Code トランスクリプトの最新 mtime を updatedAt として返す既定実装。 */
7
+ export declare function claudeTranscriptActivityProvider(projectsBase?: string): SessionActivityProvider;
@@ -0,0 +1,44 @@
1
+ // sessionActivityProvider.ts
2
+ // tailii (TS host) — セッション更新時刻の権威解決(session-list-lifecycle 1.3/1.4)
3
+ // Swift 版 SessionActivityProvider.swift の移植。
4
+ // updatedAt は Claude Code トランスクリプト(`~/.claude/projects/<slug>/*.jsonl`)の最大 mtime。
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import { standardize } from "./paths.js";
9
+ /** cwd を Claude Code projects の slug へ写す(`/` と `.` を `-` に置換)。 */
10
+ export function activitySlugForCwd(cwd) {
11
+ return standardize(cwd).replaceAll("/", "-").replaceAll(".", "-");
12
+ }
13
+ /** Claude Code トランスクリプトの最新 mtime を updatedAt として返す既定実装。 */
14
+ export function claudeTranscriptActivityProvider(projectsBase) {
15
+ const base = projectsBase ?? path.join(os.homedir(), ".claude", "projects");
16
+ return (session) => {
17
+ const cwd = session.cwd;
18
+ if (!cwd)
19
+ return null;
20
+ const dir = path.join(base, activitySlugForCwd(cwd));
21
+ let entries;
22
+ try {
23
+ entries = fs.readdirSync(dir);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ let latest = null;
29
+ for (const entry of entries) {
30
+ if (!entry.endsWith(".jsonl") || entry.startsWith("."))
31
+ continue;
32
+ try {
33
+ const secs = Math.floor(fs.statSync(path.join(dir, entry)).mtimeMs / 1000);
34
+ if (latest === null || secs > latest)
35
+ latest = secs;
36
+ }
37
+ catch {
38
+ // 個別ファイルの stat 失敗はスキップ。
39
+ }
40
+ }
41
+ return latest;
42
+ };
43
+ }
44
+ //# sourceMappingURL=sessionActivityProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionActivityProvider.js","sourceRoot":"","sources":["../src/sessionActivityProvider.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,oEAAoE;AACpE,6CAA6C;AAC7C,kFAAkF;AAElF,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAKzC,kEAAkE;AAClE,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACpE,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,gCAAgC,CAAC,YAAqB;IACpE,MAAM,IAAI,GAAG,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IAC5E,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;QACxB,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;QACrD,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YACjE,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;gBAC3E,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,GAAG,MAAM;oBAAE,MAAM,GAAG,IAAI,CAAC;YACtD,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ /** セッションのアイドル起点を記録し、timeout 超過集合を返すトラッカー。 */
2
+ export declare class SessionIdleTracker {
3
+ private readonly timeout;
4
+ /** name → アイドル起点(Unix 秒)。未設定=アクティブ/対象外。 */
5
+ private idleSince;
6
+ constructor(timeout: number);
7
+ /** 設定中の timeout(秒)。 */
8
+ get timeoutSeconds(): number;
9
+ /** アイドル起点を設定する(chat 離脱, 要件 4.2)。 */
10
+ markIdle(name: string, at: number): void;
11
+ /** アイドル起点を解除してアクティブ化する(reattach, 要件 4.4)。 */
12
+ markActive(name: string): void;
13
+ /** kill 後にエントリを掃除する(二重 kill 回避)。 */
14
+ remove(name: string): void;
15
+ /** `now - idleSince >= timeout` を満たす name 集合(name 昇順、未設定は除外, 4.3/4.5)。 */
16
+ expired(now: number): string[];
17
+ /** 現在アイドル起点が設定されている name 集合(テスト/検証補助)。 */
18
+ idleNames(): string[];
19
+ }
@@ -0,0 +1,42 @@
1
+ // sessionIdleTracker.ts
2
+ // tailii (TS host) — tmux アイドル起点の計時(session-list-lifecycle 4.x)
3
+ // Swift 版 SessionIdleTracker.swift の移植(Node は単一スレッドのため actor は不要)。
4
+ /** セッションのアイドル起点を記録し、timeout 超過集合を返すトラッカー。 */
5
+ export class SessionIdleTracker {
6
+ timeout;
7
+ /** name → アイドル起点(Unix 秒)。未設定=アクティブ/対象外。 */
8
+ idleSince = new Map();
9
+ constructor(timeout) {
10
+ this.timeout = timeout;
11
+ }
12
+ /** 設定中の timeout(秒)。 */
13
+ get timeoutSeconds() {
14
+ return this.timeout;
15
+ }
16
+ /** アイドル起点を設定する(chat 離脱, 要件 4.2)。 */
17
+ markIdle(name, at) {
18
+ this.idleSince.set(name, at);
19
+ }
20
+ /** アイドル起点を解除してアクティブ化する(reattach, 要件 4.4)。 */
21
+ markActive(name) {
22
+ this.idleSince.delete(name);
23
+ }
24
+ /** kill 後にエントリを掃除する(二重 kill 回避)。 */
25
+ remove(name) {
26
+ this.idleSince.delete(name);
27
+ }
28
+ /** `now - idleSince >= timeout` を満たす name 集合(name 昇順、未設定は除外, 4.3/4.5)。 */
29
+ expired(now) {
30
+ const result = [];
31
+ for (const [name, since] of this.idleSince) {
32
+ if (now - since >= this.timeout)
33
+ result.push(name);
34
+ }
35
+ return result.sort();
36
+ }
37
+ /** 現在アイドル起点が設定されている name 集合(テスト/検証補助)。 */
38
+ idleNames() {
39
+ return [...this.idleSince.keys()].sort();
40
+ }
41
+ }
42
+ //# sourceMappingURL=sessionIdleTracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionIdleTracker.js","sourceRoot":"","sources":["../src/sessionIdleTracker.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,gEAAgE;AAChE,mEAAmE;AAEnE,6CAA6C;AAC7C,MAAM,OAAO,kBAAkB;IAIA;IAH7B,2CAA2C;IACnC,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE9C,YAA6B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;IAEhD,uBAAuB;IACvB,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,oCAAoC;IACpC,QAAQ,CAAC,IAAY,EAAE,EAAU;QAC/B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC/B,CAAC;IAED,6CAA6C;IAC7C,UAAU,CAAC,IAAY;QACrB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,oCAAoC;IACpC,MAAM,CAAC,IAAY;QACjB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,0EAA0E;IAC1E,OAAO,CAAC,GAAW;QACjB,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3C,IAAI,GAAG,GAAG,KAAK,IAAI,IAAI,CAAC,OAAO;gBAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;IAED,0CAA0C;IAC1C,SAAS;QACP,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,CAAC;CACF"}
@@ -0,0 +1,27 @@
1
+ import type { SessionInfo } from "./protocol.js";
2
+ import type { SessionActivityProvider } from "./sessionActivityProvider.js";
3
+ import type { TmuxSessionManager } from "./tmux.js";
4
+ /** 1 ページ分の整列済みセッションと続き位置。 */
5
+ export interface SessionListPage {
6
+ sessions: SessionInfo[];
7
+ nextCursor: string | null;
8
+ }
9
+ /** limit 省略時の既定 1 ページ件数(要件 2.9)。 */
10
+ export declare const SESSION_LIST_DEFAULT_LIMIT = 5;
11
+ interface CursorKey {
12
+ u: number;
13
+ n: string;
14
+ }
15
+ /** `(updatedAt, name)` を base64url(JSON) の不透明トークンへ符号化する。 */
16
+ export declare function encodeSessionListCursor(updatedAt: number, name: string): string;
17
+ /** 不透明トークンを整列キーへ復号する。復号不能は null(呼び出し側は先頭ページ扱い)。 */
18
+ export declare function decodeSessionListCursor(token: string): CursorKey | null;
19
+ /** list を更新時間降順/名前昇順に整列し keyset ページングする純サービス。 */
20
+ export declare class SessionListService {
21
+ private readonly sessionManager;
22
+ private readonly activityProvider;
23
+ constructor(sessionManager: TmuxSessionManager, activityProvider: SessionActivityProvider);
24
+ /** 更新時間降順/名前昇順で整列し、cursor より後方を limit 件返す。 */
25
+ page(limit: number | undefined, cursor: string | undefined): Promise<SessionListPage>;
26
+ }
27
+ export {};
@@ -0,0 +1,85 @@
1
+ // sessionListService.ts
2
+ // tailii (TS host) — セッション一覧の整列 + keyset ページング(session-list-lifecycle 1.x/2.x)
3
+ // Swift 版 SessionListService.swift の移植。
4
+ // (updatedAt desc, name asc) の安定順に整列し、不透明カーソルより後方を 1 ページ切り出す。
5
+ /** limit 省略時の既定 1 ページ件数(要件 2.9)。 */
6
+ export const SESSION_LIST_DEFAULT_LIMIT = 5;
7
+ /** `(updatedAt, name)` を base64url(JSON) の不透明トークンへ符号化する。 */
8
+ export function encodeSessionListCursor(updatedAt, name) {
9
+ // Swift 版 .sortedKeys と同じ `{"n":…,"u":…}` の正準形。
10
+ const json = JSON.stringify({ n: name, u: updatedAt });
11
+ return Buffer.from(json, "utf8").toString("base64url");
12
+ }
13
+ /** 不透明トークンを整列キーへ復号する。復号不能は null(呼び出し側は先頭ページ扱い)。 */
14
+ export function decodeSessionListCursor(token) {
15
+ try {
16
+ const raw = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
17
+ if (typeof raw !== "object" || raw === null)
18
+ return null;
19
+ const obj = raw;
20
+ if (typeof obj["u"] !== "number" || typeof obj["n"] !== "string")
21
+ return null;
22
+ return { u: obj["u"], n: obj["n"] };
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** list を更新時間降順/名前昇順に整列し keyset ページングする純サービス。 */
29
+ export class SessionListService {
30
+ sessionManager;
31
+ activityProvider;
32
+ constructor(sessionManager, activityProvider) {
33
+ this.sessionManager = sessionManager;
34
+ this.activityProvider = activityProvider;
35
+ }
36
+ /** 更新時間降順/名前昇順で整列し、cursor より後方を limit 件返す。 */
37
+ async page(limit, cursor) {
38
+ const raw = await this.sessionManager.list();
39
+ // updatedAt を付与する(未解決は省略 = 比較上 0)。
40
+ // tmux 由来の per-session 活動時刻(sessionManager.list() が付与)を最優先し、
41
+ // 無い場合のみ従来の provider(claude transcript mtime)へフォールバックする。
42
+ // これにより同一 cwd の複数セッションでも共有トランスクリプト mtime に潰されず、
43
+ // セッション個別の最近使用順で正しく整列する(最新セッションが一覧に埋もれない)。
44
+ const annotated = raw.map((info) => {
45
+ if (info.updatedAt !== undefined)
46
+ return info;
47
+ const updatedAt = this.activityProvider(info);
48
+ return updatedAt === null ? { ...info } : { ...info, updatedAt };
49
+ });
50
+ // (updatedAt desc, name asc) の安定ソート。
51
+ const sorted = annotated.slice().sort((lhs, rhs) => {
52
+ const lu = lhs.updatedAt ?? 0;
53
+ const ru = rhs.updatedAt ?? 0;
54
+ if (lu !== ru)
55
+ return ru - lu;
56
+ return lhs.name < rhs.name ? -1 : lhs.name > rhs.name ? 1 : 0;
57
+ });
58
+ // keyset seek: cursor(末尾キー)より「後方」の先頭位置を求める。
59
+ let startIndex = 0;
60
+ if (cursor !== undefined) {
61
+ const key = decodeSessionListCursor(cursor);
62
+ if (key !== null) {
63
+ const idx = sorted.findIndex((info) => isAfter(info, key));
64
+ startIndex = idx === -1 ? sorted.length : idx;
65
+ }
66
+ }
67
+ const effectiveLimit = Math.max(0, limit ?? SESSION_LIST_DEFAULT_LIMIT);
68
+ const endIndex = Math.min(startIndex + effectiveLimit, sorted.length);
69
+ const pageSessions = sorted.slice(startIndex, endIndex);
70
+ const hasMore = endIndex < sorted.length;
71
+ const last = pageSessions[pageSessions.length - 1];
72
+ const nextCursor = hasMore && last !== undefined
73
+ ? encodeSessionListCursor(last.updatedAt ?? 0, last.name)
74
+ : null;
75
+ return { sessions: pageSessions, nextCursor };
76
+ }
77
+ }
78
+ /** 降順 seek における「後方」判定: `u < u0` または(`u == u0` かつ `n > n0`)。 */
79
+ function isAfter(info, key) {
80
+ const u = info.updatedAt ?? 0;
81
+ if (u !== key.u)
82
+ return u < key.u;
83
+ return info.name > key.n;
84
+ }
85
+ //# sourceMappingURL=sessionListService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionListService.js","sourceRoot":"","sources":["../src/sessionListService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /** 1 セッション分のメタデータ(createdAt は Unix 秒)。 */
2
+ export interface SessionMeta {
3
+ name: string;
4
+ cwd: string;
5
+ createdAt: number;
6
+ /** そのセッションを駆動するエージェント(claude=既定 / codex)。未記録は claude 相当。 */
7
+ agent?: "claude" | "codex";
8
+ /** Claude の会話 JSONL 名に対応する session-id。未記録は従来どおり最新 JSONL 解決へ戻す。 */
9
+ claudeSessionId?: string;
10
+ }
11
+ /** session 名が安全でないときに投げる型付きエラー。 */
12
+ export declare class InvalidSessionNameError extends Error {
13
+ constructor(name: string);
14
+ }
15
+ /** session 名の安全性を検証する(空 / null バイト / `/` / `.`・`..` を拒否)。 */
16
+ export declare function validateSessionName(name: string): void;
17
+ /** デフォルトのベースディレクトリ(`~/.tailii/sessions`)。 */
18
+ export declare function defaultSessionsBase(): string;
19
+ /**
20
+ * セッションメタデータの永続ストア(cwd 権威記録)。
21
+ * 1 ファイル 1 セッション(`<base>/<name>.json`)。テストは一時 dir を注入する。
22
+ */
23
+ export declare class SessionMetadataStore {
24
+ private readonly base;
25
+ constructor(base?: string);
26
+ /** セッションメタデータを保存する(同一 name は上書き)。 */
27
+ put(meta: SessionMeta): void;
28
+ /** 指定 name のメタデータ。存在しない / 読めない場合は null(throw しない設計)。 */
29
+ get(name: string): SessionMeta | null;
30
+ /** 保存済みの全メタデータ(壊れたファイルは無視、順序不定)。 */
31
+ all(): SessionMeta[];
32
+ private fileFor;
33
+ }