tailii-host 0.1.0 → 0.3.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.
- package/README.md +35 -14
- package/dist/{broker.d.ts → approval/broker.d.ts} +9 -5
- package/dist/{broker.js → approval/broker.js} +42 -6
- package/dist/approval/broker.js.map +1 -0
- package/dist/{hook.d.ts → approval/hook.d.ts} +48 -7
- package/dist/{hook.js → approval/hook.js} +359 -50
- package/dist/approval/hook.js.map +1 -0
- package/dist/backend/herdr.d.ts +226 -0
- package/dist/backend/herdr.js +749 -0
- package/dist/backend/herdr.js.map +1 -0
- package/dist/backend/sessionBackend.d.ts +81 -0
- package/dist/backend/sessionBackend.js +114 -0
- package/dist/backend/sessionBackend.js.map +1 -0
- package/dist/backend/tmux.d.ts +125 -0
- package/dist/backend/tmux.js +392 -0
- package/dist/backend/tmux.js.map +1 -0
- package/dist/{chatTailController.d.ts → chat/chatTailController.d.ts} +29 -6
- package/dist/{chatTailController.js → chat/chatTailController.js} +94 -18
- package/dist/chat/chatTailController.js.map +1 -0
- package/dist/{imageService.d.ts → chat/imageService.d.ts} +12 -1
- package/dist/{imageService.js → chat/imageService.js} +39 -11
- package/dist/chat/imageService.js.map +1 -0
- package/dist/chat/remoteQuestionMonitor.d.ts +27 -0
- package/dist/chat/remoteQuestionMonitor.js +128 -0
- package/dist/chat/remoteQuestionMonitor.js.map +1 -0
- package/dist/chat/subagentTailer.d.ts +30 -0
- package/dist/chat/subagentTailer.js +704 -0
- package/dist/chat/subagentTailer.js.map +1 -0
- package/dist/chat/subagentTranscript.d.ts +20 -0
- package/dist/chat/subagentTranscript.js +273 -0
- package/dist/chat/subagentTranscript.js.map +1 -0
- package/dist/{transcriptTailer.d.ts → chat/transcriptTailer.d.ts} +12 -1
- package/dist/{transcriptTailer.js → chat/transcriptTailer.js} +183 -10
- package/dist/chat/transcriptTailer.js.map +1 -0
- package/dist/cli.js +15 -10
- package/dist/cli.js.map +1 -1
- package/dist/codex/codexAccountUsage.d.ts +22 -0
- package/dist/codex/codexAccountUsage.js +128 -0
- package/dist/codex/codexAccountUsage.js.map +1 -0
- package/dist/codex/codexAppServer.d.ts +166 -0
- package/dist/codex/codexAppServer.js +1297 -0
- package/dist/codex/codexAppServer.js.map +1 -0
- package/dist/codex/codexNativeTurnController.d.ts +153 -0
- package/dist/codex/codexNativeTurnController.js +673 -0
- package/dist/codex/codexNativeTurnController.js.map +1 -0
- package/dist/codex/codexRolloutTailer.d.ts +100 -0
- package/dist/codex/codexRolloutTailer.js +568 -0
- package/dist/codex/codexRolloutTailer.js.map +1 -0
- package/dist/{codexSessionStore.d.ts → codex/codexSessionStore.d.ts} +10 -2
- package/dist/{codexSessionStore.js → codex/codexSessionStore.js} +184 -15
- package/dist/codex/codexSessionStore.js.map +1 -0
- package/dist/codex/codexSubagentTracker.d.ts +21 -0
- package/dist/codex/codexSubagentTracker.js +281 -0
- package/dist/codex/codexSubagentTracker.js.map +1 -0
- package/dist/codex/codexToolActivity.d.ts +69 -0
- package/dist/codex/codexToolActivity.js +425 -0
- package/dist/codex/codexToolActivity.js.map +1 -0
- package/dist/{codexUsage.d.ts → codex/codexUsage.d.ts} +1 -1
- package/dist/{codexUsage.js → codex/codexUsage.js} +19 -26
- package/dist/codex/codexUsage.js.map +1 -0
- package/dist/commands/doctor.d.ts +65 -0
- package/dist/commands/doctor.js +425 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/{hookSettings.d.ts → commands/hookSettings.d.ts} +14 -1
- package/dist/{hookSettings.js → commands/hookSettings.js} +96 -12
- package/dist/commands/hookSettings.js.map +1 -0
- package/dist/{kick.js → commands/kick.js} +16 -1
- package/dist/commands/kick.js.map +1 -0
- package/dist/{launch.d.ts → commands/launch.d.ts} +67 -5
- package/dist/commands/launch.js +716 -0
- package/dist/commands/launch.js.map +1 -0
- package/dist/commands/legacyHomeMigration.js.map +1 -0
- package/dist/{pairingCode.d.ts → commands/pairingCode.d.ts} +29 -2
- package/dist/{pairingCode.js → commands/pairingCode.js} +120 -11
- package/dist/commands/pairingCode.js.map +1 -0
- package/dist/{pushTokenCommand.d.ts → commands/pushTokenCommand.d.ts} +1 -1
- package/dist/{pushTokenCommand.js → commands/pushTokenCommand.js} +2 -2
- package/dist/commands/pushTokenCommand.js.map +1 -0
- package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
- package/dist/{setup.js → commands/setup.js} +133 -75
- package/dist/commands/setup.js.map +1 -0
- package/dist/engine/context.d.ts +145 -0
- package/dist/engine/context.js +184 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/engine.d.ts +121 -0
- package/dist/engine/engine.js +799 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/engine/handlers/accountUsage.d.ts +53 -0
- package/dist/engine/handlers/accountUsage.js +203 -0
- package/dist/engine/handlers/accountUsage.js.map +1 -0
- package/dist/engine/handlers/claudeModels.d.ts +2 -0
- package/dist/engine/handlers/claudeModels.js +23 -0
- package/dist/engine/handlers/claudeModels.js.map +1 -0
- package/dist/engine/handlers/codex.d.ts +2 -0
- package/dist/engine/handlers/codex.js +97 -0
- package/dist/engine/handlers/codex.js.map +1 -0
- package/dist/engine/handlers/conversation.d.ts +2 -0
- package/dist/engine/handlers/conversation.js +259 -0
- package/dist/engine/handlers/conversation.js.map +1 -0
- package/dist/engine/handlers/core.d.ts +2 -0
- package/dist/engine/handlers/core.js +66 -0
- package/dist/engine/handlers/core.js.map +1 -0
- package/dist/engine/handlers/git.d.ts +2 -0
- package/dist/engine/handlers/git.js +127 -0
- package/dist/engine/handlers/git.js.map +1 -0
- package/dist/engine/handlers/index.d.ts +2 -0
- package/dist/engine/handlers/index.js +32 -0
- package/dist/engine/handlers/index.js.map +1 -0
- package/dist/engine/handlers/mode.d.ts +2 -0
- package/dist/engine/handlers/mode.js +194 -0
- package/dist/engine/handlers/mode.js.map +1 -0
- package/dist/engine/handlers/officialApps.d.ts +2 -0
- package/dist/engine/handlers/officialApps.js +79 -0
- package/dist/engine/handlers/officialApps.js.map +1 -0
- package/dist/engine/handlers/preview.d.ts +2 -0
- package/dist/engine/handlers/preview.js +59 -0
- package/dist/engine/handlers/preview.js.map +1 -0
- package/dist/engine/handlers/session.d.ts +2 -0
- package/dist/engine/handlers/session.js +385 -0
- package/dist/engine/handlers/session.js.map +1 -0
- package/dist/engine/handlers/usage.d.ts +2 -0
- package/dist/engine/handlers/usage.js +96 -0
- package/dist/engine/handlers/usage.js.map +1 -0
- package/dist/engine/handlers/workspace.d.ts +2 -0
- package/dist/engine/handlers/workspace.js +110 -0
- package/dist/engine/handlers/workspace.js.map +1 -0
- package/dist/engine/slashCommands.d.ts +3 -0
- package/dist/engine/slashCommands.js +211 -0
- package/dist/engine/slashCommands.js.map +1 -0
- package/dist/hub/engineRelaySocket.d.ts +58 -0
- package/dist/hub/engineRelaySocket.js +161 -0
- package/dist/hub/engineRelaySocket.js.map +1 -0
- package/dist/hub/hubClient.d.ts +28 -0
- package/dist/hub/hubClient.js +187 -0
- package/dist/hub/hubClient.js.map +1 -0
- package/dist/hub/hubDaemon.d.ts +69 -0
- package/dist/hub/hubDaemon.js +466 -0
- package/dist/hub/hubDaemon.js.map +1 -0
- package/dist/hub/hubProtocol.d.ts +161 -0
- package/dist/hub/hubProtocol.js +379 -0
- package/dist/hub/hubProtocol.js.map +1 -0
- package/dist/hub/panePreviewPump.d.ts +65 -0
- package/dist/hub/panePreviewPump.js +187 -0
- package/dist/hub/panePreviewPump.js.map +1 -0
- package/dist/hub/questionInjection.d.ts +19 -0
- package/dist/hub/questionInjection.js +126 -0
- package/dist/hub/questionInjection.js.map +1 -0
- package/dist/hub/reaper.d.ts +74 -0
- package/dist/hub/reaper.js +333 -0
- package/dist/hub/reaper.js.map +1 -0
- package/dist/hub/sessionHub.d.ts +203 -0
- package/dist/hub/sessionHub.js +1862 -0
- package/dist/hub/sessionHub.js.map +1 -0
- package/dist/protocol/common.d.ts +18 -0
- package/dist/protocol/common.js +78 -0
- package/dist/protocol/common.js.map +1 -0
- package/dist/protocol/decode.d.ts +3 -0
- package/dist/protocol/decode.js +1036 -0
- package/dist/protocol/decode.js.map +1 -0
- package/dist/protocol/encode.d.ts +3 -0
- package/dist/protocol/encode.js +84 -0
- package/dist/protocol/encode.js.map +1 -0
- package/dist/protocol/messages.d.ts +1002 -0
- package/dist/protocol/messages.js +8 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol.d.ts +4 -329
- package/dist/protocol.js +13 -531
- package/dist/protocol.js.map +1 -1
- package/dist/push/apnsConfigStore.js.map +1 -0
- package/dist/push/apnsJwtProvider.js.map +1 -0
- package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
- package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
- package/dist/push/approvalPushNotifier.js.map +1 -0
- package/dist/push/deviceTokenStore.js.map +1 -0
- package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
- package/dist/push/pushTypes.js.map +1 -0
- package/dist/push/sendLog.js.map +1 -0
- package/dist/services/accountIdentity.d.ts +44 -0
- package/dist/services/accountIdentity.js +184 -0
- package/dist/services/accountIdentity.js.map +1 -0
- package/dist/services/claudeCredentialRefresh.d.ts +30 -0
- package/dist/services/claudeCredentialRefresh.js +345 -0
- package/dist/services/claudeCredentialRefresh.js.map +1 -0
- package/dist/services/claudeModelCatalog.d.ts +21 -0
- package/dist/services/claudeModelCatalog.js +181 -0
- package/dist/services/claudeModelCatalog.js.map +1 -0
- package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
- package/dist/services/dirLister.js +169 -0
- package/dist/services/dirLister.js.map +1 -0
- package/dist/services/fileService.d.ts +10 -0
- package/dist/services/fileService.js +123 -0
- package/dist/services/fileService.js.map +1 -0
- package/dist/services/gitService.d.ts +79 -0
- package/dist/services/gitService.js +648 -0
- package/dist/services/gitService.js.map +1 -0
- package/dist/services/hostVersions.d.ts +38 -0
- package/dist/services/hostVersions.js +120 -0
- package/dist/services/hostVersions.js.map +1 -0
- package/dist/services/officialApps.d.ts +148 -0
- package/dist/services/officialApps.js +1079 -0
- package/dist/services/officialApps.js.map +1 -0
- package/dist/services/planUsageFetcher.d.ts +119 -0
- package/dist/services/planUsageFetcher.js +357 -0
- package/dist/services/planUsageFetcher.js.map +1 -0
- package/dist/services/previewServer.d.ts +31 -0
- package/dist/services/previewServer.js +233 -0
- package/dist/services/previewServer.js.map +1 -0
- package/dist/services/quicGateway.d.ts +143 -0
- package/dist/services/quicGateway.js +292 -0
- package/dist/services/quicGateway.js.map +1 -0
- package/dist/services/serveService.d.ts +50 -0
- package/dist/services/serveService.js +302 -0
- package/dist/services/serveService.js.map +1 -0
- package/dist/services/usageAggregator.js.map +1 -0
- package/dist/sessions/claudeSessionStore.d.ts +80 -0
- package/dist/sessions/claudeSessionStore.js +477 -0
- package/dist/sessions/claudeSessionStore.js.map +1 -0
- package/dist/sessions/heartbeat.d.ts +24 -0
- package/dist/sessions/heartbeat.js +86 -0
- package/dist/sessions/heartbeat.js.map +1 -0
- package/dist/sessions/liveSessionJoin.d.ts +20 -0
- package/dist/sessions/liveSessionJoin.js +80 -0
- package/dist/sessions/liveSessionJoin.js.map +1 -0
- package/dist/sessions/sessionActivityProvider.d.ts +14 -0
- package/dist/sessions/sessionActivityProvider.js +57 -0
- package/dist/sessions/sessionActivityProvider.js.map +1 -0
- package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
- package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
- package/dist/sessions/sessionListService.js.map +1 -0
- package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
- package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
- package/dist/sessions/sessionMetadataStore.js.map +1 -0
- package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
- package/dist/sessions/sessionSearch.js.map +1 -0
- package/dist/shared/hostDisplayName.d.ts +11 -0
- package/dist/shared/hostDisplayName.js +50 -0
- package/dist/shared/hostDisplayName.js.map +1 -0
- package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
- package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
- package/dist/shared/lineWriter.js.map +1 -0
- package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
- package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
- package/dist/{paths.js → shared/paths.js} +9 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/permissionMode.d.ts +10 -0
- package/dist/shared/permissionMode.js +51 -0
- package/dist/shared/permissionMode.js.map +1 -0
- package/dist/shared/skillInjection.d.ts +9 -0
- package/dist/shared/skillInjection.js +16 -0
- package/dist/shared/skillInjection.js.map +1 -0
- package/dist/shared/sleep.js.map +1 -0
- package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
- package/dist/{socketPath.js → shared/socketPath.js} +22 -0
- package/dist/shared/socketPath.js.map +1 -0
- package/dist/shared/version.js.map +1 -0
- package/package.json +13 -7
- package/dist/apnsConfigStore.js.map +0 -1
- package/dist/apnsJwtProvider.js.map +0 -1
- package/dist/approvalPushNotifier.js.map +0 -1
- package/dist/broker.js.map +0 -1
- package/dist/chatTailController.js.map +0 -1
- package/dist/claudeSessionStore.d.ts +0 -15
- package/dist/claudeSessionStore.js +0 -201
- package/dist/claudeSessionStore.js.map +0 -1
- package/dist/codexRolloutTailer.d.ts +0 -56
- package/dist/codexRolloutTailer.js +0 -303
- package/dist/codexRolloutTailer.js.map +0 -1
- package/dist/codexSessionStore.js.map +0 -1
- package/dist/codexUsage.js.map +0 -1
- package/dist/deviceTokenStore.js.map +0 -1
- package/dist/dirLister.js +0 -98
- package/dist/dirLister.js.map +0 -1
- package/dist/doctor.d.ts +0 -36
- package/dist/doctor.js +0 -162
- package/dist/doctor.js.map +0 -1
- package/dist/engine.d.ts +0 -85
- package/dist/engine.js +0 -962
- package/dist/engine.js.map +0 -1
- package/dist/hook.js.map +0 -1
- package/dist/hookSettings.js.map +0 -1
- package/dist/idleReaper.d.ts +0 -13
- package/dist/idleReaper.js +0 -30
- package/dist/idleReaper.js.map +0 -1
- package/dist/imageService.js.map +0 -1
- package/dist/kick.js.map +0 -1
- package/dist/launch.js +0 -396
- package/dist/launch.js.map +0 -1
- package/dist/legacyHomeMigration.js.map +0 -1
- package/dist/lineWriter.js.map +0 -1
- package/dist/mdnsAdvertiser.d.ts +0 -46
- package/dist/mdnsAdvertiser.js +0 -268
- package/dist/mdnsAdvertiser.js.map +0 -1
- package/dist/pairingCode.js.map +0 -1
- package/dist/paths.js.map +0 -1
- package/dist/permissionMode.d.ts +0 -8
- package/dist/permissionMode.js +0 -32
- package/dist/permissionMode.js.map +0 -1
- package/dist/planUsageFetcher.d.ts +0 -35
- package/dist/planUsageFetcher.js +0 -172
- package/dist/planUsageFetcher.js.map +0 -1
- package/dist/pushTokenCommand.js.map +0 -1
- package/dist/pushTypes.js.map +0 -1
- package/dist/sendLog.js.map +0 -1
- package/dist/sessionActivityProvider.d.ts +0 -7
- package/dist/sessionActivityProvider.js +0 -44
- package/dist/sessionActivityProvider.js.map +0 -1
- package/dist/sessionIdleTracker.d.ts +0 -19
- package/dist/sessionIdleTracker.js +0 -42
- package/dist/sessionIdleTracker.js.map +0 -1
- package/dist/sessionListService.js.map +0 -1
- package/dist/sessionMetadataStore.js.map +0 -1
- package/dist/sessionSearch.js.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/sleep.js.map +0 -1
- package/dist/socketPath.js.map +0 -1
- package/dist/subagentTailer.d.ts +0 -15
- package/dist/subagentTailer.js +0 -399
- package/dist/subagentTailer.js.map +0 -1
- package/dist/tmux.d.ts +0 -62
- package/dist/tmux.js +0 -163
- package/dist/tmux.js.map +0 -1
- package/dist/transcriptTailer.js.map +0 -1
- package/dist/usageAggregator.js.map +0 -1
- package/dist/version.js.map +0 -1
- /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
- /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
- /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
- /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
- /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
- /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
- /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
- /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
- /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
- /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
- /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
- /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
- /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
- /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
- /package/dist/{sendLog.js → push/sendLog.js} +0 -0
- /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
- /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
- /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
- /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
- /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
- /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
- /package/dist/{sleep.js → shared/sleep.js} +0 -0
- /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
- /package/dist/{version.js → shared/version.js} +0 -0
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
// officialApps.ts
|
|
2
|
+
// tailii host — Claude / ChatGPT 公式アプリ連携の固定コマンドアダプタ。
|
|
3
|
+
//
|
|
4
|
+
// クライアントから実行ファイル・argv・URL・任意文字列は受け取らない。provider と
|
|
5
|
+
// open/repair/stop の型付き操作だけを受け、現行の検証済み CLI 出力だけを解釈する。
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { defaultInjectedPath } from "../commands/launch.js";
|
|
11
|
+
const CLAUDE_SESSION_PREFIX = "https://claude.ai/code/";
|
|
12
|
+
const CHATGPT_CODEX_PAIR_URL = "https://chatgpt.com/codex/pair";
|
|
13
|
+
const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
14
|
+
const TRANSCRIPT_TAIL_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
const TRANSCRIPT_FRESH_SKEW_MS = 5_000;
|
|
16
|
+
const MAX_IDENTIFIER_BYTES = 128;
|
|
17
|
+
const MAX_PAIRING_CODE_BYTES = 2_048;
|
|
18
|
+
const PROVIDER_COMMAND_TIMEOUT_MS = 22_000;
|
|
19
|
+
const CLAUDE_AUTH_TIMEOUT_MS = 5_000;
|
|
20
|
+
const SUPPORTED_CLAUDE_VERSIONS = new Set(["2.1.215", "2.1.218", "2.1.220"]);
|
|
21
|
+
const SUPPORTED_CODEX_VERSIONS = new Set(["0.144.5", "0.145.0"]);
|
|
22
|
+
// Remote Control が active と CLI が認識している最中に /remote-control を再実行すると、
|
|
23
|
+
// 2.1.220 はバナーではなく modal dialog(Disconnect / Show QR / Continue)を開き、
|
|
24
|
+
// Enter/Esc を受けるまで pane を塞ぎ続ける(自動では閉じない)。iPhone からは不可視の
|
|
25
|
+
// ため、検出したら Esc で閉じて dialog 内の URL を採用する。
|
|
26
|
+
const CLAUDE_DIALOG_URL_PREFIX = "This session is available in the Claude mobile app and at ";
|
|
27
|
+
const CLAUDE_DIALOG_DISCONNECT_LABEL = "Disconnect this session";
|
|
28
|
+
const CLAUDE_FAILURES = [
|
|
29
|
+
["Remote Control requires a claude.ai subscription", "claude_subscription_required"],
|
|
30
|
+
["Remote Control requires a full-scope login token", "claude_full_scope_login_required"],
|
|
31
|
+
[
|
|
32
|
+
"Unable to determine your organization for Remote Control eligibility",
|
|
33
|
+
"claude_auth_refresh_required",
|
|
34
|
+
],
|
|
35
|
+
["Remote Control is not yet enabled for your account", "claude_remote_not_enabled"],
|
|
36
|
+
["Couldn’t verify Remote Control eligibility", "claude_eligibility_check_failed"],
|
|
37
|
+
["Couldn't verify Remote Control eligibility", "claude_eligibility_check_failed"],
|
|
38
|
+
[
|
|
39
|
+
"Couldn’t verify your organization’s Remote Control policy",
|
|
40
|
+
"claude_eligibility_check_failed",
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"Remote Control is only available when using Claude via api.anthropic.com",
|
|
44
|
+
"claude_api_endpoint_unsupported",
|
|
45
|
+
],
|
|
46
|
+
["Remote Control is disabled by your organization", "claude_remote_disabled_by_policy"],
|
|
47
|
+
["disableRemoteControl", "claude_remote_disabled_by_policy"],
|
|
48
|
+
["Remote credentials fetch failed", "claude_credentials_fetch_failed"],
|
|
49
|
+
["Couldn’t reconnect to your Remote Control session", "claude_reconnect_failed"],
|
|
50
|
+
[
|
|
51
|
+
"Your organization requires Trusted Devices for Remote Control, but this device is not enrolled",
|
|
52
|
+
"claude_trusted_device_required",
|
|
53
|
+
],
|
|
54
|
+
["session expired for trusted-device check", "claude_auth_refresh_required"],
|
|
55
|
+
["workspace trust", "claude_workspace_trust_required"],
|
|
56
|
+
];
|
|
57
|
+
export class OfficialAppsService {
|
|
58
|
+
claudePath;
|
|
59
|
+
codexPath;
|
|
60
|
+
runCommand;
|
|
61
|
+
now;
|
|
62
|
+
actionLockPath;
|
|
63
|
+
claudePollIntervalMs;
|
|
64
|
+
claudeStartTimeoutMs;
|
|
65
|
+
claudeReconnectGraceMs;
|
|
66
|
+
claudeReinjectAfterMs;
|
|
67
|
+
diagnosticLogPath;
|
|
68
|
+
codexRemoteControl;
|
|
69
|
+
constructor(options = {}) {
|
|
70
|
+
this.claudePath = options.claudePath ?? "claude";
|
|
71
|
+
this.codexPath = options.codexPath ?? "codex";
|
|
72
|
+
this.runCommand = options.commandRunner ?? runFixedCommand;
|
|
73
|
+
this.now = options.now ?? (() => Math.floor(Date.now() / 1_000));
|
|
74
|
+
this.actionLockPath =
|
|
75
|
+
options.actionLockPath ?? path.join(os.homedir(), ".tailii", "official-app-action.lock");
|
|
76
|
+
this.claudePollIntervalMs = options.claudePollIntervalMs ?? 250;
|
|
77
|
+
// アーカイブ後の再接続はバナー再表示まで 10-15 秒かかることを実測済み。
|
|
78
|
+
this.claudeStartTimeoutMs = options.claudeStartTimeoutMs ?? 20_000;
|
|
79
|
+
this.claudeReconnectGraceMs = options.claudeReconnectGraceMs ?? 5_000;
|
|
80
|
+
this.claudeReinjectAfterMs = options.claudeReinjectAfterMs ?? 6_000;
|
|
81
|
+
this.diagnosticLogPath = options.diagnosticLogPath ?? null;
|
|
82
|
+
this.codexRemoteControl = options.codexRemoteControl ?? null;
|
|
83
|
+
}
|
|
84
|
+
/** 障害調査用の常時ログ。失敗時にどの分岐で落ちたかを事後特定できるようにする。 */
|
|
85
|
+
diag(message) {
|
|
86
|
+
if (this.diagnosticLogPath === null)
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
fs.appendFileSync(this.diagnosticLogPath, `[${new Date().toISOString()}] ${message}\n`);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// 診断ログは本処理を妨げない。
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async status(context) {
|
|
96
|
+
const version = await this.providerVersion(context.provider);
|
|
97
|
+
if (version === null)
|
|
98
|
+
return unavailableStatus(context.provider, "official_cli_unavailable");
|
|
99
|
+
if (context.provider === "codex") {
|
|
100
|
+
if (!context.canMutateCodexDaemon) {
|
|
101
|
+
return unavailableStatus("codex", "codex_agent_busy", version);
|
|
102
|
+
}
|
|
103
|
+
if (this.codexRemoteControl !== null) {
|
|
104
|
+
const status = await this.codexRemoteControl.remoteControlStatus();
|
|
105
|
+
if (status === null) {
|
|
106
|
+
return unavailableStatus("codex", "codex_app_server_remote_unavailable", version);
|
|
107
|
+
}
|
|
108
|
+
switch (status.status) {
|
|
109
|
+
case "connected":
|
|
110
|
+
return {
|
|
111
|
+
provider: "codex",
|
|
112
|
+
version,
|
|
113
|
+
state: "active",
|
|
114
|
+
canOpen: true,
|
|
115
|
+
canStart: false,
|
|
116
|
+
};
|
|
117
|
+
case "disabled":
|
|
118
|
+
return {
|
|
119
|
+
provider: "codex",
|
|
120
|
+
version,
|
|
121
|
+
state: "inactive",
|
|
122
|
+
canOpen: false,
|
|
123
|
+
canStart: true,
|
|
124
|
+
};
|
|
125
|
+
case "connecting":
|
|
126
|
+
return unavailableStatus("codex", "codex_remote_connecting", version);
|
|
127
|
+
case "errored":
|
|
128
|
+
return unavailableStatus("codex", "codex_remote_errored", version);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
provider: "codex",
|
|
133
|
+
version,
|
|
134
|
+
state: "inactive",
|
|
135
|
+
canOpen: false,
|
|
136
|
+
canStart: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const authReason = await this.claudeAuthReason();
|
|
140
|
+
if (authReason !== null)
|
|
141
|
+
return unavailableStatus("claude", authReason, version);
|
|
142
|
+
return this.claudeStatus(context, version);
|
|
143
|
+
}
|
|
144
|
+
async perform(context, action, automaticEnable, paired) {
|
|
145
|
+
const release = this.acquireActionLock();
|
|
146
|
+
if (release === null) {
|
|
147
|
+
return unavailableResult(context.provider, "official_action_in_progress");
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const version = await this.providerVersion(context.provider);
|
|
151
|
+
if (version === null) {
|
|
152
|
+
this.diag(`perform cli unavailable provider=${context.provider}`);
|
|
153
|
+
return unavailableResult(context.provider, "official_cli_unavailable");
|
|
154
|
+
}
|
|
155
|
+
if (context.provider === "claude") {
|
|
156
|
+
if (action === "stop") {
|
|
157
|
+
return this.stopClaude(context);
|
|
158
|
+
}
|
|
159
|
+
const authReason = await this.claudeAuthReason();
|
|
160
|
+
if (authReason !== null) {
|
|
161
|
+
this.diag(`perform auth reason=${authReason}`);
|
|
162
|
+
return unavailableResult("claude", authReason);
|
|
163
|
+
}
|
|
164
|
+
return this.performClaude(context);
|
|
165
|
+
}
|
|
166
|
+
if (!context.canMutateCodexDaemon) {
|
|
167
|
+
return unavailableResult("codex", "codex_agent_busy");
|
|
168
|
+
}
|
|
169
|
+
return this.performCodex(action, automaticEnable, paired);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
release();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async providerVersion(provider) {
|
|
176
|
+
const executable = provider === "claude" ? this.claudePath : this.codexPath;
|
|
177
|
+
const output = await this.runCommand(executable, ["--version"], CLAUDE_AUTH_TIMEOUT_MS);
|
|
178
|
+
if (output?.success !== true)
|
|
179
|
+
return null;
|
|
180
|
+
const text = output.stdout.trim();
|
|
181
|
+
if (provider === "claude") {
|
|
182
|
+
const version = text.endsWith(" (Claude Code)")
|
|
183
|
+
? text.slice(0, -" (Claude Code)".length)
|
|
184
|
+
: null;
|
|
185
|
+
return version !== null && SUPPORTED_CLAUDE_VERSIONS.has(version) ? version : null;
|
|
186
|
+
}
|
|
187
|
+
const version = text.startsWith("codex-cli ") ? text.slice("codex-cli ".length) : null;
|
|
188
|
+
return version !== null && SUPPORTED_CODEX_VERSIONS.has(version) ? version : null;
|
|
189
|
+
}
|
|
190
|
+
async claudeAuthReason() {
|
|
191
|
+
const output = await this.runCommand(this.claudePath, ["auth", "status", "--json"], CLAUDE_AUTH_TIMEOUT_MS);
|
|
192
|
+
if (output?.success !== true)
|
|
193
|
+
return "claude_auth_status_failed";
|
|
194
|
+
const auth = parseClaudeAuth(output.stdout);
|
|
195
|
+
if (auth === null)
|
|
196
|
+
return "claude_auth_status_invalid";
|
|
197
|
+
if (!auth.loggedIn)
|
|
198
|
+
return "claude_login_required";
|
|
199
|
+
if (auth.authMethod !== "claude.ai" || auth.apiProvider !== "firstParty") {
|
|
200
|
+
return "claude_subscription_login_required";
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
async claudeStatus(context, version) {
|
|
205
|
+
const paneText = await captureOfficialPane(context.sessionManager, context.session);
|
|
206
|
+
const dialogUrl = paneText === null ? null : extractClaudeRemoteDialogUrl(paneText);
|
|
207
|
+
const active = dialogUrl !== null || (paneText !== null && statusBarShowsRemoteControl(paneText));
|
|
208
|
+
if (active) {
|
|
209
|
+
const launchUrl = lastTranscriptRemoteControlEntry(context.claudeTranscriptPath)?.url ??
|
|
210
|
+
dialogUrl ??
|
|
211
|
+
(paneText === null ? null : extractBannerClaudeUrl(paneText));
|
|
212
|
+
return {
|
|
213
|
+
provider: "claude",
|
|
214
|
+
version,
|
|
215
|
+
state: "active",
|
|
216
|
+
canOpen: true,
|
|
217
|
+
canStart: false,
|
|
218
|
+
...(launchUrl !== null ? { launchUrl } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (context.canInjectClaudeCommand) {
|
|
222
|
+
return {
|
|
223
|
+
provider: "claude",
|
|
224
|
+
version,
|
|
225
|
+
state: "inactive",
|
|
226
|
+
canOpen: false,
|
|
227
|
+
canStart: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return unavailableStatus("claude", "claude_agent_busy", version);
|
|
231
|
+
}
|
|
232
|
+
async performClaude(context) {
|
|
233
|
+
const before = await captureOfficialPane(context.sessionManager, context.session);
|
|
234
|
+
this.diag(`perform start session=${context.session} paneBytes=${before === null ? "null" : Buffer.byteLength(before, "utf8")} transcript=${context.claudeTranscriptPath ?? "none"} canInject=${context.canInjectClaudeCommand}`);
|
|
235
|
+
const staleDialog = before === null ? null : extractClaudeRemoteDialogUrl(before);
|
|
236
|
+
if (staleDialog !== null) {
|
|
237
|
+
await this.dismissClaudeRemoteDialog(context);
|
|
238
|
+
this.diag(`perform open via stale dialog session=${context.session}`);
|
|
239
|
+
return openResult("claude", staleDialog);
|
|
240
|
+
}
|
|
241
|
+
if (before !== null && statusBarShowsRemoteControl(before)) {
|
|
242
|
+
const url = lastTranscriptRemoteControlEntry(context.claudeTranscriptPath)?.url ??
|
|
243
|
+
extractBannerClaudeUrl(before);
|
|
244
|
+
if (url !== null) {
|
|
245
|
+
this.diag(`perform open via pre-check /rc session=${context.session}`);
|
|
246
|
+
return openResult("claude", url);
|
|
247
|
+
}
|
|
248
|
+
// active なのに URL 不明(transcript 欠落・バナー窓外): 注入すると CLI は
|
|
249
|
+
// dialog を開くので、poll 側の dialog 経路で URL を回収できる。
|
|
250
|
+
this.diag(`perform /rc lit but url unknown session=${context.session}`);
|
|
251
|
+
}
|
|
252
|
+
if (!context.canInjectClaudeCommand) {
|
|
253
|
+
this.diag(`perform busy session=${context.session}`);
|
|
254
|
+
return unavailableResult("claude", "claude_agent_busy");
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await context.sessionManager.sendTextSubmit(context.session, "/remote-control");
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
this.diag(`perform inject failed session=${context.session} error=${String(error).slice(0, 120)}`);
|
|
261
|
+
return unavailableResult("claude", "claude_start_failed");
|
|
262
|
+
}
|
|
263
|
+
this.diag(`perform injected session=${context.session}`);
|
|
264
|
+
const injectedAtMs = Date.now();
|
|
265
|
+
const deadline = injectedAtMs + this.claudeStartTimeoutMs;
|
|
266
|
+
let injectionAttempts = 1;
|
|
267
|
+
// transcript は stat(size/mtime) が変わったときだけ読み直す(8MiB 窓を 250ms
|
|
268
|
+
// 毎に全読みしない)。実行証跡(local_command 記録)の有無も同じ stat ゲートで
|
|
269
|
+
// 更新し、一度 true になったら固定する(記録は消えない)。
|
|
270
|
+
let cachedEntry = null;
|
|
271
|
+
let cachedStamp = "";
|
|
272
|
+
let commandRecordSeen = false;
|
|
273
|
+
const currentTranscriptEntry = () => {
|
|
274
|
+
if (context.claudeTranscriptPath === null)
|
|
275
|
+
return null;
|
|
276
|
+
let stamp;
|
|
277
|
+
try {
|
|
278
|
+
const stat = fs.statSync(context.claudeTranscriptPath);
|
|
279
|
+
stamp = `${stat.size}:${stat.mtimeMs}`;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (stamp !== cachedStamp) {
|
|
285
|
+
cachedEntry = lastTranscriptRemoteControlEntry(context.claudeTranscriptPath);
|
|
286
|
+
if (!commandRecordSeen) {
|
|
287
|
+
commandRecordSeen = transcriptHasRemoteControlCommandSince(context.claudeTranscriptPath, injectedAtMs - TRANSCRIPT_FRESH_SKEW_MS);
|
|
288
|
+
}
|
|
289
|
+
cachedStamp = stamp;
|
|
290
|
+
}
|
|
291
|
+
return cachedEntry;
|
|
292
|
+
};
|
|
293
|
+
let latestText = "";
|
|
294
|
+
let paneMisses = 0;
|
|
295
|
+
do {
|
|
296
|
+
await delay(this.claudePollIntervalMs);
|
|
297
|
+
const paneText = await captureOfficialPane(context.sessionManager, context.session);
|
|
298
|
+
// transcript の bridge_status は pane 取得の成否と独立に判定する(pane が
|
|
299
|
+
// 一時的に読めない状態が続いても activation 成功を取りこぼさない)。
|
|
300
|
+
const entry = currentTranscriptEntry();
|
|
301
|
+
// 新規 activation は bridge_status を追記する。注入以降の行だけを新規成功と
|
|
302
|
+
// みなす(TRANSCRIPT_FRESH_SKEW_MS は書込タイムスタンプとの許容ずれ)。
|
|
303
|
+
if (entry !== null &&
|
|
304
|
+
entry.atMs !== null &&
|
|
305
|
+
entry.atMs >= injectedAtMs - TRANSCRIPT_FRESH_SKEW_MS) {
|
|
306
|
+
this.diag(`perform open via fresh bridge_status session=${context.session}`);
|
|
307
|
+
return openResult("claude", entry.url);
|
|
308
|
+
}
|
|
309
|
+
if (paneText === null) {
|
|
310
|
+
paneMisses += 1;
|
|
311
|
+
// pane が読めないまま grace を超えたら、既知 URL を最善手として返す
|
|
312
|
+
// (capture 障害で 20 秒待って失敗にするより回復性を優先)。
|
|
313
|
+
if (entry !== null && Date.now() - injectedAtMs > this.claudeReconnectGraceMs) {
|
|
314
|
+
this.diag(`perform open via known URL (pane unreadable x${paneMisses}) session=${context.session}`);
|
|
315
|
+
return openResult("claude", entry.url);
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
latestText = paneText;
|
|
320
|
+
// 会話がアーカイブされた直後は CLI がまだ active 扱いのため、注入した
|
|
321
|
+
// /remote-control が dialog になる。閉じて URL を返す(再接続は CLI 側が担う)。
|
|
322
|
+
const dialogUrl = extractClaudeRemoteDialogUrl(paneText);
|
|
323
|
+
if (dialogUrl !== null) {
|
|
324
|
+
await this.dismissClaudeRemoteDialog(context);
|
|
325
|
+
this.diag(`perform open via dialog session=${context.session}`);
|
|
326
|
+
return openResult("claude", dialogUrl);
|
|
327
|
+
}
|
|
328
|
+
// 同一プロセスの再接続は bridge_status を追記しない(実測)。/rc 点灯を
|
|
329
|
+
// 確認できたら既知 URL(同一プロセスで不変)を返す。新規行の追記を先に
|
|
330
|
+
// 拾えるよう、注入直後の grace 期間は entry 追記待ちを優先する。
|
|
331
|
+
if (entry !== null &&
|
|
332
|
+
statusBarShowsRemoteControl(paneText) &&
|
|
333
|
+
Date.now() - injectedAtMs > this.claudeReconnectGraceMs) {
|
|
334
|
+
this.diag(`perform open via /rc + known URL session=${context.session}`);
|
|
335
|
+
return openResult("claude", entry.url);
|
|
336
|
+
}
|
|
337
|
+
if (entry === null) {
|
|
338
|
+
const bannerUrl = extractBannerClaudeUrl(paneText);
|
|
339
|
+
if (bannerUrl !== null) {
|
|
340
|
+
this.diag(`perform open via banner session=${context.session}`);
|
|
341
|
+
return openResult("claude", bannerUrl);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const reason = classifyClaudeFailure(paneText);
|
|
345
|
+
if (reason !== null) {
|
|
346
|
+
this.diag(`perform failure marker=${reason} session=${context.session}`);
|
|
347
|
+
return unavailableResult("claude", reason);
|
|
348
|
+
}
|
|
349
|
+
// RC 切断直後の limbo では注入テキストが入力欄エコーごと黙殺される(実測:
|
|
350
|
+
// record も pane 変化も残らない)。実行証跡(local_command 記録)が出ないまま
|
|
351
|
+
// 時間が経ったら、limbo が明けた前提で再注入する(少し待てば成功する挙動を
|
|
352
|
+
// 手動の押し直しからホスト内リトライへ移す)。claude が処理中(ユーザーが
|
|
353
|
+
// 並行してメッセージを送った等)の再注入は、queued 化してターン後に意図せず
|
|
354
|
+
// 実行されるため見送る。
|
|
355
|
+
if (injectionAttempts < 3 &&
|
|
356
|
+
Date.now() - injectedAtMs > this.claudeReinjectAfterMs * injectionAttempts &&
|
|
357
|
+
!commandRecordSeen &&
|
|
358
|
+
!statusBarShowsProcessing(paneText)) {
|
|
359
|
+
injectionAttempts += 1;
|
|
360
|
+
this.diag(`perform reinject attempt=${injectionAttempts} session=${context.session}`);
|
|
361
|
+
try {
|
|
362
|
+
await context.sessionManager.sendTextSubmit(context.session, "/remote-control");
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// 再注入失敗は poll 継続(最終的に timeout で報告する)。
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} while (Date.now() <= deadline);
|
|
369
|
+
this.diag(`perform timeout session=${context.session} paneMisses=${paneMisses} ` +
|
|
370
|
+
`transcript=${context.claudeTranscriptPath ?? "none"} lastPaneBytes=${Buffer.byteLength(latestText, "utf8")}`);
|
|
371
|
+
return unavailableResult("claude", classifyClaudeFailure(latestText) ?? "claude_start_failed");
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Remote Control の切断。2.1.220 の `/remote-control` はトグルではなく、active 中の
|
|
375
|
+
* 再実行で dialog(Disconnect / Show QR / Continue, カーソルは Continue)を開く。
|
|
376
|
+
* dialog を出して Up Up + Enter で「Disconnect this session」を選択し、ステータス
|
|
377
|
+
* バーの `/rc` 消灯で成功を検証する(実測 2026-07-27)。
|
|
378
|
+
*/
|
|
379
|
+
async stopClaude(context) {
|
|
380
|
+
const before = await captureOfficialPane(context.sessionManager, context.session);
|
|
381
|
+
this.diag(`stop start session=${context.session} paneBytes=${before === null ? "null" : Buffer.byteLength(before, "utf8")}`);
|
|
382
|
+
let dialogVisible = before !== null && extractClaudeRemoteDialogUrl(before) !== null;
|
|
383
|
+
if (!dialogVisible) {
|
|
384
|
+
if (before === null || !statusBarShowsRemoteControl(before)) {
|
|
385
|
+
this.diag(`stop already off session=${context.session}`);
|
|
386
|
+
return { provider: "claude", outcome: "stopped" };
|
|
387
|
+
}
|
|
388
|
+
if (!context.canInjectClaudeCommand) {
|
|
389
|
+
return unavailableResult("claude", "claude_agent_busy");
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
await context.sessionManager.sendTextSubmit(context.session, "/remote-control");
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return unavailableResult("claude", "claude_stop_failed");
|
|
396
|
+
}
|
|
397
|
+
// dialog が開くのを待つ。
|
|
398
|
+
const dialogDeadline = Date.now() + Math.min(8_000, this.claudeStartTimeoutMs);
|
|
399
|
+
while (Date.now() <= dialogDeadline) {
|
|
400
|
+
await delay(this.claudePollIntervalMs);
|
|
401
|
+
const paneText = await captureOfficialPane(context.sessionManager, context.session);
|
|
402
|
+
if (paneText !== null && extractClaudeRemoteDialogUrl(paneText) !== null) {
|
|
403
|
+
dialogVisible = true;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!dialogVisible) {
|
|
408
|
+
this.diag(`stop dialog never appeared session=${context.session}`);
|
|
409
|
+
return unavailableResult("claude", "claude_stop_failed");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
await context.sessionManager.sendKeys(context.session, ["Up", "Up", "Enter"]);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
await this.dismissClaudeRemoteDialog(context);
|
|
417
|
+
return unavailableResult("claude", "claude_stop_failed");
|
|
418
|
+
}
|
|
419
|
+
const offDeadline = Date.now() + Math.min(8_000, this.claudeStartTimeoutMs);
|
|
420
|
+
while (Date.now() <= offDeadline) {
|
|
421
|
+
await delay(this.claudePollIntervalMs);
|
|
422
|
+
const paneText = await captureOfficialPane(context.sessionManager, context.session);
|
|
423
|
+
if (paneText !== null &&
|
|
424
|
+
!statusBarShowsRemoteControl(paneText) &&
|
|
425
|
+
extractClaudeRemoteDialogUrl(paneText) === null) {
|
|
426
|
+
this.diag(`stop confirmed session=${context.session}`);
|
|
427
|
+
return { provider: "claude", outcome: "stopped" };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// dialog が残っている可能性に備えて閉じ、失敗として報告する。
|
|
431
|
+
await this.dismissClaudeRemoteDialog(context);
|
|
432
|
+
this.diag(`stop verify timeout session=${context.session}`);
|
|
433
|
+
return unavailableResult("claude", "claude_stop_failed");
|
|
434
|
+
}
|
|
435
|
+
async dismissClaudeRemoteDialog(context) {
|
|
436
|
+
try {
|
|
437
|
+
await context.sessionManager.sendKeys(context.session, ["Escape"]);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// 閉じられなくても open は返す。次回 perform 時に再検出して再試行する。
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async performCodex(action, automaticEnable, paired) {
|
|
444
|
+
if (this.codexRemoteControl !== null) {
|
|
445
|
+
return this.performCodexViaAppServer(action, automaticEnable, paired);
|
|
446
|
+
}
|
|
447
|
+
if (action === "stop") {
|
|
448
|
+
const stopped = await this.runCommand(this.codexPath, ["remote-control", "stop", "--json"], PROVIDER_COMMAND_TIMEOUT_MS);
|
|
449
|
+
return stopped?.success === true && parseCodexStop(stopped.stdout)
|
|
450
|
+
? { provider: "codex", outcome: "stopped" }
|
|
451
|
+
: unavailableResult("codex", "codex_stop_failed");
|
|
452
|
+
}
|
|
453
|
+
if (!automaticEnable) {
|
|
454
|
+
return unavailableResult("codex", "codex_automatic_enable_disabled");
|
|
455
|
+
}
|
|
456
|
+
const started = await this.runCommand(this.codexPath, ["remote-control", "start", "--json"], PROVIDER_COMMAND_TIMEOUT_MS);
|
|
457
|
+
const start = started?.success === true ? parseCodexStart(started.stdout) : null;
|
|
458
|
+
if (start === null)
|
|
459
|
+
return unavailableResult("codex", "codex_start_failed");
|
|
460
|
+
if (start === "connecting")
|
|
461
|
+
return unavailableResult("codex", "codex_remote_connecting");
|
|
462
|
+
const pairedOutput = await this.runCommand(this.codexPath, ["remote-control", "pair", "--json"], PROVIDER_COMMAND_TIMEOUT_MS);
|
|
463
|
+
const pairing = pairedOutput?.success === true ? parseCodexPairing(pairedOutput.stdout) : null;
|
|
464
|
+
if (pairing === null)
|
|
465
|
+
return unavailableResult("codex", "codex_pair_failed");
|
|
466
|
+
if (pairing.expiresAt <= this.now()) {
|
|
467
|
+
return unavailableResult("codex", "codex_pair_expired");
|
|
468
|
+
}
|
|
469
|
+
const launchUrl = codexPairingLaunchUrl(pairing.pairingCode);
|
|
470
|
+
if (launchUrl === null) {
|
|
471
|
+
return unavailableResult("codex", "codex_pair_failed");
|
|
472
|
+
}
|
|
473
|
+
if (action === "open" && paired)
|
|
474
|
+
return openResult("codex", launchUrl);
|
|
475
|
+
if (pairing.manualPairingCode === null) {
|
|
476
|
+
return unavailableResult("codex", "codex_manual_pair_unavailable");
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
provider: "codex",
|
|
480
|
+
outcome: "pair",
|
|
481
|
+
launchUrl,
|
|
482
|
+
manualPairingCode: pairing.manualPairingCode,
|
|
483
|
+
expiresAt: pairing.expiresAt,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
async performCodexViaAppServer(action, automaticEnable, paired) {
|
|
487
|
+
const remote = this.codexRemoteControl;
|
|
488
|
+
if (remote === null)
|
|
489
|
+
return unavailableResult("codex", "codex_app_server_remote_unavailable");
|
|
490
|
+
if (action === "stop") {
|
|
491
|
+
const stopped = await remote.disableRemoteControl();
|
|
492
|
+
return stopped?.status === "disabled"
|
|
493
|
+
? { provider: "codex", outcome: "stopped" }
|
|
494
|
+
: unavailableResult("codex", "codex_stop_failed");
|
|
495
|
+
}
|
|
496
|
+
if (!automaticEnable) {
|
|
497
|
+
return unavailableResult("codex", "codex_automatic_enable_disabled");
|
|
498
|
+
}
|
|
499
|
+
const enabled = await remote.enableRemoteControl();
|
|
500
|
+
if (enabled === null || enabled.status === "disabled") {
|
|
501
|
+
return unavailableResult("codex", "codex_start_failed");
|
|
502
|
+
}
|
|
503
|
+
// errored は同じ enrollment の別 App Server が既に online の場合にも返る。この場合も
|
|
504
|
+
// pairing artifact は同じ environment へ発行できるため、pair/open を阻害しない。
|
|
505
|
+
const pairing = await remote.startRemoteControlPairing();
|
|
506
|
+
if (pairing === null)
|
|
507
|
+
return unavailableResult("codex", "codex_pair_failed");
|
|
508
|
+
if (pairing.expiresAt <= this.now()) {
|
|
509
|
+
return unavailableResult("codex", "codex_pair_expired");
|
|
510
|
+
}
|
|
511
|
+
const launchUrl = codexPairingLaunchUrl(pairing.pairingCode);
|
|
512
|
+
if (launchUrl === null) {
|
|
513
|
+
return unavailableResult("codex", "codex_pair_failed");
|
|
514
|
+
}
|
|
515
|
+
if (action === "open" && paired)
|
|
516
|
+
return openResult("codex", launchUrl);
|
|
517
|
+
const manual = pairing.manualPairingCode;
|
|
518
|
+
if (manual === null ||
|
|
519
|
+
Buffer.byteLength(manual, "utf8") > 64 ||
|
|
520
|
+
!/^[A-Z0-9-]{4,64}$/u.test(manual)) {
|
|
521
|
+
return unavailableResult("codex", "codex_manual_pair_unavailable");
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
provider: "codex",
|
|
525
|
+
outcome: "pair",
|
|
526
|
+
launchUrl,
|
|
527
|
+
manualPairingCode: manual,
|
|
528
|
+
expiresAt: pairing.expiresAt,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
acquireActionLock() {
|
|
532
|
+
fs.mkdirSync(path.dirname(this.actionLockPath), { recursive: true, mode: 0o700 });
|
|
533
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
534
|
+
try {
|
|
535
|
+
const fd = fs.openSync(this.actionLockPath, "wx", 0o600);
|
|
536
|
+
fs.writeFileSync(fd, `${process.pid}\n`);
|
|
537
|
+
return () => {
|
|
538
|
+
try {
|
|
539
|
+
fs.closeSync(fd);
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// 既に閉じていても後続 unlink を試す。
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
fs.unlinkSync(this.actionLockPath);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
// プロセス終了時など、既に回収済みなら無視。
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
if (error.code !== "EEXIST")
|
|
554
|
+
return null;
|
|
555
|
+
const owner = readLockPid(this.actionLockPath);
|
|
556
|
+
if (owner !== null && processIsAlive(owner))
|
|
557
|
+
return null;
|
|
558
|
+
try {
|
|
559
|
+
fs.unlinkSync(this.actionLockPath);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function captureOfficialPane(sessionManager, session) {
|
|
570
|
+
try {
|
|
571
|
+
// 再接続時のバナーは新規行ではなく「初回 activation の位置」に復元される
|
|
572
|
+
// (実測)。長い会話でも拾えるよう窓を広めに取る(MAX_OUTPUT_BYTES 内)。
|
|
573
|
+
const text = await sessionManager.capturePane(session, {
|
|
574
|
+
lines: 600,
|
|
575
|
+
joinWrappedLines: true,
|
|
576
|
+
});
|
|
577
|
+
return Buffer.byteLength(text, "utf8") <= MAX_OUTPUT_BYTES ? text : null;
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* pane 最下部のステータスバーに `/rc` インジケータが点灯しているか。
|
|
585
|
+
* バナー文言はチャット本文の引用(デバッグ表示・ユーザーの貼り付け)で偽陽性に
|
|
586
|
+
* なり得るのに対し、末尾行の TUI クロームは本文が占有できないため活性判定の権威。
|
|
587
|
+
*/
|
|
588
|
+
export function statusBarShowsRemoteControl(text) {
|
|
589
|
+
const lines = text
|
|
590
|
+
.split("\n")
|
|
591
|
+
.map((line) => line.trimEnd())
|
|
592
|
+
.filter((line) => line.trim() !== "");
|
|
593
|
+
// 入力プロンプト行(❯ で始まる)は下書きに `/rc` と打たれ得るため除外する。
|
|
594
|
+
return lines
|
|
595
|
+
.slice(-3)
|
|
596
|
+
.some((line) => !line.trim().startsWith("❯") && /(^|\s)\/rc$/u.test(line));
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* pane 最下部のステータスバーが処理中(`esc to interrupt`)を示しているか。
|
|
600
|
+
* アイドル時のバー(`? for shortcuts`)には現れない実測差分を使う。
|
|
601
|
+
* 本文引用対策として末尾 3 行だけを見る。
|
|
602
|
+
*/
|
|
603
|
+
export function statusBarShowsProcessing(text) {
|
|
604
|
+
const lines = text
|
|
605
|
+
.split("\n")
|
|
606
|
+
.map((line) => line.trimEnd())
|
|
607
|
+
.filter((line) => line.trim() !== "");
|
|
608
|
+
return lines
|
|
609
|
+
.slice(-3)
|
|
610
|
+
.some((line) => !line.trim().startsWith("❯") && line.includes("esc to interrupt"));
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* transcript 末尾から最新の bridge_status(Remote Control activation)行を読む。
|
|
614
|
+
* CLI 自身が書く構造化行なので、pane と違い本文引用による偽陽性がない。
|
|
615
|
+
* URL は同一 CLI プロセス内の再接続で不変(実測)のため、最後の 1 件が現在値。
|
|
616
|
+
*/
|
|
617
|
+
function readTranscriptTail(transcriptPath) {
|
|
618
|
+
try {
|
|
619
|
+
const size = fs.statSync(transcriptPath).size;
|
|
620
|
+
// activation は会話冒頭側にあり得る。巨大 tool 出力で末尾から遠のくため
|
|
621
|
+
// 窓は広めに取る(この取りこぼしを 256KiB で実際に踏んだ)。
|
|
622
|
+
const start = Math.max(0, size - TRANSCRIPT_TAIL_BYTES);
|
|
623
|
+
const fd = fs.openSync(transcriptPath, "r");
|
|
624
|
+
try {
|
|
625
|
+
const buffer = Buffer.alloc(size - start);
|
|
626
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
627
|
+
return buffer.toString("utf8");
|
|
628
|
+
}
|
|
629
|
+
finally {
|
|
630
|
+
fs.closeSync(fd);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 注入以降に /remote-control の実行記録(local_command)が書かれたか。
|
|
639
|
+
* RC 切断直後の limbo では typed 入力が入力欄エコーごと黙殺される(実測)ため、
|
|
640
|
+
* 「注入したのに実行証跡が無い」ことの検出に使う(再注入の判断材料)。
|
|
641
|
+
*/
|
|
642
|
+
export function transcriptHasRemoteControlCommandSince(transcriptPath, sinceMs) {
|
|
643
|
+
if (transcriptPath === null)
|
|
644
|
+
return false;
|
|
645
|
+
const tail = readTranscriptTail(transcriptPath);
|
|
646
|
+
if (tail === null)
|
|
647
|
+
return false;
|
|
648
|
+
for (const line of tail.split("\n").reverse()) {
|
|
649
|
+
if (!line.includes('"local_command"') || !line.includes("/remote-control"))
|
|
650
|
+
continue;
|
|
651
|
+
let entry;
|
|
652
|
+
try {
|
|
653
|
+
entry = JSON.parse(line);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (!isRecord(entry) || entry["subtype"] !== "local_command")
|
|
659
|
+
continue;
|
|
660
|
+
const content = typeof entry["content"] === "string" ? entry["content"] : "";
|
|
661
|
+
if (!content.includes("<command-name>/remote-control</command-name>"))
|
|
662
|
+
continue;
|
|
663
|
+
const timestamp = typeof entry["timestamp"] === "string" ? Date.parse(entry["timestamp"]) : NaN;
|
|
664
|
+
if (!Number.isNaN(timestamp) && timestamp >= sinceMs)
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
export function lastTranscriptRemoteControlEntry(transcriptPath) {
|
|
670
|
+
if (transcriptPath === null)
|
|
671
|
+
return null;
|
|
672
|
+
const tail = readTranscriptTail(transcriptPath);
|
|
673
|
+
if (tail === null)
|
|
674
|
+
return null;
|
|
675
|
+
const lines = tail.split("\n");
|
|
676
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
677
|
+
const line = lines[index] ?? "";
|
|
678
|
+
if (!line.includes('"bridge_status"'))
|
|
679
|
+
continue;
|
|
680
|
+
let entry;
|
|
681
|
+
try {
|
|
682
|
+
entry = JSON.parse(line);
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (!isRecord(entry) || entry["subtype"] !== "bridge_status")
|
|
688
|
+
continue;
|
|
689
|
+
const content = typeof entry["content"] === "string" ? entry["content"] : "";
|
|
690
|
+
if (!content.includes("/remote-control is active"))
|
|
691
|
+
continue;
|
|
692
|
+
const at = content.indexOf(CLAUDE_SESSION_PREFIX);
|
|
693
|
+
if (at < 0)
|
|
694
|
+
continue;
|
|
695
|
+
const raw = content.slice(at).split(/\s/u, 1)[0] ?? "";
|
|
696
|
+
const candidate = raw.endsWith(".") ? raw.slice(0, -1) : raw;
|
|
697
|
+
if (!validClaudeUrl(candidate))
|
|
698
|
+
continue;
|
|
699
|
+
const timestamp = typeof entry["timestamp"] === "string" ? Date.parse(entry["timestamp"]) : NaN;
|
|
700
|
+
return { url: candidate, atMs: Number.isNaN(timestamp) ? null : timestamp };
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* pane から activation バナーの URL を拾う最終フォールバック。行頭(trim 後)が
|
|
706
|
+
* activation 文言で始まる行だけを banner とみなし、同一行 or 直後行の URL を採る。
|
|
707
|
+
* 本文引用への完全な耐性はないため、transcript(bridge_status) が引けない場合のみ使う。
|
|
708
|
+
*/
|
|
709
|
+
export function extractBannerClaudeUrl(text) {
|
|
710
|
+
const lines = text.split("\n");
|
|
711
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
712
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
713
|
+
if (!trimmed.startsWith("/remote-control is active") &&
|
|
714
|
+
!trimmed.startsWith("Remote Control is active")) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const sameLineAt = trimmed.indexOf(CLAUDE_SESSION_PREFIX);
|
|
718
|
+
if (sameLineAt >= 0) {
|
|
719
|
+
const candidate = trimmed.slice(sameLineAt).split(/\s/u, 1)[0] ?? "";
|
|
720
|
+
if (validClaudeUrl(candidate))
|
|
721
|
+
return candidate;
|
|
722
|
+
}
|
|
723
|
+
for (let next = index + 1; next < Math.min(lines.length, index + 3); next += 1) {
|
|
724
|
+
const nextTrimmed = (lines[next] ?? "").trim();
|
|
725
|
+
if (nextTrimmed === "")
|
|
726
|
+
continue;
|
|
727
|
+
if (!nextTrimmed.startsWith(CLAUDE_SESSION_PREFIX))
|
|
728
|
+
break;
|
|
729
|
+
const candidate = nextTrimmed.split(/\s/u, 1)[0] ?? "";
|
|
730
|
+
if (validClaudeUrl(candidate))
|
|
731
|
+
return candidate;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
export function extractClaudeRemoteDialogUrl(text) {
|
|
738
|
+
// チャット本文がダイアログ文言を引用しただけの偽陽性を避けるため、substring では
|
|
739
|
+
// なく trim 後の行単位で照合する(実ダイアログは各要素が行頭に独立して並ぶ)。
|
|
740
|
+
const lines = text.split("\n").map((line) => line.trim());
|
|
741
|
+
if (!lines.some((line) => line === CLAUDE_DIALOG_DISCONNECT_LABEL))
|
|
742
|
+
return null;
|
|
743
|
+
if (!lines.some((line) => line.startsWith("Enter to select")))
|
|
744
|
+
return null;
|
|
745
|
+
const urlLine = [...lines]
|
|
746
|
+
.reverse()
|
|
747
|
+
.find((line) => line.startsWith(CLAUDE_DIALOG_URL_PREFIX));
|
|
748
|
+
if (urlLine === undefined)
|
|
749
|
+
return null;
|
|
750
|
+
const raw = urlLine.slice(CLAUDE_DIALOG_URL_PREFIX.length).split(/\s/u, 1)[0] ?? "";
|
|
751
|
+
const candidate = raw.endsWith(".") ? raw.slice(0, -1) : raw;
|
|
752
|
+
return validClaudeUrl(candidate) ? candidate : null;
|
|
753
|
+
}
|
|
754
|
+
export function validClaudeUrl(value) {
|
|
755
|
+
if (!value.startsWith(CLAUDE_SESSION_PREFIX))
|
|
756
|
+
return false;
|
|
757
|
+
const identifier = value.slice(CLAUDE_SESSION_PREFIX.length);
|
|
758
|
+
return (identifier.length > 0 &&
|
|
759
|
+
Buffer.byteLength(identifier, "utf8") <= MAX_IDENTIFIER_BYTES &&
|
|
760
|
+
/^[A-Za-z0-9_-]+$/u.test(identifier));
|
|
761
|
+
}
|
|
762
|
+
export function classifyClaudeFailure(text) {
|
|
763
|
+
return CLAUDE_FAILURES.find(([needle]) => text.includes(needle))?.[1] ?? null;
|
|
764
|
+
}
|
|
765
|
+
function parseClaudeAuth(text) {
|
|
766
|
+
const value = parseStrictObject(text, [
|
|
767
|
+
"loggedIn",
|
|
768
|
+
"authMethod",
|
|
769
|
+
"apiProvider",
|
|
770
|
+
"email",
|
|
771
|
+
"orgId",
|
|
772
|
+
"orgName",
|
|
773
|
+
"subscriptionType",
|
|
774
|
+
]);
|
|
775
|
+
if (value === null)
|
|
776
|
+
return null;
|
|
777
|
+
if (typeof value["loggedIn"] !== "boolean" ||
|
|
778
|
+
!boundedText(value["authMethod"]) ||
|
|
779
|
+
!boundedText(value["apiProvider"]) ||
|
|
780
|
+
!boundedText(value["email"], 512) ||
|
|
781
|
+
!boundedText(value["orgId"], 512) ||
|
|
782
|
+
!boundedText(value["orgName"], 512) ||
|
|
783
|
+
!boundedText(value["subscriptionType"])) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
loggedIn: value["loggedIn"],
|
|
788
|
+
authMethod: value["authMethod"],
|
|
789
|
+
apiProvider: value["apiProvider"],
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
export function parseCodexStart(text) {
|
|
793
|
+
const value = parseStrictObject(text, [
|
|
794
|
+
"mode",
|
|
795
|
+
"status",
|
|
796
|
+
"serverName",
|
|
797
|
+
"environmentId",
|
|
798
|
+
"timedOut",
|
|
799
|
+
"daemon",
|
|
800
|
+
]);
|
|
801
|
+
if (value === null || value["mode"] !== "daemon")
|
|
802
|
+
return null;
|
|
803
|
+
const status = value["status"];
|
|
804
|
+
if (status !== "connected" && status !== "connecting")
|
|
805
|
+
return null;
|
|
806
|
+
if (!boundedText(value["serverName"], 256))
|
|
807
|
+
return null;
|
|
808
|
+
if (value["environmentId"] !== null &&
|
|
809
|
+
value["environmentId"] !== undefined &&
|
|
810
|
+
!boundedIdentifier(value["environmentId"])) {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
if (typeof value["timedOut"] !== "boolean" || (status === "connected" && value["timedOut"])) {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
return validCodexDaemon(value["daemon"]) ? status : null;
|
|
817
|
+
}
|
|
818
|
+
function validCodexDaemon(value) {
|
|
819
|
+
if (!isRecord(value))
|
|
820
|
+
return false;
|
|
821
|
+
const status = value["status"];
|
|
822
|
+
if (status === "bootstrapped") {
|
|
823
|
+
if (!hasOnlyKeys(value, [
|
|
824
|
+
"status",
|
|
825
|
+
"backend",
|
|
826
|
+
"autoUpdateEnabled",
|
|
827
|
+
"remoteControlEnabled",
|
|
828
|
+
"managedCodexPath",
|
|
829
|
+
"managedCodexVersion",
|
|
830
|
+
"socketPath",
|
|
831
|
+
"cliVersion",
|
|
832
|
+
"appServerVersion",
|
|
833
|
+
])) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
return (boundedText(value["backend"]) &&
|
|
837
|
+
typeof value["autoUpdateEnabled"] === "boolean" &&
|
|
838
|
+
value["remoteControlEnabled"] === true &&
|
|
839
|
+
validCodexPaths(value["managedCodexPath"], value["socketPath"]) &&
|
|
840
|
+
validOptionalText(value["managedCodexVersion"]) &&
|
|
841
|
+
boundedText(value["cliVersion"]) &&
|
|
842
|
+
boundedText(value["appServerVersion"]));
|
|
843
|
+
}
|
|
844
|
+
if (!["alreadyRunning", "started", "restarted", "running"].includes(String(status))) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (!hasOnlyKeys(value, [
|
|
848
|
+
"status",
|
|
849
|
+
"backend",
|
|
850
|
+
"pid",
|
|
851
|
+
"managedCodexPath",
|
|
852
|
+
"managedCodexVersion",
|
|
853
|
+
"socketPath",
|
|
854
|
+
"cliVersion",
|
|
855
|
+
"appServerVersion",
|
|
856
|
+
])) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
return ((value["backend"] === undefined || boundedText(value["backend"])) &&
|
|
860
|
+
(value["pid"] === undefined ||
|
|
861
|
+
(typeof value["pid"] === "number" && Number.isInteger(value["pid"]) && value["pid"] > 0)) &&
|
|
862
|
+
validCodexPaths(value["managedCodexPath"], value["socketPath"]) &&
|
|
863
|
+
validOptionalText(value["managedCodexVersion"]) &&
|
|
864
|
+
validOptionalText(value["cliVersion"]) &&
|
|
865
|
+
validOptionalText(value["appServerVersion"]));
|
|
866
|
+
}
|
|
867
|
+
export function parseCodexPairing(text) {
|
|
868
|
+
const value = parseStrictObject(text, [
|
|
869
|
+
"pairingCode",
|
|
870
|
+
"manualPairingCode",
|
|
871
|
+
"environmentId",
|
|
872
|
+
"expiresAt",
|
|
873
|
+
]);
|
|
874
|
+
if (value === null ||
|
|
875
|
+
!boundedSecret(value["pairingCode"], MAX_PAIRING_CODE_BYTES) ||
|
|
876
|
+
!boundedIdentifier(value["environmentId"]) ||
|
|
877
|
+
typeof value["expiresAt"] !== "number" ||
|
|
878
|
+
!Number.isInteger(value["expiresAt"]) ||
|
|
879
|
+
value["expiresAt"] <= 0) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
const code = value["manualPairingCode"];
|
|
883
|
+
if (code !== null && code !== undefined) {
|
|
884
|
+
if (typeof code !== "string" || !/^[A-Z0-9-]{4,64}$/u.test(code))
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
pairingCode: value["pairingCode"],
|
|
889
|
+
manualPairingCode: typeof code === "string" ? code : null,
|
|
890
|
+
expiresAt: value["expiresAt"],
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function codexPairingLaunchUrl(pairingCode) {
|
|
894
|
+
if (!boundedSecret(pairingCode, MAX_PAIRING_CODE_BYTES))
|
|
895
|
+
return null;
|
|
896
|
+
const url = new URL(CHATGPT_CODEX_PAIR_URL);
|
|
897
|
+
url.searchParams.set("pairing_code", pairingCode);
|
|
898
|
+
return url.toString();
|
|
899
|
+
}
|
|
900
|
+
export function parseCodexStop(text) {
|
|
901
|
+
const value = parseStrictObject(text, [
|
|
902
|
+
"status",
|
|
903
|
+
"backend",
|
|
904
|
+
"pid",
|
|
905
|
+
"managedCodexPath",
|
|
906
|
+
"managedCodexVersion",
|
|
907
|
+
"socketPath",
|
|
908
|
+
"cliVersion",
|
|
909
|
+
"appServerVersion",
|
|
910
|
+
]);
|
|
911
|
+
if (value === null || (value["status"] !== "stopped" && value["status"] !== "notRunning")) {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
return ((value["backend"] === undefined || boundedText(value["backend"])) &&
|
|
915
|
+
(value["pid"] === undefined ||
|
|
916
|
+
(typeof value["pid"] === "number" && Number.isInteger(value["pid"]) && value["pid"] > 0)) &&
|
|
917
|
+
validCodexPaths(value["managedCodexPath"], value["socketPath"]) &&
|
|
918
|
+
validOptionalText(value["managedCodexVersion"]) &&
|
|
919
|
+
validOptionalText(value["cliVersion"]) &&
|
|
920
|
+
validOptionalText(value["appServerVersion"]));
|
|
921
|
+
}
|
|
922
|
+
function parseStrictObject(text, keys) {
|
|
923
|
+
try {
|
|
924
|
+
const value = JSON.parse(text);
|
|
925
|
+
return isRecord(value) && hasOnlyKeys(value, keys) ? value : null;
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function isRecord(value) {
|
|
932
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
933
|
+
}
|
|
934
|
+
function hasOnlyKeys(value, keys) {
|
|
935
|
+
const allowed = new Set(keys);
|
|
936
|
+
return Object.keys(value).every((key) => allowed.has(key)) &&
|
|
937
|
+
keys.every((key) => Object.hasOwn(value, key) || optionalCodexField(key));
|
|
938
|
+
}
|
|
939
|
+
function optionalCodexField(key) {
|
|
940
|
+
return [
|
|
941
|
+
"environmentId",
|
|
942
|
+
"backend",
|
|
943
|
+
"pid",
|
|
944
|
+
"managedCodexVersion",
|
|
945
|
+
"cliVersion",
|
|
946
|
+
"appServerVersion",
|
|
947
|
+
"manualPairingCode",
|
|
948
|
+
].includes(key);
|
|
949
|
+
}
|
|
950
|
+
function validCodexPaths(binary, socket) {
|
|
951
|
+
return (typeof binary === "string" &&
|
|
952
|
+
typeof socket === "string" &&
|
|
953
|
+
path.isAbsolute(binary) &&
|
|
954
|
+
path.isAbsolute(socket) &&
|
|
955
|
+
Buffer.byteLength(binary, "utf8") <= 4_096 &&
|
|
956
|
+
Buffer.byteLength(socket, "utf8") <= 4_096);
|
|
957
|
+
}
|
|
958
|
+
function boundedIdentifier(value) {
|
|
959
|
+
return (typeof value === "string" &&
|
|
960
|
+
value.length > 0 &&
|
|
961
|
+
Buffer.byteLength(value, "utf8") <= MAX_IDENTIFIER_BYTES &&
|
|
962
|
+
/^[A-Za-z0-9_.-]+$/u.test(value));
|
|
963
|
+
}
|
|
964
|
+
function boundedText(value, maximum = MAX_IDENTIFIER_BYTES) {
|
|
965
|
+
return (typeof value === "string" &&
|
|
966
|
+
value.length > 0 &&
|
|
967
|
+
Buffer.byteLength(value, "utf8") <= maximum &&
|
|
968
|
+
!/[\u0000-\u001F\u007F]/u.test(value));
|
|
969
|
+
}
|
|
970
|
+
function validOptionalText(value) {
|
|
971
|
+
return value === null || value === undefined || boundedText(value);
|
|
972
|
+
}
|
|
973
|
+
function boundedSecret(value, maximum) {
|
|
974
|
+
return (typeof value === "string" &&
|
|
975
|
+
value.length > 0 &&
|
|
976
|
+
Buffer.byteLength(value, "utf8") <= maximum &&
|
|
977
|
+
!/[\s\u0000-\u001F\u007F]/u.test(value));
|
|
978
|
+
}
|
|
979
|
+
function classifySpawnSuccess(code, signal) {
|
|
980
|
+
return code === 0 && signal === null;
|
|
981
|
+
}
|
|
982
|
+
async function runFixedCommand(executable, args, timeoutMs) {
|
|
983
|
+
return new Promise((resolve) => {
|
|
984
|
+
let settled = false;
|
|
985
|
+
let overflow = false;
|
|
986
|
+
const stdout = [];
|
|
987
|
+
const stderr = [];
|
|
988
|
+
let stdoutBytes = 0;
|
|
989
|
+
let stderrBytes = 0;
|
|
990
|
+
const child = spawn(executable, [...args], {
|
|
991
|
+
cwd: os.homedir(),
|
|
992
|
+
// SSH 経由で spawn された engine は非ログインシェルの PATH(~/.local/bin 等を
|
|
993
|
+
// 含まない)を継承し、`claude` 解決に失敗して official_cli_unavailable になる
|
|
994
|
+
// (QUIC gw 経由は --path 注入済みで成功する、という transport 依存の実障害)。
|
|
995
|
+
// launch と同じ既知ディレクトリを前置して両 transport で解決を揃える。
|
|
996
|
+
env: {
|
|
997
|
+
...process.env,
|
|
998
|
+
PATH: [defaultInjectedPath(), process.env["PATH"] ?? ""].filter(Boolean).join(":"),
|
|
999
|
+
},
|
|
1000
|
+
shell: false,
|
|
1001
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1002
|
+
});
|
|
1003
|
+
const finish = (value) => {
|
|
1004
|
+
if (settled)
|
|
1005
|
+
return;
|
|
1006
|
+
settled = true;
|
|
1007
|
+
clearTimeout(timer);
|
|
1008
|
+
resolve(value);
|
|
1009
|
+
};
|
|
1010
|
+
const append = (chunks, chunk, current) => {
|
|
1011
|
+
const next = current + chunk.length;
|
|
1012
|
+
if (next > MAX_OUTPUT_BYTES) {
|
|
1013
|
+
overflow = true;
|
|
1014
|
+
child.kill("SIGKILL");
|
|
1015
|
+
return next;
|
|
1016
|
+
}
|
|
1017
|
+
chunks.push(chunk);
|
|
1018
|
+
return next;
|
|
1019
|
+
};
|
|
1020
|
+
child.stdout.on("data", (chunk) => {
|
|
1021
|
+
stdoutBytes = append(stdout, chunk, stdoutBytes);
|
|
1022
|
+
});
|
|
1023
|
+
child.stderr.on("data", (chunk) => {
|
|
1024
|
+
stderrBytes = append(stderr, chunk, stderrBytes);
|
|
1025
|
+
});
|
|
1026
|
+
child.once("error", () => finish(null));
|
|
1027
|
+
child.once("close", (code, signal) => {
|
|
1028
|
+
if (overflow)
|
|
1029
|
+
return finish(null);
|
|
1030
|
+
finish({
|
|
1031
|
+
success: classifySpawnSuccess(code, signal),
|
|
1032
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
1033
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
const timer = setTimeout(() => {
|
|
1037
|
+
child.kill("SIGKILL");
|
|
1038
|
+
finish(null);
|
|
1039
|
+
}, timeoutMs);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
function readLockPid(lockPath) {
|
|
1043
|
+
try {
|
|
1044
|
+
const value = Number.parseInt(fs.readFileSync(lockPath, "utf8").trim(), 10);
|
|
1045
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function processIsAlive(pid) {
|
|
1052
|
+
try {
|
|
1053
|
+
process.kill(pid, 0);
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
return error.code === "EPERM";
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function unavailableStatus(provider, reason, version) {
|
|
1061
|
+
return {
|
|
1062
|
+
provider,
|
|
1063
|
+
...(version !== undefined ? { version } : {}),
|
|
1064
|
+
state: "unavailable",
|
|
1065
|
+
canOpen: false,
|
|
1066
|
+
canStart: false,
|
|
1067
|
+
unavailableReason: reason,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function unavailableResult(provider, reason) {
|
|
1071
|
+
return { provider, outcome: "unavailable", unavailableReason: reason };
|
|
1072
|
+
}
|
|
1073
|
+
function openResult(provider, launchUrl) {
|
|
1074
|
+
return { provider, outcome: "open", launchUrl };
|
|
1075
|
+
}
|
|
1076
|
+
function delay(milliseconds) {
|
|
1077
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1078
|
+
}
|
|
1079
|
+
//# sourceMappingURL=officialApps.js.map
|