@bash0816/copilot-termux 1.0.83 → 1.0.89

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.
@@ -1,1633 +0,0 @@
1
- 'use strict';
2
- // Override process.platform so @github/copilot treats Android as Linux.
3
- // @github/copilot is distributed unmodified (LICENSE Section 2).
4
- // This file is our code and is NOT part of the @github/copilot software.
5
- Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
6
-
7
- if (process.report) {
8
- const orig = process.report.getReport.bind(process.report);
9
- process.report.getReport = (...args) => {
10
- const r = orig(...args);
11
- if (r && r.header) r.header.platform = 'linux';
12
- return r;
13
- };
14
- }
15
-
16
- // Redirect linux-arm64 native addons to bionic-compatible variants.
17
- // bionic Node.js cannot load glibc addons; glibc fallback causes segfault.
18
- const Module = require('module');
19
- const fs = require('fs');
20
- const path = require('path');
21
- const { execFile } = require('child_process');
22
- const { promisify } = require('util');
23
- const execFileAsync = promisify(execFile);
24
-
25
- // git CLIを配列引数で呼ぶ(shell経由なし、インジェクション対策)。失敗時は例外を投げず安全な既定値を返す。
26
- async function runGit(cwd, args) {
27
- try {
28
- const { stdout } = await execFileAsync('git', args, { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
29
- return stdout;
30
- } catch (_) {
31
- return null;
32
- }
33
- }
34
-
35
- // backupFile() は戻り値を `${r.substring(9,25)}-${Date.now()}` としてバックアップファイル名に使う。
36
- // substring(9,25) は "git-sha1:" (9文字) プレフィックスを除いた先頭16文字を取る計算であり、
37
- // ネイティブ実装の実際の戻り値フォーマットが "git-sha1:<40hex>" であることを示している。
38
- // 互換性のため git hash-object 相当(blob SHA1)を "git-sha1:" プレフィックス付きで返す。
39
- async function hashFileContent(gitRoot, filePath) {
40
- try {
41
- const out = await runGit(gitRoot, ['hash-object', '--', filePath]);
42
- if (!out) return null;
43
- return `git-sha1:${out.trim()}`;
44
- } catch (_) {
45
- return null;
46
- }
47
- }
48
-
49
- // GIT-ASYNC-002: bionicフォールバック限定のSIGSEGV(gitLegacyRemotesAsync/gitWorkingDirectoryContextAsync/
50
- // gitRepoIdentifierAtPathAsync、いずれもRust tokio非同期)をJSスタブで置換するための共通ヘルパー。
51
- // git CLI(`git remote -v`)の出力をパースしてリモート一覧を取得する。shell経由なし(execFileAsync配列引数)。
52
- async function listGitRemotes(gitRoot) {
53
- const out = await runGit(gitRoot, ['remote', '-v']);
54
- if (!out) return [];
55
- const seen = new Set();
56
- const remotes = [];
57
- for (const line of out.split('\n')) {
58
- const m = /^(\S+)\s+(\S+)\s+\(fetch\)$/.exec(line);
59
- if (!m) continue;
60
- const [, name, url] = m;
61
- if (seen.has(name)) continue;
62
- seen.add(name);
63
- remotes.push({ Name: name, FetchURL: url });
64
- }
65
- return remotes;
66
- }
67
-
68
- // git remote URLをowner/name/hostへパースする。https/ssh/scp-like形式に対応。
69
- // パース不能な場合はnullを返す(呼び出し側はrepository情報なしとして扱う)。
70
- function parseGitRemoteUrl(url) {
71
- if (!url) return null;
72
- if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(url)) {
73
- // scp-like: [user@]host:path (例: git@github.com:owner/repo.git)
74
- const m = /^(?:[^@/]+@)?([^:/]+):(.+)$/.exec(url);
75
- if (!m) return null;
76
- const host = m[1];
77
- const parts = m[2].replace(/\.git$/, '').split('/').filter(Boolean);
78
- if (parts.length < 2) return null;
79
- return { host, owner: parts[parts.length - 2], name: parts[parts.length - 1] };
80
- }
81
- try {
82
- const u = new URL(url);
83
- const parts = u.pathname.replace(/\.git$/, '').split('/').filter(Boolean);
84
- if (parts.length < 2) return null;
85
- return { host: u.hostname, owner: parts[parts.length - 2], name: parts[parts.length - 1] };
86
- } catch (_) {
87
- return null;
88
- }
89
- }
90
-
91
- // origin(なければ先頭のリモート)からリポジトリ識別子を解決する。
92
- // vendor実装(app.jsの`tge()`/`tmr()`相当)に合わせ、GitHub Cloud(github.com/*.ghe.com)以外は
93
- // hostTypeを'other'とする(自己ホストGHE Serverの判定手がかりがないのは実装側と同じ制約)。
94
- async function resolveRepoIdentifier(gitRoot) {
95
- const remotes = await listGitRemotes(gitRoot);
96
- if (!remotes.length) return null;
97
- const origin = remotes.find((r) => r.Name === 'origin') || remotes[0];
98
- const parsed = parseGitRemoteUrl(origin.FetchURL);
99
- if (!parsed) return null;
100
- const hostType = (parsed.host === 'github.com' || parsed.host.endsWith('.ghe.com')) ? 'github' : 'other';
101
- return { identifier: `${parsed.owner}/${parsed.name}`, hostType, host: parsed.host };
102
- }
103
-
104
- // working directory context: cwdからgitRoot/branch/repository情報を解決する。
105
- // vendor実装(gitWorkingDirectoryContextAsync)と同じく、非git配下ではcwdのみを持つ
106
- // オブジェクトを返す(nullは返さない。nullを返すと呼び出し側で`.gitRoot`アクセス時に
107
- // TypeErrorが発生し無応答exit 0になることを実機で確認済み)。
108
- async function buildWorkingDirectoryContext(cwd) {
109
- const result = { cwd };
110
- const rootOut = await runGit(cwd, ['rev-parse', '--show-toplevel']);
111
- const gitRoot = rootOut ? rootOut.trim() : null;
112
- if (!gitRoot) return result;
113
- result.gitRoot = gitRoot;
114
- const branchOut = await runGit(gitRoot, ['branch', '--show-current']);
115
- const branch = branchOut ? branchOut.trim() : '';
116
- if (branch) result.branch = branch;
117
- const identifier = await resolveRepoIdentifier(gitRoot);
118
- if (identifier) {
119
- result.repository = identifier.identifier;
120
- result.hostType = identifier.hostType;
121
- result.repositoryHost = identifier.host;
122
- }
123
- return result;
124
- }
125
-
126
- const _pkgVersion = (() => {
127
- try { return require(path.join(__dirname, '..', 'package.json')).version; } catch (_) { return '1.0.65'; }
128
- })();
129
-
130
- // Bundled Termux-native pty.node (built against bionic, not glibc).
131
- const NATIVE_PTY = path.join(__dirname, 'native', 'pty.node');
132
-
133
- const origResolve = Module._resolveFilename.bind(Module);
134
- Module._resolveFilename = function (request, parent, isMain, options) {
135
- if (typeof request === 'string' && request.endsWith('.node')) {
136
- // Redirect pty.node to our bundled Termux-native build.
137
- if (path.basename(request) === 'pty.node' &&
138
- (request.includes('linux-arm64') || request.includes('linuxmusl-arm64'))) {
139
- // glibc mode では bionic NDK pty.node を使わない(glibc Node との ABI 不整合)
140
- if (!process.env.COPILOT_TERMUX_GLIBC_MODE && fs.existsSync(NATIVE_PTY)) return NATIVE_PTY;
141
- }
142
-
143
- // Redirect other linux-arm64 addons to linuxmusl-arm64 variants.
144
- // In glibc mode (COPILOT_TERMUX_GLIBC_MODE=1), skip redirect — glibc addons load natively.
145
- if (request.includes('linux-arm64') && !process.env.COPILOT_TERMUX_GLIBC_MODE) {
146
- const muslReq = request.replace(/linux-arm64/g, 'linuxmusl-arm64');
147
- try {
148
- const resolved = origResolve(muslReq, parent, isMain, options);
149
- if (fs.existsSync(resolved)) return resolved;
150
- } catch (e) {
151
- if (e.code !== 'MODULE_NOT_FOUND') throw e;
152
- }
153
- // Block glibc fallback — loading linux-arm64 (glibc) on bionic causes segfault.
154
- throw new Error(
155
- `[copilot-termux] Cannot load glibc addon on bionic: ${request}\n` +
156
- 'No linuxmusl-arm64 variant available. This addon is not supported on Android.'
157
- );
158
- }
159
- }
160
- return origResolve(request, parent, isMain, options);
161
- };
162
-
163
- // [glibc mode の設計方針]
164
- // glibc mode(COPILOT_TERMUX_GLIBC_MODE=1)は「native実装に全面的に任せるモード」ではない。
165
- // 正しくは「glibc addon のロードを可能にしつつ、Copilot-Termux が検証済みの
166
- // JS通信/認証/モデル解決の経路は両モード共通で維持するモード」である。
167
- // 常時適用されるJSスタブ(isGlibcMode分岐がない箇所)は、bionic回避だけでなく
168
- // Copilot token交換・Free/Enterprise endpoint補正・モデルstale対策・
169
- // tools payload補正・stream互換維持を兼ねているため、glibc modeでも意図的に残す。
170
- // native実装に戻す変更は、スタブ単位ではなくauth/network/model/streamの連鎖単位で
171
- // 実機検証してから行う(2026-07-06 GPT-5.5レビュー結論)。
172
-
173
- // [Android bionic 対応] linuxmusl-arm64/runtime.node は Rust tokio を使う。
174
- // bionic 上で musl pthread ABI でスレッドを生成すると TUI 起動時に SIGSEGV。
175
- // runtime.node ロード後に Rust async 初期化関数を no-op に差し替えて阻止する。
176
- const origLoad = Module._load.bind(Module);
177
- Module._load = function (request, parent, isMain) {
178
- const result = origLoad(request, parent, isMain);
179
- if (typeof request === 'string' &&
180
- path.basename(request) === 'runtime.node') {
181
- if (result.__copilotTermuxPatched) return result;
182
- let _nfIdSeq = 3e6;
183
- const isGlibcMode = !!process.env.COPILOT_TERMUX_GLIBC_MODE;
184
- if (!isGlibcMode) {
185
- // MCP-BIONIC-001/CAPI-RENAME-001: preflight — 実際にexportを書き換える前に、
186
- // 全bionicガードグループの存在を確認する。1グループでも全alias欠落していれば、
187
- // TOKIO_PATTERN一括no-op化を含む以降の変更を一切行わずthrowする(原子性)。
188
- const BIONIC_SIGSEGV_STUB_GROUPS = [
189
- ['capiClientRetrieveAvailableModels', 'capiClientRetrieveModelsForAuth'], // CAPI-RENAME-001: upstream 1.0.76でrename
190
- ['mcpClientConnectStreamableHttpWithHandlersAndOnclose'],
191
- ['mcpNativeHostConnect'], // MCP-BIONIC-002: bionic上でMCPサーバー接続処理が
192
- // tokioワーカースレッド内でSIGSEGVするため、既存パターンと同型でクリーンなthrowに変換する
193
- ];
194
- const _missingGroups = [];
195
- const _groupTargets = [];
196
- for (const group of BIONIC_SIGSEGV_STUB_GROUPS) {
197
- const foundNames = group.filter(name => typeof result[name] === 'function');
198
- if (foundNames.length === 0) {
199
- _missingGroups.push(group.join('/'));
200
- } else {
201
- _groupTargets.push(foundNames);
202
- }
203
- }
204
- if (_missingGroups.length > 0) {
205
- throw new Error(
206
- `[copilot-termux] BIONIC_SIGSEGV_STUB_GROUPS target(s) not found in runtime.node: ${_missingGroups.join(', ')}. ` +
207
- 'This likely means upstream renamed/removed all known aliases for this guard. ' +
208
- 'Re-audit before releasing for bionic.'
209
- );
210
- }
211
-
212
- // preflight通過後、ここから実際にresultをin-place変更する。
213
- // Rust tokio を使う関数群を no-op に差し替え。
214
- // sessionStore*/sessionSqlite* は非同期 SQLite (tokio)、
215
- // modelHttp*/networkFetch*/ahpRelay*/websocketResponses* は Rust HTTP (tokio)。
216
- // jsonrpcServer* は拡張 JSON-RPC サーバー (ThreadsafeFunction)、
217
- // lspClient* は LSP クライアント (ThreadsafeFunction)。
218
- // featureFlagService* は同期 Rust のため除外(no-op にすると .handle クラッシュ)。
219
- // TOKIO_EXCLUDED: TOKIO_PATTERNの前方一致(lspClient等)に誤って巻き込まれる同期関数の個別除外。
220
- // lspClientOwnedProcessId は同期PID取得関数(1.0.76 G3レビューで発覚、実app.js内で
221
- // `u=h.lspClientOwnedProcessId(d);...{handle:d,pid:u,exit:a}`という同期使用を確認済み)。
222
- const TOKIO_EXCLUDED = new Set([
223
- 'lspClientOwnedProcessId',
224
- ]);
225
- const TOKIO_PATTERN = /^(ahpRelay|ahpRelayAuthenticate|ahpRelayCancelTurn|ahpRelayCoerceCopilotUsage|ahpRelayCompactionDedupeKey|ahpRelayCompleteInput|ahpRelayCompletions|ahpRelayConfirmToolCall|ahpRelayConnect|ahpRelayCreate|ahpRelayCreateSession|ahpRelayDispose|ahpRelayFormatConnectError|ahpRelayFormatResumeNotFound|ahpRelayGetPlan|ahpRelayIsDisplayOnlyEventKind|ahpRelayIsTurnAlreadyActiveRejection|ahpRelayListCheckpoints|ahpRelayListWorkspaceFiles|ahpRelayMapCompletionItemsJson|ahpRelayMapHostSkillsJson|ahpRelayMirrorHostRegistriesJson|ahpRelayNewestCompletedCompactionJson|ahpRelayPendingRequiredResourcesJson|ahpRelayProjectHostSummary|ahpRelayReadCheckpoint|ahpRelayRefreshSessions|ahpRelayReleaseSession|ahpRelayRemovePendingMessage|ahpRelaySelectAgent|ahpRelaySessionDiff|ahpRelaySetMode|ahpRelaySetModel|ahpRelaySetPendingMessage|ahpRelaySetSessionApproveAll|ahpRelaySetTitle|ahpRelayStartTurn|ahpRelayStateJson|ahpRelaySubscribeSession|ahpRelayTerminalDispose|ahpRelayTerminalEnsure|ahpRelayTerminalWrite|jsonrpcServer|jsonrpcServerAddConnection|jsonrpcServerBeginShutdown|jsonrpcServerConnectionClose|jsonrpcServerConnectionNotify|jsonrpcServerConnectionNotifyAfterResponse|jsonrpcServerConnectionRequest|jsonrpcServerConnectionWrite|jsonrpcServerCreate|jsonrpcServerDispatchComplete|jsonrpcServerRegisterHookCallback|jsonrpcServerRegisterSession|jsonrpcServerRemove|jsonrpcServerRemoveSession|jsonrpcServerStartTcpListener|jsonrpcServerStopTcpListener|jsonrpcServerUnregisterHookCallback|jsonrpcServerUnregisterSessionHookCallback|lspClient|lspClientCloseDocument|lspClientCreateOwned|lspClientCreateOwnedSandboxed|lspClientDispose|lspClientEnhanceStartupErrorMessage|lspClientFindSourceFile|lspClientInitialize|lspClientInitialized|lspClientOpenDocument|lspClientOwnedProcessId|lspClientOwnedShutdown|lspClientRequest|lspClientTakeExitInfo|lspClientWaitForDiagnostics|lspClientWaitForProjectLoad|modelHttp|modelHttpCancelRequest|modelHttpRegisterCancellation|modelHttpResetNetworking|networkFetch|networkFetchBuffered|networkFetchGetExtraCaPems|networkFetchNextRequestId|networkFetchParseTransportError|networkFetchRequestCancel|networkFetchResetClients|networkFetchWarmExtraCa|sessionSqlite|sessionSqliteClose|sessionSqliteExec|sessionSqliteFileExists|sessionSqliteOpen|sessionSqliteQuery|sessionSqliteRun|sessionStore|sessionStoreBeginForgeSkillProposalGeneration|sessionStoreClose|sessionStoreCloseAll|sessionStoreCompleteForgeSkillProposalGeneration|sessionStoreDefaultPath|sessionStoreDeleteDynamicContextItem|sessionStoreEnsureSession|sessionStoreExec|sessionStoreExecuteReadOnly|sessionStoreExecuteReadOnlyAsync|sessionStoreExecuteReadOnlyWithCap|sessionStoreFailStaleGeneratingForgeSkillProposals|sessionStoreGetCheckpoints|sessionStoreGetDynamicContextBoard|sessionStoreGetDynamicContextItem|sessionStoreGetFiles|sessionStoreGetForgeSkillProposalByFingerprint|sessionStoreGetForgeSkillProposalById|sessionStoreGetForgeSkillProposalWorkspaceBefore|sessionStoreGetForgeTrajectoryEvents|sessionStoreGetForgeTrajectoryEventsForScope|sessionStoreGetMaxTurnIndex|sessionStoreGetRefs|sessionStoreGetSession|sessionStoreGetStats|sessionStoreGetTurns|sessionStoreIncrementDynamicContextCount|sessionStoreIncrementDynamicContextReadCount|sessionStoreIndexWorkspaceArtifact|sessionStoreInsertAssistantUsageEventWithRuntimeDefaults|sessionStoreInsertCheckpointWithRuntimeDefaults|sessionStoreInsertDynamicContextItem|sessionStoreInsertFileWithRuntimeDefaults|sessionStoreInsertForgeTrajectoryEventWithRuntimeDefaults|sessionStoreInsertRefWithRuntimeDefaults|sessionStoreInsertTurnWithRuntimeDefaults|sessionStoreListForgeSkillProposals|sessionStoreOpen|sessionStoreSearch|sessionStoreTrackingEventOperations|sessionStoreTrackingExtractFilePath|sessionStoreTrackingExtractForgeTrajectoryEvents|sessionStoreTrackingExtractRefsFromBash|sessionStoreTrackingExtractRefsFromMcpTool|sessionStoreTrackingExtractRepoFromMcpTool|sessionStoreTrackingFinalizePostToolUseInput|sessionStoreTrackingFlushOperations|sessionStoreTrackingForgeAgentFeatureFlagName|sessionStoreTrackingInitSessionState|sessionStoreTrackingInitialState|sessionStoreTrackingIsForgeEnabledInEnvironment|sessionStoreTrackingIsForgeTrackingEnabled|sessionStoreTrackingPostToolUsePlan|sessionStoreTransitionForgeSkillProposalStatus|sessionStoreUpsertDynamicContextItem|sessionStoreUpsertSessionWithRuntimeDefaults|websocketResponses|websocketResponsesPersistent)/;
226
- for (const key of Object.keys(result)) {
227
- if (TOKIO_PATTERN.test(key) && !TOKIO_EXCLUDED.has(key) && typeof result[key] === 'function') {
228
- result[key] = () => undefined;
229
- }
230
- }
231
- // networkFetchNextRequestId は networkFetchStreamStart/RequestCancel の相関キーとして
232
- // 実際に使われるため、no-opのままにはできない。TOKIO_PATTERNの一括no-op化で
233
- // 潰された直後にJSの単調増加ID生成で上書きする。
234
- result.networkFetchNextRequestId = () => 'nf-' + (++_nfIdSeq);
235
-
236
- // preflightで確認済みのグループのみスタブ化(この時点で欠落は無いことが保証されている)
237
- for (const foundNames of _groupTargets) {
238
- for (const name of foundNames) {
239
- result[name] = () => {
240
- throw new Error(`${name} unsupported on Android bionic (native tokio disabled to avoid SIGSEGV)`);
241
- };
242
- }
243
- }
244
- // git*Async: Rust tokio async functions — type-safe stubs to prevent SIGSEGV.
245
- // Returns empty/null values matching what app.js callers expect.
246
- const GIT_ASYNC_STUBS = {
247
- gitMutateAsync: async () => undefined,
248
- gitCommandAsync: async () => '',
249
- gitRemotesAsync: async () => [],
250
- gitDiffFileAsync: async () => '',
251
- gitHashFileAsync: async () => [],
252
- gitMergeBaseAsync: async () => null,
253
- gitDiffForRefAsync: async () => '',
254
- gitCurrentBranchAsync: async () => null,
255
- gitDefaultBranchAsync: async () => null,
256
- gitListWorktreesAsync: async () => [],
257
- gitBranchAndHeadAsync: async () => null,
258
- gitSubmodulePathsAsync: async () => [],
259
- gitUntrackedPathsAsync: async () => [],
260
- gitDiffNameStatusAsync: async () => '',
261
- gitStatusPorcelainAsync: async () => '',
262
- gitWorkingTreeStatusAsync: async () => ({ hasUnstagedChanges: false, hasStagedChanges: false, hasUntrackedFiles: false }),
263
- gitStatusPorcelainAllAsync: async () => null,
264
- gitCurrentBranchRemoteAsync: async () => null,
265
- gitWorkingTreeDiffStatsAsync: async () => ({ linesAdded: 0, linesRemoved: 0 }),
266
- gitFindRootWithOptionalWorktreeResolutionAsync: async (cwd) => {
267
- const out = await runGit(cwd || process.cwd(), ['rev-parse', '--show-toplevel']);
268
- const root = out ? out.trim() : null;
269
- return root ? { found: true, gitRoot: root } : { found: false };
270
- },
271
- gitCommitShaAsync: async (gitRoot) => {
272
- const out = await runGit(gitRoot, ['rev-parse', 'HEAD']);
273
- return out ? out.trim() : null;
274
- },
275
- gitStatusFilesAsync: async (gitRoot) => {
276
- // --untracked-files=all: 未追跡ディレクトリを "?? dir/" に畳まず、配下ファイルを個別に列挙させる。
277
- // SnapshotManager.backupFile はディレクトリを保存できないため、ディレクトリ単位の畳み込みだと
278
- // 未追跡ディレクトリ配下のファイルが rollback 対象から漏れる。
279
- const out = await runGit(gitRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
280
- if (!out) return [];
281
- const entries = out.split('\0').filter(Boolean);
282
- const results = [];
283
- for (let i = 0; i < entries.length; i++) {
284
- const entry = entries[i];
285
- const status = entry.slice(0, 2);
286
- const relPath = entry.slice(3);
287
- // rename/copy: X or Y can be 'R'/'C' (index or worktree side); -z format is "to\0from\0"
288
- const isRenameOrCopy = status[0] === 'R' || status[0] === 'C' || status[1] === 'R' || status[1] === 'C';
289
- if (isRenameOrCopy && entries[i + 1] !== undefined) {
290
- i++; // skip the old path field
291
- }
292
- // 削除(D)されたファイルは lstat/hash 対象が存在しないため除外する。
293
- if (status[0] === 'D' || status[1] === 'D') continue;
294
- results.push({ path: path.join(gitRoot, relPath), status });
295
- }
296
- return results;
297
- },
298
- gitHashFilesPrefixedAsync: async (gitRoot, files) => {
299
- const list = Array.isArray(files) ? files : [];
300
- const out = [];
301
- for (const p of list) {
302
- const hash = await hashFileContent(gitRoot, p);
303
- if (hash) out.push({ path: p, hash });
304
- }
305
- return out;
306
- },
307
- gitHashSingleFileAsync: async (gitRoot, filePath) => {
308
- const hash = await hashFileContent(gitRoot, filePath);
309
- return hash || '';
310
- },
311
- gitUntrackedPathsWithOptionalDirectoryAsync: async (gitRoot, opts) => {
312
- const args = ['ls-files', '--others', '--exclude-standard'];
313
- if (opts && opts.directory) args.push('--directory');
314
- const out = await runGit(gitRoot, args);
315
- if (!out) return [];
316
- return out.split('\n').filter(Boolean);
317
- },
318
- gitLegacyRemotesAsync: async (gitRoot) => listGitRemotes(gitRoot),
319
- gitRepoIdentifierAtPathAsync: async (gitRoot) => resolveRepoIdentifier(gitRoot),
320
- gitWorkingDirectoryContextAsync: async (cwd) => buildWorkingDirectoryContext(cwd),
321
- };
322
- for (const [key, stub] of Object.entries(GIT_ASYNC_STUBS)) {
323
- if (typeof result[key] === 'function') result[key] = stub;
324
- }
325
- if (typeof result.registerLogSink === 'function') {
326
- result.registerLogSink = () => { throw new Error('[copilot-termux] registerLogSink disabled on bionic (no tokio thread)'); };
327
- }
328
- if (typeof result.networkFetchGetExtraCaPems === 'function') {
329
- result.networkFetchGetExtraCaPems = () => ({ errors: [], pems: [] });
330
- }
331
- if (typeof result.sessionSqliteOpen === 'function') {
332
- result.sessionSqliteOpen = () => 1;
333
- }
334
- if (typeof result.sessionSqliteQuery === 'function') {
335
- result.sessionSqliteQuery = () => ({ rows: '[]' });
336
- }
337
- if (typeof result.sessionSqliteRun === 'function') {
338
- result.sessionSqliteRun = () => ({ rowsAffected: 0, lastInsertRowid: null });
339
- }
340
- if (typeof result.sessionSqliteFileExists === 'function') {
341
- result.sessionSqliteFileExists = () => false;
342
- }
343
- }
344
- // agentsResolveToolAliases: v1.0.65 新規追加ネイティブ関数。
345
- // Free ユーザーのチャット時に呼ばれ、Bionic で誤ったインデックスを返すと
346
- // tools[N].function.name が undefined → Copilot API 400 になる。
347
- // ネイティブ結果を検証し、無効なら JS 実装でフォールバックする。
348
- if (typeof result.agentsResolveToolAliases === 'function') {
349
- const _nativeResolveAliases = result.agentsResolveToolAliases;
350
- result.agentsResolveToolAliases = function(allowedTools, allToolsMeta, externalToolsMeta) {
351
- try {
352
- const r = _nativeResolveAliases(allowedTools, allToolsMeta, externalToolsMeta);
353
- if (Array.isArray(r) && r.length > 0 &&
354
- r.every(i => typeof i === 'number' && i >= 0 && i < allToolsMeta.length)) {
355
- return r;
356
- }
357
- } catch(e) {
358
- }
359
- // JS フォールバック
360
- const allowed = allowedTools;
361
- // null または ["*"] → 全ツール
362
- if (!allowed || (allowed.length === 1 && allowed[0] === '*')) {
363
- return allToolsMeta.map((_, i) => i);
364
- }
365
- // 空配列 → 空配列(許可ツールなし)
366
- if (allowed.length === 0) {
367
- return [];
368
- }
369
- // 名前マッチング
370
- const allowedSet = new Set(allowed.map(n => (n || '').toLowerCase()));
371
- const indices = [];
372
- allToolsMeta.forEach((tool, i) => {
373
- const name = (tool.name || '').toLowerCase();
374
- const ns = (tool.namespacedName || '').toLowerCase();
375
- if (allowedSet.has(name) || allowedSet.has(ns)) indices.push(i);
376
- });
377
- return indices;
378
- };
379
- }
380
- // authGetCopilotApiUrl: type=token/env/user/gh-cli/api-key では native が null を返す。
381
- // _b() はこれを見て models=[] を返しモデル選択が "No supported model" になる。
382
- // OAuth token でも標準 copilot API URL は固定のため、null 時はデフォルト URL を返す。
383
- if (typeof result.authGetCopilotApiUrl === 'function') {
384
- const _nativeGetCopilotApiUrl = result.authGetCopilotApiUrl;
385
- result.authGetCopilotApiUrl = function(authInfoJson, token) {
386
- const r = _nativeGetCopilotApiUrl(authInfoJson, token);
387
- if (r != null) return r;
388
- try {
389
- const info = JSON.parse(authInfoJson);
390
- if (info && info.type !== 'hmac') {
391
- return 'https://api.githubcopilot.com';
392
- }
393
- } catch (_) {}
394
- return r;
395
- };
396
- }
397
- // capiClientPrepareRequestHeaders: native は OAuth token をそのまま Bearer にする。
398
- // Copilot 推論 API は Copilot token が必要なため、キャッシュ済み copilotToken で差し替え。
399
- if (typeof result.capiClientPrepareRequestHeaders === 'function') {
400
- const _nativePrepareRequestHeaders = result.capiClientPrepareRequestHeaders;
401
- result.capiClientPrepareRequestHeaders = function(handle, ...args) {
402
- const prepared = _nativePrepareRequestHeaders(handle, ...args);
403
- // _authMgr から有効な copilotToken を探して Authorization を差し替え
404
- const now = Date.now();
405
- for (const entry of _authMgr.values()) {
406
- if (entry.copilotToken && (entry.copilotTokenExpiry === 0 || entry.copilotTokenExpiry > now)) {
407
- if (prepared && Array.isArray(prepared.headers)) {
408
- const headers = prepared.headers.map(h =>
409
- h.name && h.name.toLowerCase() === 'authorization'
410
- ? { name: h.name, value: `Bearer ${entry.copilotToken}` }
411
- : h
412
- );
413
- return { ...prepared, headers };
414
- }
415
- }
416
- }
417
- return prepared;
418
- };
419
- }
420
- // modelsFilterToPicker: native は model_picker_enabled=true のモデルのみ返す。
421
- // Free プランは全モデル model_picker_enabled=false → native が [] を返す。
422
- // PICKER-001: fallback に gpt-4o-mini/gpt-4o を返すと TUI /model ピッカーに
423
- // ユーティリティモデルが表示されてしまう(公式仕様違反)。
424
- // auto モードの default 選択は modelResolverFirstAvailableDefaultFromOrder が担うため
425
- // ここでは fallback せず [] を返す。
426
- if (typeof result.modelsFilterToPicker === 'function') {
427
- const _nativeModelsFilterToPicker = result.modelsFilterToPicker;
428
- result.modelsFilterToPicker = function(...args) {
429
- const nativeResult = _nativeModelsFilterToPicker.apply(this, args);
430
- if (Array.isArray(nativeResult) && nativeResult.length > 0) return nativeResult;
431
- return [];
432
- };
433
- }
434
- // capiClientListModels を Node.js fetch で実装(Rust tokio SIGSEGV 回避)
435
- // _selectCapiUrl: COPILOT_API_URL が api.individual.githubcopilot.com(Free プラン専用 direct endpoint)の
436
- // 場合のみそれを使う。Enterprise proxy URL(api.business.githubcopilot.com 等)では /models が 421 に
437
- // なるため標準 CAPI に倒す。hostname 完全一致チェックで SSRF/token 漏洩を防ぐ(GPT-5.5 指摘 #1)。
438
- const _DEFAULT_CAPI_URL = 'https://api.githubcopilot.com';
439
- const _INDIVIDUAL_CAPI_URL = 'https://api.individual.githubcopilot.com';
440
- function _selectCapiUrl(rawApiUrl) {
441
- try {
442
- const u = new URL(rawApiUrl || _DEFAULT_CAPI_URL);
443
- if (u.protocol === 'https:' && u.hostname === 'api.individual.githubcopilot.com') {
444
- return _INDIVIDUAL_CAPI_URL;
445
- }
446
- } catch (e) {
447
- }
448
- return _DEFAULT_CAPI_URL;
449
- }
450
- if (typeof result.capiClientListModels === 'function') {
451
- result.capiClientListModels = async function(handle, _includeHidden, _skipCache, _applyModelLimitCaps, _networkingConfigId) {
452
- const snapshotGen = _modelListCacheGen; // 開始時の世代をキャプチャ → 完了時に照合して stale 結果を破棄
453
- let authHeaders;
454
- try {
455
- const prepared = result.capiClientPrepareRequestHeaders(handle, '', []);
456
- authHeaders = {};
457
- for (const {name, value} of prepared.headers) {
458
- authHeaders[name] = value;
459
- }
460
- } catch (e) {
461
- throw new Error(JSON.stringify({kind: 'network', message: `prepareHeaders failed: ${e.message}`}));
462
- }
463
- // fetchUrl: /models 取得URL。Free は api.individual(COPILOT_API_URL 設定時)、他は標準 CAPI。
464
- // copilotUrl: 推論URL。常に標準 CAPI 固定(v1.0.63 と同じ)。
465
- // BUG-NEW-2 で copilotUrl=fetchUrl にしたため api.individual への推論が発生していた(副作用修正)。
466
- const fetchUrl = _selectCapiUrl(process.env.COPILOT_API_URL);
467
- const copilotUrl = _DEFAULT_CAPI_URL;
468
- let res;
469
- try {
470
- res = await globalThis.fetch(`${fetchUrl}/models`, {method: 'GET', headers: authHeaders});
471
- } catch (e) {
472
- throw new Error(JSON.stringify({kind: 'network', message: e.message}));
473
- }
474
- if (!res.ok) {
475
- const body = await res.text().catch(() => '');
476
- const hdrs = [...res.headers.entries()].map(([name, value]) => ({name, value}));
477
- throw new Error(JSON.stringify({kind: 'http', status: res.status, statusText: res.statusText, body, headers: hdrs, hasRequestId: res.headers.has('x-request-id')}));
478
- }
479
- const data = await res.json();
480
- const raw = Array.isArray(data) ? data : (data.data ?? data.models ?? []);
481
- const models = Array.isArray(raw) ? raw : [];
482
- if (_modelListCacheGen === snapshotGen) {
483
- _modelListCache = models;
484
- } else {
485
- }
486
- const rateHeaders = [...res.headers.entries()].map(([name, value]) => ({name, value}));
487
- return {modelsJson: JSON.stringify(models), copilotUrl, usageRatelimitHeaders: rateHeaders, capturedAssignmentContext: undefined};
488
- };
489
- }
490
- // --- JS networkFetch* implementation (bionic: tokio networkFetch* are no-op'd) ---
491
- // B7 が QXe() から直接呼ばれる MCP 専用パスでクラッシュするため JS で代替。
492
- // networkFetchStreamStart(requestId, req) → Promise<{handle,url,status,statusText,headers}>
493
- // (2026-07-12修正: 実際のCLI本体は requestId を第1引数、req を第2引数として渡し、
494
- // 戻り値を直接thenableとして扱う。旧実装は1引数・{requestId,response}のラップ返却で
495
- // この規約と不一致だったため s.then is not a function / Failed to parse URL from undefined
496
- // エラーの原因になっていた。詳細は docs/KNOWN-BUGS.md AGENT-001参照)
497
- // networkFetchStreamRead → Promise<{ done, body?: Uint8Array }>
498
- // networkFetchStreamClose → void
499
- // networkFetchRequestCancel → void
500
- const _nfMap = new Map();
501
-
502
- result.networkFetchStreamStart = function(requestId, req) {
503
- const abortCtrl = new AbortController();
504
- const entry = { abort: () => abortCtrl.abort(), reader: null };
505
- _nfMap.set(requestId, entry);
506
-
507
- // Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
508
- let body = req.body ?? undefined;
509
- if (body) {
510
- try {
511
- const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
512
- if (parsed && Array.isArray(parsed.tools)) {
513
- const before = parsed.tools.length;
514
- parsed.tools = parsed.tools.filter(
515
- t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
516
- );
517
- if (parsed.tools.length !== before) {
518
- if (parsed.tools.length === 0) delete parsed.tools;
519
- body = JSON.stringify(parsed);
520
- }
521
- }
522
- } catch(_) {}
523
- }
524
-
525
- const headers = {};
526
- if (Array.isArray(req.headers)) {
527
- for (const { name, value } of req.headers) headers[name] = value;
528
- } else if (req.headers && typeof req.headers === 'object') {
529
- Object.assign(headers, req.headers);
530
- }
531
-
532
- const response = (async () => {
533
- let res;
534
- try {
535
- res = await globalThis.fetch(req.url, {
536
- method: req.method || 'GET',
537
- headers,
538
- body: body,
539
- signal: abortCtrl.signal,
540
- redirect: req.redirect || 'follow',
541
- });
542
- } catch (e) {
543
- _nfMap.delete(requestId);
544
- throw e;
545
- }
546
- const handle = requestId;
547
- const reader = res.body ? res.body.getReader() : null;
548
- entry.reader = reader;
549
- const respHeaders = [...res.headers.entries()].map(([name, value]) => ({ name, value }));
550
- return { handle, url: res.url, status: res.status, statusText: res.statusText, headers: respHeaders };
551
- })();
552
-
553
- return response;
554
- };
555
-
556
- result.networkFetchStreamRead = function(handle) {
557
- return (async () => {
558
- const entry = _nfMap.get(handle);
559
- if (!entry || !entry.reader) return { done: true };
560
- try {
561
- const { done, value } = await entry.reader.read();
562
- if (done) { _nfMap.delete(handle); return { done: true }; }
563
- return { done: false, body: value };
564
- } catch (e) {
565
- _nfMap.delete(handle);
566
- throw e;
567
- }
568
- })();
569
- };
570
-
571
- result.networkFetchStreamClose = function(handle) {
572
- const entry = _nfMap.get(handle);
573
- if (entry) {
574
- try { entry.reader?.cancel(); } catch (_) {}
575
- try { entry.abort(); } catch (_) {}
576
- _nfMap.delete(handle);
577
- }
578
- };
579
-
580
- result.networkFetchRequestCancel = function(requestId) {
581
- const entry = _nfMap.get(requestId);
582
- if (entry) {
583
- try { entry.abort(); } catch (_) {}
584
- _nfMap.delete(requestId);
585
- }
586
- };
587
-
588
- result.networkFetchResetClients = function() {
589
- for (const entry of _nfMap.values()) {
590
- try { entry.abort(); } catch (_) {}
591
- }
592
- _nfMap.clear();
593
- };
594
- // --- end JS networkFetch* implementation ---
595
- // --- JS model HTTP implementation (bionic: tokio modelHttp* are no-op'd) ---
596
- // Stream store: streamId → {events, index, finalMessage}
597
- const _jsStreams = new Map();
598
- let _accIdSeq = 1e6;
599
- // Accumulator store: accId → {message}
600
- const _jsAccs = new Map();
601
-
602
- // Helper: parse Anthropic SSE body into events array
603
- function _parseAnthropicSSE(bodyText) {
604
- const events = [];
605
- const lines = bodyText.split(/\r\n|\r|\n/);
606
- let dataLines = [];
607
- for (const line of lines) {
608
- if (line === '' || line === '\r') {
609
- if (dataLines.length > 0) {
610
- const dataStr = dataLines.join('\n');
611
- dataLines = [];
612
- if (dataStr.trim() === '[DONE]') continue;
613
- try { events.push(JSON.parse(dataStr)); } catch(_) {}
614
- }
615
- } else if (line.startsWith('data: ')) {
616
- dataLines.push(line.slice(6));
617
- } else if (line.startsWith('data:')) {
618
- dataLines.push(line.slice(5));
619
- }
620
- }
621
- if (dataLines.length > 0) {
622
- const dataStr = dataLines.join('\n');
623
- if (dataStr.trim() !== '[DONE]') {
624
- try { events.push(JSON.parse(dataStr)); } catch(_) {}
625
- }
626
- }
627
- return events;
628
- }
629
-
630
- // Helper: reconstruct final Anthropic message from SSE events
631
- function _reconstructFinalMessage(events) {
632
- let message = null;
633
- const contentBlocks = new Map(); // index → block
634
- for (const e of events) {
635
- if (e.type === 'message_start') {
636
- message = {
637
- id: e.message && e.message.id,
638
- type: (e.message && e.message.type) || 'message',
639
- role: (e.message && e.message.role) || 'assistant',
640
- model: e.message && e.message.model,
641
- stop_reason: (e.message && e.message.stop_reason) || null,
642
- stop_sequence: (e.message && e.message.stop_sequence) || null,
643
- usage: (e.message && e.message.usage) || { input_tokens: 0, output_tokens: 0 },
644
- content: []
645
- };
646
- } else if (e.type === 'content_block_start') {
647
- const blk = e.content_block;
648
- if (!blk) continue;
649
- if (blk.type === 'text') {
650
- contentBlocks.set(e.index, { type: 'text', text: blk.text || '' });
651
- } else if (blk.type === 'tool_use') {
652
- contentBlocks.set(e.index, { type: 'tool_use', id: blk.id, name: blk.name, _inputBuf: '' });
653
- } else if (blk.type === 'thinking') {
654
- contentBlocks.set(e.index, { type: 'thinking', thinking: blk.thinking || '', signature: blk.signature || '' });
655
- } else if (blk.type === 'redacted_thinking') {
656
- contentBlocks.set(e.index, { type: 'redacted_thinking', data: blk.data });
657
- }
658
- } else if (e.type === 'content_block_delta') {
659
- const blk = contentBlocks.get(e.index);
660
- if (!blk || !e.delta) continue;
661
- if (e.delta.type === 'text_delta') {
662
- blk.text = (blk.text || '') + (e.delta.text || '');
663
- } else if (e.delta.type === 'input_json_delta') {
664
- blk._inputBuf = (blk._inputBuf || '') + (e.delta.partial_json || '');
665
- } else if (e.delta.type === 'thinking_delta') {
666
- blk.thinking = (blk.thinking || '') + (e.delta.thinking || '');
667
- } else if (e.delta.type === 'signature_delta') {
668
- blk.signature = (blk.signature || '') + (e.delta.signature || '');
669
- }
670
- } else if (e.type === 'content_block_stop') {
671
- const blk = contentBlocks.get(e.index);
672
- if (blk && blk.type === 'tool_use') {
673
- try { blk.input = JSON.parse(blk._inputBuf || '{}'); } catch(_) { blk.input = {}; }
674
- delete blk._inputBuf;
675
- }
676
- } else if (e.type === 'message_delta') {
677
- if (message) {
678
- if (e.delta && e.delta.stop_reason != null) message.stop_reason = e.delta.stop_reason;
679
- if (e.delta && e.delta.stop_sequence != null) message.stop_sequence = e.delta.stop_sequence;
680
- if (e.usage && e.usage.output_tokens != null && message.usage) {
681
- message.usage.output_tokens = e.usage.output_tokens;
682
- }
683
- }
684
- }
685
- }
686
- if (message) {
687
- const blocks = Array.from(contentBlocks.entries())
688
- .sort(([a], [b]) => a - b)
689
- .map(([, v]) => v);
690
- message.content = blocks;
691
- }
692
- if (!message) {
693
- message = { role: 'assistant', content: [], stop_reason: 'error', stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } };
694
- }
695
- return message;
696
- }
697
-
698
- // 1. anthropicMessageStreamAccumulatorCreate → JS mock
699
- if (typeof result.anthropicMessageStreamAccumulatorCreate === 'function') {
700
- result.anthropicMessageStreamAccumulatorCreate = function(_handleJson) {
701
- const id = _accIdSeq++;
702
- _jsAccs.set(id, { message: null });
703
- return { json: JSON.stringify({ accumulatorId: id }) };
704
- };
705
- }
706
-
707
- // 2. anthropicMessageStreamAccumulatorFinish → return accumulated message
708
- if (typeof result.anthropicMessageStreamAccumulatorFinish === 'function') {
709
- result.anthropicMessageStreamAccumulatorFinish = function(id) {
710
- const acc = _jsAccs.get(id) || { message: null };
711
- _jsAccs.delete(id);
712
- return { json: JSON.stringify({ message: acc.message }) };
713
- };
714
- }
715
-
716
- // 3. anthropicMessageStreamAccumulatorDrop → cleanup
717
- if (typeof result.anthropicMessageStreamAccumulatorDrop === 'function') {
718
- result.anthropicMessageStreamAccumulatorDrop = function(id) {
719
- _jsAccs.delete(id);
720
- };
721
- }
722
-
723
- // 4. modelHttpStreamStart → fetch full SSE body, parse events
724
- result.modelHttpStreamStart = async function(jsonArg) {
725
- const req = JSON.parse(jsonArg);
726
- let body = req.body;
727
- if (body !== null && body !== undefined && typeof body === 'object') {
728
- if (body.type === 'Buffer' && Array.isArray(body.data)) {
729
- body = Buffer.from(body.data);
730
- } else {
731
- body = JSON.stringify(body);
732
- }
733
- }
734
- // Fix 2 (MODEL-001): /responses 以外は reasoning_effort を除去(/v1/messages 等が 400 になるのを防ぐ)
735
- if (body && req.url && !req.url.includes('/responses')) {
736
- try {
737
- const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
738
- if (parsed && 'reasoning_effort' in parsed) {
739
- delete parsed.reasoning_effort;
740
- body = JSON.stringify(parsed);
741
- }
742
- } catch(_) {}
743
- }
744
- // Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
745
- if (body) {
746
- try {
747
- const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
748
- if (parsed && Array.isArray(parsed.tools)) {
749
- const before = parsed.tools.length;
750
- parsed.tools = parsed.tools.filter(
751
- t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
752
- );
753
- if (parsed.tools.length !== before) {
754
- if (parsed.tools.length === 0) delete parsed.tools;
755
- body = JSON.stringify(parsed);
756
- }
757
- }
758
- } catch(_) {}
759
- }
760
- // Fix (TC-6): Free account model override — Enterprise→Free 切替後に stale モデルが残る問題
761
- body = _applyModelOverride(body, req.url, 'modelHttpStreamStart');
762
- // body 書き換え後に content-length が古くなるのを防ぐ(reasoning_effort/tools/model 除去で長さが変わる)
763
- const fetchHeaders = {};
764
- for (const [k, v] of Object.entries(req.headers || {})) {
765
- if (k.toLowerCase() !== 'content-length') fetchHeaders[k] = v;
766
- }
767
- let res;
768
- try {
769
- res = await globalThis.fetch(req.url, {
770
- method: req.method || 'POST',
771
- headers: fetchHeaders,
772
- body: body,
773
- });
774
- } catch(e) {
775
- throw new Error(JSON.stringify({ kind: 'network', message: e.message }));
776
- }
777
- const headers = {};
778
- for (const [k, v] of res.headers.entries()) headers[k] = v;
779
- const bodyText = await res.text().catch(() => '');
780
- if (res.status < 200 || res.status >= 300) {
781
- return { json: JSON.stringify({ bodyText, status: res.status, statusText: res.statusText, headers, streamId: null }) };
782
- }
783
- const events = _parseAnthropicSSE(bodyText);
784
- const finalMessage = _reconstructFinalMessage(events);
785
- const streamId = 'js-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
786
- _jsStreams.set(streamId, { events, index: 0, finalMessage });
787
- return { json: JSON.stringify({ bodyText: null, status: res.status, statusText: res.statusText, headers, streamId }) };
788
- };
789
- // Helper: convert Anthropic SSE event → chunkContext for processAnthropicStreamingChunkContext
790
- function _toChunkContext(event, streamId) {
791
- const base = { content: '', size: 0, chunkBoundary: false, messageStart: false, streamingId: streamId };
792
- if (!event || !event.type) return base;
793
- switch (event.type) {
794
- case 'message_start':
795
- return { ...base, messageStart: true };
796
- case 'content_block_delta': {
797
- const d = event.delta;
798
- if (!d) return base;
799
- if (d.type === 'text_delta') {
800
- const t = d.text || '';
801
- return { ...base, content: t, size: Buffer.byteLength(t, 'utf8') };
802
- }
803
- if (d.type === 'thinking_delta') {
804
- const r = d.thinking || '';
805
- return { ...base, reasoningContent: r, size: Buffer.byteLength(r, 'utf8') };
806
- }
807
- return base;
808
- }
809
- case 'content_block_stop':
810
- case 'message_delta':
811
- case 'message_stop':
812
- return { ...base, chunkBoundary: true };
813
- default:
814
- return base;
815
- }
816
- }
817
- // 5. modelHttpStreamNextAnthropicMessageEvent → yield events one by one
818
- result.modelHttpStreamNextAnthropicMessageEvent = async function(streamId, accId) {
819
- const st = _jsStreams.get(streamId);
820
- if (!st || st.index >= st.events.length) {
821
- if (st) {
822
- const acc = _jsAccs.get(accId);
823
- if (acc) acc.message = st.finalMessage;
824
- _jsStreams.delete(streamId);
825
- }
826
- return null;
827
- }
828
- const event = st.events[st.index++];
829
- const chunkContext = _toChunkContext(event, streamId);
830
- const tokenEvent = event.type === 'content_block_delta' && !!(event.delta && event.delta.type === 'text_delta');
831
- return { json: JSON.stringify({ kind: 'ok', processResult: { event, chunkContext, tokenEvent, copilotUsage: null } }) };
832
- };
833
-
834
- // 6. modelHttpStreamCancel → cleanup
835
- result.modelHttpStreamCancel = function(streamId) {
836
- _jsStreams.delete(streamId);
837
- };
838
-
839
- // 7. modelHttpRequest → non-streaming fetch
840
- result.modelHttpRequest = async function(jsonArg) {
841
- const req = JSON.parse(jsonArg);
842
- let body = req.body;
843
- if (body !== null && body !== undefined && typeof body === 'object') {
844
- if (body.type === 'Buffer' && Array.isArray(body.data)) {
845
- body = Buffer.from(body.data);
846
- } else {
847
- body = JSON.stringify(body);
848
- }
849
- }
850
- // Fix 2 (MODEL-001): /responses 以外は reasoning_effort を除去
851
- if (body && req.url && !req.url.includes('/responses')) {
852
- try {
853
- const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
854
- if (parsed && 'reasoning_effort' in parsed) {
855
- delete parsed.reasoning_effort;
856
- body = JSON.stringify(parsed);
857
- }
858
- } catch(_) {}
859
- }
860
- // Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
861
- if (body) {
862
- try {
863
- const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
864
- if (parsed && Array.isArray(parsed.tools)) {
865
- const before = parsed.tools.length;
866
- parsed.tools = parsed.tools.filter(
867
- t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
868
- );
869
- if (parsed.tools.length !== before) {
870
- if (parsed.tools.length === 0) delete parsed.tools;
871
- body = JSON.stringify(parsed);
872
- }
873
- }
874
- } catch(_) {}
875
- }
876
- // Fix (TC-6): Free account model override — Enterprise→Free 切替後に stale モデルが残る問題
877
- body = _applyModelOverride(body, req.url, 'modelHttpRequest');
878
- // body 書き換え後に content-length が古くなるのを防ぐ
879
- const fetchHeaders = {};
880
- for (const [k, v] of Object.entries(req.headers || {})) {
881
- if (k.toLowerCase() !== 'content-length') fetchHeaders[k] = v;
882
- }
883
- let res;
884
- try {
885
- res = await globalThis.fetch(req.url, {
886
- method: req.method || 'POST',
887
- headers: fetchHeaders,
888
- body: body,
889
- });
890
- } catch(e) {
891
- throw new Error(JSON.stringify({ kind: 'network', message: e.message }));
892
- }
893
- const headers = {};
894
- for (const [k, v] of res.headers.entries()) headers[k] = v;
895
- const bodyText = await res.text().catch(() => '');
896
- return { json: JSON.stringify({ bodyText, status: res.status, statusText: res.statusText, headers }) };
897
- };
898
- // 8. responsesStreamDrive → JS streamをネイティブリデューサーに流す
899
- // native responsesStreamDrive は Rust tokio でストリームを読む。
900
- // JS streamId("js-"で始まる)はRust側にないためクラッシュ → JS実装で代替。
901
- const _nativeReducerProcessEvent = typeof result.openaiResponsesStreamReducerProcessEvent === 'function'
902
- ? result.openaiResponsesStreamReducerProcessEvent
903
- : null;
904
- result.responsesStreamDrive = async function(streamId, reducerId, hasProcessors, onChunkCallback) {
905
- const st = _jsStreams.get(streamId);
906
- if (!st) {
907
- throw new Error(`Native mod HTTP stream was not found: ${streamId}`);
908
- }
909
- _jsStreams.delete(streamId);
910
- let copilotUsage = null;
911
- for (const event of st.events) {
912
- if (!_nativeReducerProcessEvent) continue;
913
- let parsed;
914
- try { parsed = JSON.parse(_nativeReducerProcessEvent(reducerId, JSON.stringify(event)).json); }
915
- catch (_) { continue; }
916
- if (parsed && parsed.copilotUsage !== undefined && parsed.copilotUsage !== null)
917
- copilotUsage = parsed.copilotUsage;
918
- const cc = parsed && parsed.chunkContext;
919
- if (hasProcessors && typeof onChunkCallback === 'function' && cc &&
920
- (cc.content || cc.messageStart || cc.reportIntentArguments || cc.chunkBoundary || cc.size > 0)) {
921
- try { onChunkCallback(JSON.stringify(cc)); } catch (_) {}
922
- }
923
- }
924
- return { json: JSON.stringify({ kind: 'ok', copilotUsage, ttftMs: null, interTokenLatencyMs: null }) };
925
- };
926
- // Helper: Free アカウント切替後に Enterprise モデルが残る問題を防ぐ model override。
927
- // /chat/completions 系 URL にのみ適用し、_modelListCache の enabled モデルと照合。
928
- // 対象外の場合は body をそのまま返す(破壊なし)。
929
- function _applyModelOverride(body, url, prefix) {
930
- try {
931
- if (!body) return body;
932
- const pathname = new URL(url).pathname;
933
- // /chat/completions のみを対象にする
934
- if (!pathname.endsWith('/chat/completions')) return body;
935
- const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
936
- if (!parsed || !parsed.model) return body;
937
- if (_modelListCache && _modelListCache.length > 0) {
938
- const enabledIds = new Set(
939
- _modelListCache.filter(m => m?.policy?.state === 'enabled').map(m => m.id).filter(Boolean)
940
- );
941
- if (!enabledIds.has(parsed.model)) {
942
- // goldeneye-free-auto のみを対象にする
943
- const OVERRIDE_CANDIDATES = ['goldeneye-free-auto'];
944
- let fallbackModel = null;
945
- for (const id of OVERRIDE_CANDIDATES) {
946
- const m = _modelListCache.find(m => m?.id === id && m?.policy?.state === 'enabled');
947
- if (m) { fallbackModel = id; break; }
948
- }
949
- if (fallbackModel) {
950
- parsed.model = fallbackModel;
951
- return JSON.stringify(parsed);
952
- }
953
- }
954
- }
955
- } catch(_) {}
956
- return body;
957
- }
958
-
959
- // 9. chatCompletionStreamDrive → OpenAI-compatible chat completion stream処理
960
- // native は JS-side streamId("js-")を知らないため JS で代替。
961
- result.chatCompletionStreamDrive = async function(streamId, reducerId, hasProcessors, onChunkCallback) {
962
- const st = _jsStreams.get(streamId);
963
- if (!st) {
964
- throw new Error(`Native model HTTP stream was not found: ${streamId}`);
965
- }
966
- _jsStreams.delete(streamId);
967
- let content = '', finishReason = null, id = null, model = null, role = 'assistant', usage = null, created = null;
968
- const toolCallsByIndex = new Map(); // index → {id, type, function: {name, arguments}}
969
- let functionCall = null; // 旧 delta.function_call 形式
970
- let isFirstChunk = true;
971
- // Fix (GPT-5.5 No-Go #1): 終端 finish_reason が来たか追跡する
972
- // finishReason || 'stop' のデフォルトは壊れたストリームを正常完了に見せる危険がある
973
- let seenTerminalFinishReason = false;
974
- const TERMINAL_FINISH_REASONS = new Set(['stop', 'tool_calls', 'function_call', 'length', 'content_filter']);
975
- for (const event of st.events) {
976
- if (!event) continue;
977
- if (event.id) id = event.id;
978
- if (event.model) model = event.model;
979
- if (event.usage) usage = event.usage;
980
- if (event.created) created = event.created;
981
- if (!Array.isArray(event.choices)) continue;
982
- for (const choice of event.choices) {
983
- if (choice.delta) {
984
- if (choice.delta.role) role = choice.delta.role;
985
- if (typeof choice.delta.content === 'string') content += choice.delta.content;
986
- // tool_calls 断片を index ごとに集約
987
- if (Array.isArray(choice.delta.tool_calls)) {
988
- for (const tc of choice.delta.tool_calls) {
989
- const idx = tc.index ?? 0;
990
- if (!toolCallsByIndex.has(idx)) {
991
- toolCallsByIndex.set(idx, { id: '', type: 'function', function: { name: '', arguments: '' } });
992
- }
993
- const entry = toolCallsByIndex.get(idx);
994
- if (tc.id) entry.id = tc.id;
995
- if (tc.type) entry.type = tc.type;
996
- if (tc.function) {
997
- if (tc.function.name) entry.function.name += tc.function.name;
998
- if (typeof tc.function.arguments === 'string') entry.function.arguments += tc.function.arguments;
999
- }
1000
- }
1001
- }
1002
- // 旧 function_call 形式
1003
- if (choice.delta.function_call) {
1004
- if (!functionCall) functionCall = { name: '', arguments: '' };
1005
- if (choice.delta.function_call.name) functionCall.name += choice.delta.function_call.name;
1006
- if (typeof choice.delta.function_call.arguments === 'string') functionCall.arguments += choice.delta.function_call.arguments;
1007
- }
1008
- }
1009
- if (choice.finish_reason) {
1010
- finishReason = choice.finish_reason;
1011
- if (TERMINAL_FINISH_REASONS.has(choice.finish_reason)) seenTerminalFinishReason = true;
1012
- }
1013
- }
1014
- if (hasProcessors && typeof onChunkCallback === 'function') {
1015
- let chunkContent = '', isBoundary = false, hasToolDelta = false;
1016
- for (const choice of event.choices) {
1017
- if (choice.delta && typeof choice.delta.content === 'string') chunkContent += choice.delta.content;
1018
- // Fix (GPT-5.5 No-Go #2): tool/function call delta も callback に通知する
1019
- if (choice.delta && (choice.delta.tool_calls || choice.delta.function_call)) hasToolDelta = true;
1020
- if (choice.finish_reason) isBoundary = true;
1021
- }
1022
- if (chunkContent || isBoundary || isFirstChunk || hasToolDelta) {
1023
- try { onChunkCallback(JSON.stringify({ content: chunkContent, size: Buffer.byteLength(chunkContent, 'utf8'), chunkBoundary: isBoundary, messageStart: isFirstChunk, streamingId: streamId })); } catch (_) {}
1024
- }
1025
- isFirstChunk = false;
1026
- }
1027
- }
1028
- const toolCalls = toolCallsByIndex.size > 0
1029
- ? Array.from(toolCallsByIndex.entries()).sort(([a], [b]) => a - b).map(([, v]) => v)
1030
- : undefined;
1031
- // tool call 系では content は null が正しい(テキストと排他)
1032
- const msgContent = (toolCalls || functionCall) ? (content || null) : content;
1033
- const message = { role, content: msgContent };
1034
- if (toolCalls) message.tool_calls = toolCalls;
1035
- if (functionCall) message.function_call = functionCall;
1036
- // Fix (GPT-5.5 No-Go #1): 終端が来ていない場合は 'stop' を補完しない
1037
- const effectiveFinishReason = seenTerminalFinishReason ? finishReason : (finishReason ?? null);
1038
- const completion = {
1039
- id: id || ('chatcmpl-' + Date.now().toString(36)),
1040
- object: 'chat.completion',
1041
- created: created || Math.floor(Date.now() / 1000),
1042
- model: model || 'gpt-4o',
1043
- choices: [{ index: 0, message, finish_reason: effectiveFinishReason, logprobs: null }],
1044
- usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1045
- };
1046
- return { json: JSON.stringify({ kind: 'ok', completion, copilotUsage: null, ttftMs: null, interTokenLatencyMs: null }) };
1047
- };
1048
- // glibc modeでも維持(isGlibcMode分岐なし): bionic回避に加えCopilot token交換・アカウント切替キャッシュ管理を兼ねるため
1049
- // === authManager* JS stubs (1.0.64: tokio thread spawn → SIGSEGV on bionic) ===
1050
- const _authMgr = new Map(); // uuid → { cachedInfo, pendingInfo, cachedToken, cachedHost, gen }
1051
- let _modelListCache = null; // capiClientListModels が取得したモデルリストキャッシュ(modelResolver fallback 用)
1052
- let _modelListCacheGen = 0; // アカウント切替時にインクリメント → 古い /models 結果の上書きを防ぐ
1053
-
1054
- result.authManagerCreate = function(uuid, hostUri, userAgent, path, normSpec, header, envVar, disableAutoLogin) {
1055
- _authMgr.set(uuid, { cachedInfo: null, pendingInfo: null, cachedToken: null, cachedHost: null, gen: 0, copilotToken: null, copilotTokenExpiry: 0 });
1056
- // native 非呼び出し: tokio runtime 生成を阻止
1057
- };
1058
-
1059
- async function _buildAuthInfo(token, hostUri) {
1060
- let login = null;
1061
- let copilotUser = null;
1062
- try {
1063
- const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
1064
- const res = await globalThis.fetch(`${apiHost}/user`, {
1065
- headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}` },
1066
- signal: AbortSignal.timeout(5000),
1067
- });
1068
- if (res.ok) login = (await res.json()).login;
1069
- } catch (e) {
1070
- }
1071
- // copilot_internal/user から copilotUser 全体を取得して authInfo に含める。
1072
- // app.js の Wa(authInfo) は authInfo.copilotUser.endpoints.api から CAPI base URL を導く。
1073
- // copilotUser: null のままでは is_mcp_enabled・quota・plan 情報も失われる。
1074
- // env var は後続の capiClientListModels stub / authGetCopilotApiUrl stub でも参照するため並記する。
1075
- try {
1076
- const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
1077
- const r = await globalThis.fetch(`${apiHost}/copilot_internal/user`, {
1078
- headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': process.env.GITHUB_COPILOT_INTEGRATION_ID || 'copilot-developer-cli' },
1079
- signal: AbortSignal.timeout(5000),
1080
- });
1081
- if (r.ok) {
1082
- const info = await r.json();
1083
- copilotUser = info;
1084
- const apiUrl = info?.endpoints?.api;
1085
- if (apiUrl && typeof apiUrl === 'string') process.env.COPILOT_API_URL = apiUrl;
1086
- } else {
1087
- }
1088
- } catch (e) {
1089
- }
1090
- // Copilot API token 取得(推論 API は OAuth token を受け付けないため交換が必要)
1091
- let copilotToken = null;
1092
- let copilotTokenExpiry = 0;
1093
- try {
1094
- const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
1095
- const t = await globalThis.fetch(`${apiHost}/copilot_internal/v2/token`, {
1096
- method: 'GET',
1097
- headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': process.env.GITHUB_COPILOT_INTEGRATION_ID || 'copilot-developer-cli' },
1098
- signal: AbortSignal.timeout(5000),
1099
- });
1100
- if (t.ok) {
1101
- const td = await t.json();
1102
- copilotToken = td.token || null;
1103
- copilotTokenExpiry = copilotToken ? _normalizeCopilotTokenExpiry(td.expires_at) : 0;
1104
- } else {
1105
- let body = null;
1106
- try { const raw = await t.text(); body = raw.length > 500 ? raw.slice(0, 500) + '…' : raw; } catch (_) {}
1107
- }
1108
- } catch (e) {
1109
- }
1110
- return JSON.stringify({
1111
- authInfo: { type: 'token', host: hostUri, token, login, copilotUser },
1112
- token,
1113
- copilotToken,
1114
- copilotTokenExpiry,
1115
- });
1116
- }
1117
-
1118
- async function _resolveOrCache(uuid, token, env) {
1119
- const entry = _authMgr.get(uuid);
1120
- if (!entry) return null;
1121
- const hostUri = ((typeof result.githubGetUri === 'function'
1122
- ? result.githubGetUri(
1123
- (env && env.COPILOT_GH_HOST) || undefined,
1124
- (env && env.GH_HOST) || undefined
1125
- )
1126
- : null) || 'https://github.com').replace(/\/+$/, '');
1127
- if (entry.cachedInfo !== null && (entry.cachedToken !== token || entry.cachedHost !== hostUri)) {
1128
- entry.cachedInfo = null;
1129
- entry.pendingInfo = null;
1130
- entry.copilotToken = null;
1131
- entry.copilotTokenExpiry = 0;
1132
- _modelListCache = null;
1133
- _modelListCacheGen++;
1134
- delete process.env.COPILOT_API_URL;
1135
- }
1136
- if (entry.cachedInfo !== null) {
1137
- return entry.cachedInfo;
1138
- }
1139
- if (!entry.pendingInfo) {
1140
- const gen = entry.gen;
1141
- entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
1142
- if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
1143
- entry.cachedInfo = info;
1144
- entry.cachedToken = token;
1145
- entry.cachedHost = hostUri;
1146
- entry.pendingInfo = null;
1147
- try {
1148
- const parsed = JSON.parse(info);
1149
- if (parsed && parsed.authInfo &&
1150
- typeof parsed.authInfo.login === 'string' && parsed.authInfo.login.length > 0) {
1151
- _loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
1152
- }
1153
- // authManagerSwitchToAuth / authManagerLoginUser と同様に copilotToken を設定
1154
- // _resolveOrCache 経由(TUI 初回起動)でも copilot token が使われるようにする
1155
- entry.copilotToken = (parsed && parsed.copilotToken) || null;
1156
- entry.copilotTokenExpiry = (parsed && parsed.copilotTokenExpiry) || 0;
1157
- } catch (_) {}
1158
- return info;
1159
- }).catch(err => {
1160
- if (entry.gen === gen) entry.pendingInfo = null;
1161
- throw err;
1162
- });
1163
- }
1164
- return entry.pendingInfo;
1165
- }
1166
-
1167
- result.authManagerLoadAuthInfo = async function(uuid, env, _storeTokenPlaintext) {
1168
- const token = await _readGhToken(env);
1169
- if (!token) return null;
1170
- return _resolveOrCache(uuid, token, env);
1171
- };
1172
- result.authManagerGetCurrentAuthInfo = async function(uuid, env, _storeTokenPlaintext) {
1173
- const entry = _authMgr.get(uuid);
1174
- if (!entry) return null;
1175
- if (entry.cachedInfo !== null) return entry.cachedInfo;
1176
- if (entry.pendingInfo) return entry.pendingInfo;
1177
- const token = await _readGhToken(env);
1178
- if (!token) return null;
1179
- return _resolveOrCache(uuid, token, env);
1180
- };
1181
- result.authManagerGetAllAuthAvailable = async function(uuid, env, _storeTokenPlaintext) {
1182
- const token = await _readGhToken(env);
1183
- if (!token) return [];
1184
- const info = await _resolveOrCache(uuid, token, env);
1185
- return info ? [info] : [];
1186
- };
1187
- result.authManagerGetLastAuthErrors = function(uuid) { return []; };
1188
- result.authManagerClearCache = function(uuid) {
1189
- const e = _authMgr.get(uuid);
1190
- if (e) { e.gen++; e.cachedInfo = null; e.pendingInfo = null; e.cachedToken = null; e.cachedHost = null; e.copilotToken = null; e.copilotTokenExpiry = 0; _modelListCache = null; _modelListCacheGen++; delete process.env.COPILOT_API_URL; }
1191
- };
1192
- result.authManagerSwitchToAuth = async function(uuid, authInfoJson, token) {
1193
- const entry = _authMgr.get(uuid);
1194
- if (!entry) return;
1195
- // Clear stale enterprise endpoint and auth cache regardless of token presence
1196
- delete process.env.COPILOT_API_URL;
1197
- _modelListCache = null;
1198
- _modelListCacheGen++;
1199
- const gen = ++entry.gen;
1200
- entry.cachedInfo = null;
1201
- entry.cachedToken = null;
1202
- entry.cachedHost = null;
1203
- entry.pendingInfo = null;
1204
- entry.copilotToken = null;
1205
- entry.copilotTokenExpiry = 0;
1206
- if (!token) return;
1207
- const hostUri = (() => {
1208
- try { return (JSON.parse(authInfoJson)?.host || 'https://github.com').replace(/\/+$/, ''); }
1209
- catch (_) { return 'https://github.com'; }
1210
- })();
1211
- entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
1212
- if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
1213
- entry.cachedInfo = info;
1214
- entry.cachedToken = token;
1215
- entry.cachedHost = hostUri;
1216
- entry.pendingInfo = null;
1217
- try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
1218
- return info;
1219
- }).catch(err => {
1220
- if (entry.gen === gen) entry.pendingInfo = null;
1221
- });
1222
- await entry.pendingInfo;
1223
- };
1224
- result.authManagerLoginUser = async function(uuid, host, login, token) {
1225
- const entry = _authMgr.get(uuid);
1226
- if (!entry || !token) return;
1227
- const hostUri = (host || 'https://github.com').replace(/\/+$/, '');
1228
- _loginTokens.set(`${hostUri}:${login || ''}`, token);
1229
- delete process.env.COPILOT_API_URL;
1230
- _modelListCache = null;
1231
- _modelListCacheGen++;
1232
- const gen = ++entry.gen;
1233
- entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
1234
- if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
1235
- entry.cachedInfo = info;
1236
- entry.cachedToken = token;
1237
- entry.cachedHost = hostUri;
1238
- entry.pendingInfo = null;
1239
- try {
1240
- const parsed = JSON.parse(info);
1241
- if (parsed && parsed.authInfo &&
1242
- typeof parsed.authInfo.login === 'string' && parsed.authInfo.login.length > 0) {
1243
- _loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
1244
- }
1245
- } catch (_) {}
1246
- try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
1247
- return info;
1248
- }).catch(err => { if (entry.gen === gen) entry.pendingInfo = null; return null; });
1249
- await entry.pendingInfo; // ensure cachedInfo is set before app.js continues
1250
- };
1251
- result.authManagerLogout = async function(uuid, authInfoJson) {
1252
- const e = _authMgr.get(uuid);
1253
- if (e) { e.cachedInfo = null; e.pendingInfo = null; e.cachedToken = null; e.cachedHost = null; e.copilotToken = null; e.copilotTokenExpiry = 0; _modelListCache = null; _modelListCacheGen++; delete process.env.COPILOT_API_URL; }
1254
- return true;
1255
- };
1256
- result.authManagerRefreshCopilotUser = async function(uuid) {
1257
- return null; // null → JS側 authInfoWithTokenPromise フォールバック
1258
- };
1259
- result.authManagerDestroy = function(uuid) { _authMgr.delete(uuid); };
1260
-
1261
- if (!isGlibcMode && typeof result.authResolveAuthInfoFromToken === 'function') {
1262
- result.authResolveAuthInfoFromToken = async function(token, hostUri, skipCache, userAgent) {
1263
- if (!token) return JSON.stringify(null);
1264
- let login = null;
1265
- try {
1266
- const apiHost = (hostUri || 'https://github.com').replace('://github.com', '://api.github.com');
1267
- const res = await globalThis.fetch(`${apiHost}/user`, {
1268
- headers: { Authorization: `token ${token}`, 'User-Agent': userAgent || `copilot-termux/${_pkgVersion}` },
1269
- signal: AbortSignal.timeout(5000),
1270
- });
1271
- if (res.ok) login = (await res.json()).login;
1272
- } catch (_) {}
1273
- const host = hostUri || 'https://github.com';
1274
- return JSON.stringify({ type: 'token', host, token, login, copilotUser: null });
1275
- };
1276
- }
1277
- // glibc mode では native authResolveAuthInfoFromToken を使う(SSL_CERT_FILE で TLS 修正済み)
1278
- // === end authManager* stubs ===
1279
- // tokenStore*(Create/Destroy/GetToken/StoreToken/RemoveToken/GetAnyToken/
1280
- // StoreCurrentTokenInConfig)はJS再実装せずnativeにそのまま委譲する
1281
- // (2026-07-26、G1 3案目Conditional Go)。upstream 1.0.75でauthManager*が
1282
- // OOPのAuthManagerHandleクラスへ完全移行し、tokenStoreGetTokenが唯一のトークン
1283
- // 供給元になったが、native実装はbionic/glibc両モード・1.0.73/1.0.75両バージョンで
1284
- // config.jsonの読み書き(read-modify-write、既存エントリ保持)が正しく機能し、
1285
- // クラッシュしないことを実機検証済み(詳細はdocs/KNOWN-BUGS.md参照)。旧JS再実装は
1286
- // _tokStore/_loginTokensという常に空のメモリキャッシュしか見ておらず、
1287
- // config.jsonの実トークンに一切アクセスできていなかった(認証失敗の真因)。
1288
- //
1289
- // _loginTokensは直前のauthManager*ブロック(_resolveOrCache/authManagerLoginUser/
1290
- // authManagerSwitchToAuth)が読み書きするため、1.0.73(authManager*が生きたコード)
1291
- // 互換のためここに残す。tokenStore*からは一切参照されない。
1292
- const _loginTokens = new Map(); // "host:login" → oauthToken (authManager*ブロック専用)
1293
-
1294
- if (!isGlibcMode) {
1295
- // === urlManager* JS stubs ===
1296
- const _urlMgr = new Map();
1297
- let _urlSeq = 8e6;
1298
- result.urlManagerCreate = function(urls, unrestricted) {
1299
- const id = ++_urlSeq;
1300
- _urlMgr.set(id, { urls: Array.isArray(urls) ? [...urls] : [], unrestricted: !!unrestricted });
1301
- return id;
1302
- };
1303
- result.urlManagerAddUrl = function(h, url) { const e = _urlMgr.get(h); if (e) e.urls.push(url); };
1304
- result.urlManagerDispose = function(h) { _urlMgr.delete(h); };
1305
- result.urlManagerGetUrls = function(h) { return (_urlMgr.get(h) || {}).urls || []; };
1306
- result.urlManagerIsUrlAllowed = function(h, url) { return true; };
1307
- result.urlManagerIsUnrestrictedMode = function(h) { return true; };
1308
- result.urlManagerSetUnrestrictedMode = function(h, v) { const e = _urlMgr.get(h); if (e) e.unrestricted = v; };
1309
- // === end urlManager* stubs ===
1310
-
1311
- // === pathManager* JS stubs ===
1312
- const _pathMgr = new Map();
1313
- let _pathSeq = 7e6;
1314
- result.pathManagerCreateRestricted = async function(dirs, primary) {
1315
- const id = ++_pathSeq;
1316
- _pathMgr.set(id, { dirs: Array.isArray(dirs) ? [...dirs] : [], primary: primary || null });
1317
- return id;
1318
- };
1319
- result.pathManagerCreateUnrestricted = async function(primary) {
1320
- const id = ++_pathSeq;
1321
- _pathMgr.set(id, { dirs: [], primary: primary || null });
1322
- return id;
1323
- };
1324
- result.pathManagerDispose = function(h) { _pathMgr.delete(h); };
1325
- result.pathManagerAddDirectory = function(h, dir) { const e = _pathMgr.get(h); if (e) e.dirs.push(dir); };
1326
- result.pathManagerGetDirectories = function(h) { return (_pathMgr.get(h) || {}).dirs || []; };
1327
- result.pathManagerGetPrimaryDirectory = function(h) { return (_pathMgr.get(h) || {}).primary || null; };
1328
- result.pathManagerIsPathWithinWorkspace = function(h, p) {
1329
- const e = _pathMgr.get(h);
1330
- if (!e || !e.primary) return true;
1331
- return p === e.primary || p.startsWith(e.primary + '/');
1332
- };
1333
- result.pathManagerUpdatePrimaryDirectory = function(h, dir) { const e = _pathMgr.get(h); if (e) e.primary = dir; };
1334
- result.pathManagerIsPathWithinAllowedDirectories = function(h, p) {
1335
- const e = _pathMgr.get(h);
1336
- if (!e || e.dirs.length === 0) return true;
1337
- return e.dirs.some(dir => p === dir || p.startsWith(dir + '/'));
1338
- };
1339
- // === end pathManager* stubs ===
1340
-
1341
- // === telemetryQueue* JS stubs ===
1342
- let _telSeq = 6e6;
1343
- result.telemetryQueueCreate = function(name, _config) { return ++_telSeq; };
1344
- result.telemetryQueueDispose = function(h) {};
1345
- result.telemetryQueueEnqueue = function(h, event) {};
1346
- result.telemetryQueueSetDebugLogPayload = function(h, v) {};
1347
- // === end telemetryQueue* stubs ===
1348
-
1349
- // === telemetryAppInsightsServiceState* JS stubs (1.0.65+: Azure AppInsights HTTP I/O → tokio SIGSEGV on bionic) ===
1350
- if (typeof result.telemetryAppInsightsServiceStateCreate === 'function') {
1351
- result.telemetryAppInsightsServiceStateCreate = function() { return ++_telSeq; };
1352
- result.telemetryAppInsightsServiceStateDispose = function(h) {};
1353
- result.telemetryAppInsightsServiceStateEnqueue = function(h, e, n, r) { return JSON.stringify({}); };
1354
- result.telemetryAppInsightsServiceStateAuthSucceeded = function(h, e) { return JSON.stringify({}); };
1355
- result.telemetryAppInsightsServiceStateLogout = function(h) { return JSON.stringify({}); };
1356
- }
1357
- // === end telemetryAppInsightsServiceState* stubs ===
1358
-
1359
- // === telemetryDelegatingSender* JS stubs (1.0.65+: fire-and-forget HTTP send → tokio SIGSEGV on bionic) ===
1360
- if (typeof result.telemetryDelegatingSenderCreate === 'function') {
1361
- result.telemetryDelegatingSenderCreate = function() { return ++_telSeq; };
1362
- result.telemetryDelegatingSenderDispose = function(h) { return JSON.stringify({ disposeDelegate: false }); };
1363
- result.telemetryDelegatingSenderConfigure = function(h, hasDelegate) {
1364
- return JSON.stringify({ disposePreviousDelegate: false, disposeNewDelegate: false });
1365
- };
1366
- result.telemetryDelegatingSenderIsConfigured = function(h) { return false; };
1367
- result.telemetryDelegatingSenderRequiresDelegate = function(h) {};
1368
- result.telemetryDelegatingSenderSetInternalCorrelationIds = function(h, ids) {
1369
- return JSON.stringify({ applyToDelegate: false });
1370
- };
1371
- }
1372
- // === end telemetryDelegatingSender* stubs ===
1373
-
1374
- // === telemetrySessionTelemetryState* JS stubs (1.0.65+) ===
1375
- if (typeof result.telemetrySessionTelemetryStateCreate === 'function') {
1376
- result.telemetrySessionTelemetryStateCreate = function(h, e) { return ++_telSeq; };
1377
- result.telemetrySessionTelemetryStateDispose = function(h) {};
1378
- result.telemetrySessionTelemetryStateSnapshot = function(h) { return JSON.stringify({ telemetryEvents: [] }); };
1379
- result.telemetrySessionTelemetryStateProcessSessionEvent = function(h, e, n) {
1380
- return JSON.stringify({ telemetryEvents: [] });
1381
- };
1382
- result.telemetrySessionTelemetryStateProcessToolsUpdated = function(h, e) {};
1383
- }
1384
- // === end telemetrySessionTelemetryState* stubs ===
1385
-
1386
- // === telemetryLegacyUsageHandler* JS stubs (1.0.65+) ===
1387
- if (typeof result.telemetryLegacyUsageHandlerCreate === 'function') {
1388
- result.telemetryLegacyUsageHandlerCreate = function(h, e) { return ++_telSeq; };
1389
- result.telemetryLegacyUsageHandlerDispose = function(h) {};
1390
- result.telemetryLegacyUsageHandlerProcessEvent = function(h, e) { return JSON.stringify([]); };
1391
- }
1392
- // === end telemetryLegacyUsageHandler* stubs ===
1393
-
1394
- // === permissionService* JS stubs ===
1395
- const _permSvc = new Map();
1396
- let _permSeq = 5e6;
1397
- result.permissionServiceCreate = function(config) {
1398
- const id = ++_permSeq;
1399
- _permSvc.set(id, { approveAll: !!(config && config.approveAllTool) });
1400
- return id;
1401
- };
1402
- result.permissionServiceDispose = function(h) { _permSvc.delete(h); };
1403
- result.permissionServiceRequest = async function(h, reqJson) {
1404
- return JSON.stringify({ kind: 'approved' });
1405
- };
1406
- result.permissionServiceComplete = function(h, token, ok, resultJson) {};
1407
- result.permissionServiceConfigure = function(h, approveAllTool, approveAllRead, approvedRules, deniedRules, pathMgr, urlMgr) {};
1408
- result.permissionServiceAddApprovedRules = function(h, rules) {};
1409
- result.permissionServiceGetApproveAllTool = function(h) { return (_permSvc.get(h) || {}).approveAll ?? false; };
1410
- result.permissionServiceSetApproveAllTool = function(h, v) { const e = _permSvc.get(h); if (e) e.approveAll = v; };
1411
- result.permissionServiceRemoveApprovedRules = function(h, rules) {};
1412
- result.permissionServiceCheckSamplingApproval = function(h, k) { return true; };
1413
- result.permissionServiceResetSessionApprovals = function(h) {};
1414
- result.permissionServiceAddLocationApprovedRules = function(h, rules) {};
1415
- result.permissionServiceRemoveLocationApprovedRules = function(h, rules) {};
1416
- // === end permissionService* stubs ===
1417
-
1418
- // === lspManager* JS stubs ===
1419
- let _lspMgrSeq = 4e6;
1420
- result.lspManagerCreate = function() { return ++_lspMgrSeq; };
1421
- result.lspManagerClear = function(h) {};
1422
- result.lspManagerPlanForFile = function(h, file, lang, config) { return null; };
1423
- result.lspManagerRemoveClient = function(h, key) {};
1424
- result.lspManagerShutdownKeys = function(h) { return []; };
1425
- result.lspManagerPlanForServerId = function(h, id) { return null; };
1426
- result.lspManagerCachedClientCount = function(h) { return 0; };
1427
- result.lspManagerRelevantServerIds = function(h, file, lang) { return []; };
1428
- // === end lspManager* stubs ===
1429
-
1430
- // === ifcEngine* JS stubs ===
1431
- const _ifc = new Map();
1432
- let _ifcSeq = 3e6;
1433
- result.ifcEngineCreate = function(config) { const id = ++_ifcSeq; _ifc.set(id, {}); return id; };
1434
- result.ifcEngineDispose = function(h) { _ifc.delete(h); };
1435
- result.ifcEngineToJson = function(h) { return '{}'; };
1436
- result.ifcEnginePreToolHook = async function(h, tool, argsJson, fetchHandler) { return { converged: true }; };
1437
- result.ifcEngineFetchComplete = function(h, resp) {};
1438
- result.ifcEngineGetContextLabel = function(h) { return null; };
1439
- result.ifcEngineSetContextLabel = function(h, label) {};
1440
- result.ifcEnginePostToolExecution = async function(h, tool, argsJson, fetchHandler) {
1441
- return { converged: true, applied: false, updated: false };
1442
- };
1443
- // === end ifcEngine* stubs ===
1444
- }
1445
- // --- end JS model HTTP implementation ---
1446
- // runtime.node に対する全パッチ処理(bionic分岐のpreflight・スタブ化・glibc共通処理)が
1447
- // 成功した場合にのみマーカーを設定する(原子性: 途中でthrowした場合はここに到達しないため
1448
- // マーカーは残らず、次回requireで再度preflightからやり直される)。
1449
- result.__copilotTermuxPatched = true;
1450
- }
1451
-
1452
- // === cli-native.node stubs (1.0.64+: color scheme fns use Rust tokio → SIGSEGV on bionic) ===
1453
- if (typeof request === 'string' && path.basename(request) === 'cli-native.node') {
1454
- if (!result.__copilotTermuxCliPatched) {
1455
- result.__copilotTermuxCliPatched = true;
1456
- // Rust tokio background thread → SIGSEGV on bionic. Return null so callers
1457
- // fall back to "unspecified" color scheme (uses default theme).
1458
- if (typeof result.getColorScheme === 'function') {
1459
- result.getColorScheme = () => null;
1460
- }
1461
- if (typeof result.startColorSchemeListener === 'function') {
1462
- result.startColorSchemeListener = (_cb) => undefined;
1463
- }
1464
- if (typeof result.stopColorSchemeListener === 'function') {
1465
- result.stopColorSchemeListener = () => undefined;
1466
- }
1467
- }
1468
- }
1469
- // === end cli-native.node stubs ===
1470
-
1471
- return result;
1472
- };
1473
-
1474
- // [Android bionic 対応] app.js の oPt() が globalThis.fetch を Rust ベースの B7 に
1475
- // 差し替えるのを阻止する。linuxmusl-arm64/runtime.node の Rust ネットワークスタックは
1476
- // bionic 上の実 I/O で動作しないため、Node.js ビルトイン fetch(動作確認済み)に固定する。
1477
- // GitHub OAuth token via env var or gh CLI (keychain unavailable on bionic)
1478
-
1479
- // glibc modeでも維持(isGlibcMode分岐なし、かつファイルスコープのためそもそも分岐不可):
1480
- // JS実装のnetworkFetch*/modelHttp*/capiClientListModelsがglobalThis.fetchに依存しているため、
1481
- // ここだけglibc modeでnative fetchに戻すと、JSスタブがRust fetch経由になり前提が崩れる。
1482
-
1483
- const _COPILOT_TOKEN_DEFAULT_TTL_MS = 28 * 60 * 1000;
1484
- function _normalizeCopilotTokenExpiry(expiresAt, now = Date.now()) {
1485
- const parsed = expiresAt ? Date.parse(expiresAt) : NaN;
1486
- return Number.isFinite(parsed) && parsed > now ? parsed : now + _COPILOT_TOKEN_DEFAULT_TTL_MS;
1487
- }
1488
-
1489
- async function _readGhToken(env) {
1490
- const envToken =
1491
- (env && (env.GITHUB_TOKEN || env.GH_TOKEN || env.COPILOT_GITHUB_TOKEN)) ||
1492
- process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.COPILOT_GITHUB_TOKEN;
1493
- if (envToken) {
1494
- return envToken;
1495
- }
1496
- try {
1497
- const { execFile } = require('child_process');
1498
- const ghToken = await new Promise(resolve => {
1499
- execFile('gh', ['auth', 'token'], { encoding: 'utf8', timeout: 5000 }, (err, stdout) => {
1500
- resolve(err ? null : (stdout.trim() || null));
1501
- });
1502
- });
1503
- return ghToken;
1504
- } catch (_) {
1505
- return null;
1506
- }
1507
- }
1508
-
1509
- const _nativeFetch = globalThis.fetch;
1510
- Object.defineProperty(globalThis, 'fetch', {
1511
- configurable: true,
1512
- enumerable: true,
1513
- get() { return _nativeFetch; },
1514
- set(_) { /* B7 代入を無視 */ },
1515
- });
1516
-
1517
- // UPDATE-001 / UPDATE-003: 公式 GitHub Copilot CLI (@github/copilot) の app.js に対するパッチ。
1518
- // (1) `/update` が表示するインストールコマンド文字列を fork のパッケージ名に差し替える (UPDATE-001)
1519
- // (2) upstream 公式リポジトリ (github/copilot-cli) のリリースチェックに基づく起動時通知バナーを無効化する (UPDATE-003)
1520
- // app.js は ~/.copilot-termux/<version>/package.json の "type":"module" により ESM としてロードされる。
1521
- // CJS専用の Module._extensions['.js'] はESMコンパイルに一切関与しないため機能しない
1522
- // (2026-07-02 実機再現で確認済み)。正しい介入点は node:module の registerHooks()
1523
- // (Node v22.15.0/v23.5.0+ で追加された同期 ESM Loader Hook)。未対応のNodeではフィーチャー検出で
1524
- // スキップし、パッチなしでフォールバックする。
1525
- const { fileURLToPath } = require('url');
1526
-
1527
- function isTargetCopilotAppJsUrl(url) {
1528
- if (typeof url !== 'string' || !url.startsWith('file://')) return false;
1529
- let filename;
1530
- try {
1531
- filename = fileURLToPath(url);
1532
- } catch (_) {
1533
- return false;
1534
- }
1535
- // app.js が `.copilot-termux/<version-or-current>/app.js` に直接あることを要求する
1536
- // (codex STEP8 指摘: basename + セグメント包含だけだと
1537
- // `.copilot-termux/<version>/node_modules/**/app.js` のような無関係な深い階層にも
1538
- // 誤反応しうるため、`.copilot-termux` の直後2セグメント目である場合のみ許可する)
1539
- if (path.basename(filename) !== 'app.js') return false;
1540
- const segments = filename.split(/[\\/]/);
1541
- const idx = segments.indexOf('.copilot-termux');
1542
- if (idx === -1) return false;
1543
- return idx + 2 === segments.length - 1;
1544
- }
1545
-
1546
- function patchAppJsSource(source) {
1547
- let patched = source;
1548
-
1549
- const INSTALL_CMD_PATTERN = /`npm i -g @github\/copilot@\$\{[^}]+\}`/g;
1550
- const installMatches = patched.match(INSTALL_CMD_PATTERN);
1551
- if (installMatches && installMatches.length === 1) {
1552
- patched = patched.replace(INSTALL_CMD_PATTERN, '`npm install -g @bash0816/copilot-termux`');
1553
- } else {
1554
- console.warn('[copilot-termux] UPDATE-001: update string pattern ' +
1555
- (installMatches ? 'found ' + installMatches.length + ' times' : 'not found') + ', skipping patch');
1556
- }
1557
-
1558
- const NOTIFY_PATTERN = /[a-zA-Z0-9_$]+\.gt\([a-zA-Z0-9_$]+\.tag_name,[a-zA-Z0-9_$]+\(\)\)&&\([a-zA-Z0-9_$]+\.info\(`Update available: \$\{[a-zA-Z0-9_$]+\.tag_name\}`\),[a-zA-Z0-9_$]+\.sendUpdateNotification\(`\$\{[a-zA-Z0-9_$]+\.tag_name\} available \\xB7 run \/update`\)\)/g;
1559
- const notifyMatches = patched.match(NOTIFY_PATTERN);
1560
- if (notifyMatches && notifyMatches.length === 1) {
1561
- patched = patched.replace(NOTIFY_PATTERN, 'false');
1562
- } else {
1563
- console.warn('[copilot-termux] UPDATE-003: upstream release notification pattern ' +
1564
- (notifyMatches ? 'found ' + notifyMatches.length + ' times' : 'not found') + ', skipping patch');
1565
- }
1566
-
1567
- // UPDATE-006: DO() 内の releases/latest 取得先をフォーク自身の npm dist-tag latest に差し替える。
1568
- // 変数名 o,n はDO()固有(a$e()は a,r を使うため誤爆しない)。
1569
- // changelog本文の取得(nj.execute / fetchReleaseByTag、owner:"github")は一切変更しない。
1570
- const FORK_LATEST_PATTERN = /return\(await rF\(o=>gR\("GET \/repos\/\{owner\}\/\{repo\}\/releases\/latest",\{owner:"github",repo:"copilot-cli",headers:o\}\),n\)\)\.data/g;
1571
- const forkLatestMatches = patched.match(FORK_LATEST_PATTERN);
1572
- if (forkLatestMatches && forkLatestMatches.length === 1) {
1573
- patched = patched.replace(FORK_LATEST_PATTERN,
1574
- 'return await(async()=>{try{' +
1575
- 'const res=await fetch("https://registry.npmjs.org/%40bash0816%2Fcopilot-termux/latest",{signal:AbortSignal.timeout(5000)});' +
1576
- 'if(!res.ok)throw new Error("npm registry returned "+res.status);' +
1577
- 'const data=await res.json();' +
1578
- 'if(!data||typeof data.version!=="string")throw new Error("npm registry response missing version");' +
1579
- 'const ver=data.version.replace(/-\\d+$/,"");' +
1580
- 'if(!/^\\d+\\.\\d+\\.\\d+$/.test(ver))throw new Error("invalid version: "+data.version);' +
1581
- 'return{tag_name:"v"+ver,assets:[]};' +
1582
- '}catch(e){return{error:String(e)};}})()'
1583
- );
1584
- } else {
1585
- console.warn('[copilot-termux] UPDATE-006: fork latest pattern ' +
1586
- (forkLatestMatches ? 'found ' + forkLatestMatches.length + ' times' : 'not found') + ', skipping patch');
1587
- }
1588
-
1589
- // UPDATE-006b: 「更新なし」の場合のchangelog表示をcurrent(a)からfork-latest(u)に変更する。
1590
- // upstreamは「更新なし → nj.execute(t,[a])」(aは現在バージョン)だが、
1591
- // フォーク自身のnpm latestを判定元にした結果、currentがfork-latestより新しい場合でも
1592
- // aのchangelogが出続ける問題を解消する。
1593
- // nj.execute内部(fetchReleaseByTag / owner:"github" / changelog.json)は変更しない。
1594
- const NO_UPDATE_PATTERN = /if\(!ELt\.default\.gt\(u,a\)\)return nj\.execute\(t,\[a\]\)/g;
1595
- const noUpdateMatches = patched.match(NO_UPDATE_PATTERN);
1596
- if (noUpdateMatches && noUpdateMatches.length === 1) {
1597
- patched = patched.replace(NO_UPDATE_PATTERN,
1598
- 'if(!ELt.default.gt(u,a))return nj.execute(t,[u.replace(/^v/,"")])'
1599
- );
1600
- } else {
1601
- console.warn('[copilot-termux] UPDATE-006b: no-update changelog pattern ' +
1602
- (noUpdateMatches ? 'found ' + noUpdateMatches.length + ' times' : 'not found') + ', skipping patch');
1603
- }
1604
-
1605
- return patched;
1606
- }
1607
-
1608
- if (!globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__) {
1609
- globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__ = true;
1610
- const { registerHooks } = require('module');
1611
- if (typeof registerHooks === 'function') {
1612
- registerHooks({
1613
- load(url, context, nextLoad) {
1614
- const result = nextLoad(url, context);
1615
- if (!isTargetCopilotAppJsUrl(url)) return result;
1616
- if (result.source == null) return result;
1617
- const wasNonString = typeof result.source !== 'string';
1618
- const src = wasNonString ? Buffer.from(result.source).toString('utf8') : result.source;
1619
- const patched = patchAppJsSource(src);
1620
- return Object.assign({}, result, { source: patched });
1621
- }
1622
- });
1623
- } else {
1624
- console.warn('[copilot-termux] UPDATE-001/003: node:module registerHooks() not available on this Node version (' + process.version + '), skipping app.js patch');
1625
- }
1626
- }
1627
-
1628
- module.exports.patchAppJsSource = patchAppJsSource;
1629
- module.exports.isTargetCopilotAppJsUrl = isTargetCopilotAppJsUrl;
1630
- module.exports.listGitRemotes = listGitRemotes;
1631
- module.exports.parseGitRemoteUrl = parseGitRemoteUrl;
1632
- module.exports.resolveRepoIdentifier = resolveRepoIdentifier;
1633
- module.exports.buildWorkingDirectoryContext = buildWorkingDirectoryContext;