@bash0816/copilot-termux 1.0.65 → 1.0.68

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.
@@ -32,11 +32,13 @@ Module._resolveFilename = function (request, parent, isMain, options) {
32
32
  // Redirect pty.node to our bundled Termux-native build.
33
33
  if (path.basename(request) === 'pty.node' &&
34
34
  (request.includes('linux-arm64') || request.includes('linuxmusl-arm64'))) {
35
- if (fs.existsSync(NATIVE_PTY)) return NATIVE_PTY;
35
+ // glibc mode では bionic NDK pty.node を使わない(glibc Node との ABI 不整合)
36
+ if (!process.env.COPILOT_TERMUX_GLIBC_MODE && fs.existsSync(NATIVE_PTY)) return NATIVE_PTY;
36
37
  }
37
38
 
38
39
  // Redirect other linux-arm64 addons to linuxmusl-arm64 variants.
39
- if (request.includes('linux-arm64')) {
40
+ // In glibc mode (COPILOT_TERMUX_GLIBC_MODE=1), skip redirect — glibc addons load natively.
41
+ if (request.includes('linux-arm64') && !process.env.COPILOT_TERMUX_GLIBC_MODE) {
40
42
  const muslReq = request.replace(/linux-arm64/g, 'linuxmusl-arm64');
41
43
  try {
42
44
  const resolved = origResolve(muslReq, parent, isMain, options);
@@ -64,65 +66,99 @@ Module._load = function (request, parent, isMain) {
64
66
  path.basename(request) === 'runtime.node') {
65
67
  if (result.__copilotTermuxPatched) return result;
66
68
  result.__copilotTermuxPatched = true;
67
- // Rust tokio を使う関数群を no-op に差し替え。
68
- // sessionStore*/sessionSqlite* は非同期 SQLite (tokio)
69
- // modelHttp*/networkFetch*/ahpRelay*/websocketResponses* Rust HTTP (tokio)。
70
- // jsonrpcServer* は拡張 JSON-RPC サーバー (ThreadsafeFunction)、
71
- // lspClient* は LSP クライアント (ThreadsafeFunction)。
72
- // featureFlagService* は同期 Rust のため除外(no-op にすると .handle クラッシュ)。
73
- const TOKIO_PATTERN = /^(modelHttp|networkFetch|ahpRelay|websocketResponses|sessionStore|sessionSqlite|jsonrpcServer|lspClient)/;
74
- for (const key of Object.keys(result)) {
75
- if (TOKIO_PATTERN.test(key) && typeof result[key] === 'function') {
76
- result[key] = () => undefined;
69
+ const isGlibcMode = !!process.env.COPILOT_TERMUX_GLIBC_MODE;
70
+ if (!isGlibcMode) {
71
+ // Rust tokio を使う関数群を no-op に差し替え。
72
+ // sessionStore*/sessionSqlite* は非同期 SQLite (tokio)、
73
+ // modelHttp*/networkFetch*/ahpRelay*/websocketResponses* は Rust HTTP (tokio)。
74
+ // jsonrpcServer* は拡張 JSON-RPC サーバー (ThreadsafeFunction)、
75
+ // lspClient* LSP クライアント (ThreadsafeFunction)
76
+ // featureFlagService* は同期 Rust のため除外(no-op にすると .handle クラッシュ)。
77
+ const TOKIO_PATTERN = /^(ahpRelay|ahpRelayAuthenticate|ahpRelayCancelTurn|ahpRelayCompleteInput|ahpRelayCompletions|ahpRelayConfirmToolCall|ahpRelayConnect|ahpRelayCreate|ahpRelayCreateSession|ahpRelayDispose|ahpRelayGetPlan|ahpRelayListCheckpoints|ahpRelayListWorkspaceFiles|ahpRelayPendingRequiredResourcesJson|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|jsonrpcServerRegisterSession|jsonrpcServerRemove|jsonrpcServerRemoveSession|jsonrpcServerStartTcpListener|jsonrpcServerStopTcpListener|lspClient|lspClientCloseDocument|lspClientCreateOwned|lspClientCreateOwnedSandboxed|lspClientDispose|lspClientEnhanceStartupErrorMessage|lspClientFindSourceFile|lspClientInitialize|lspClientInitialized|lspClientOpenDocument|lspClientOwnedShutdown|lspClientRequest|lspClientTakeExitInfo|lspClientWaitForDiagnostics|lspClientWaitForProjectLoad|modelHttp|modelHttpCancelRequest|modelHttpRegisterCancellation|modelHttpResetNetworking|networkFetch|networkFetchResetClients|sessionSqlite|sessionSqliteClose|sessionSqliteExec|sessionStore|sessionStoreBeginForgeSkillProposalGeneration|sessionStoreClose|sessionStoreCompleteForgeSkillProposalGeneration|sessionStoreDefaultPath|sessionStoreDeleteDynamicContextItem|sessionStoreEnsureSession|sessionStoreExec|sessionStoreExecuteReadOnly|sessionStoreFailStaleGeneratingForgeSkillProposals|sessionStoreGetCheckpoints|sessionStoreGetDynamicContextBoard|sessionStoreGetDynamicContextItem|sessionStoreGetFiles|sessionStoreGetForgeSkillProposalByFingerprint|sessionStoreGetForgeSkillProposalById|sessionStoreGetForgeSkillProposalWorkspaceBefore|sessionStoreGetForgeTrajectoryEvents|sessionStoreGetForgeTrajectoryEventsForScope|sessionStoreGetMaxTurnIndex|sessionStoreGetRefs|sessionStoreGetSession|sessionStoreGetStats|sessionStoreGetTurns|sessionStoreIncrementDynamicContextCount|sessionStoreIncrementDynamicContextReadCount|sessionStoreIndexWorkspaceArtifact|sessionStoreInsertCheckpointWithRuntimeDefaults|sessionStoreInsertDynamicContextItem|sessionStoreInsertFileWithRuntimeDefaults|sessionStoreInsertForgeTrajectoryEventWithRuntimeDefaults|sessionStoreInsertRefWithRuntimeDefaults|sessionStoreInsertTurnWithRuntimeDefaults|sessionStoreListForgeSkillProposals|sessionStoreOpen|sessionStoreSearch|sessionStoreTrackingEventOperations|sessionStoreTrackingExtractFilePath|sessionStoreTrackingExtractForgeTrajectoryEvents|sessionStoreTrackingExtractRefsFromBash|sessionStoreTrackingExtractRefsFromMcpTool|sessionStoreTrackingExtractRepoFromMcpTool|sessionStoreTrackingFlushOperations|sessionStoreTrackingInitialState|sessionStoreTransitionForgeSkillProposalStatus|sessionStoreUpsertDynamicContextItem|sessionStoreUpsertSessionWithRuntimeDefaults|websocketResponses)/;
78
+ for (const key of Object.keys(result)) {
79
+ if (TOKIO_PATTERN.test(key) && typeof result[key] === 'function') {
80
+ result[key] = () => undefined;
81
+ }
82
+ }
83
+ // git*Async: Rust tokio async functions — type-safe stubs to prevent SIGSEGV.
84
+ // Returns empty/null values matching what app.js callers expect.
85
+ const GIT_ASYNC_STUBS = {
86
+ gitMutateAsync: async () => undefined,
87
+ gitCommandAsync: async () => '',
88
+ gitRemotesAsync: async () => [],
89
+ gitDiffFileAsync: async () => '',
90
+ gitHashFileAsync: async () => [],
91
+ gitMergeBaseAsync: async () => null,
92
+ gitDiffForRefAsync: async () => '',
93
+ gitCurrentBranchAsync: async () => null,
94
+ gitDefaultBranchAsync: async () => null,
95
+ gitListWorktreesAsync: async () => [],
96
+ gitBranchAndHeadAsync: async () => null,
97
+ gitSubmodulePathsAsync: async () => [],
98
+ gitUntrackedPathsAsync: async () => [],
99
+ gitDiffNameStatusAsync: async () => '',
100
+ gitStatusPorcelainAsync: async () => '',
101
+ gitWorkingTreeStatusAsync: async () => ({ hasUnstagedChanges: false, hasStagedChanges: false, hasUntrackedFiles: false }),
102
+ gitStatusPorcelainAllAsync: async () => null,
103
+ gitCurrentBranchRemoteAsync: async () => null,
104
+ gitWorkingTreeDiffStatsAsync: async () => ({ linesAdded: 0, linesRemoved: 0 }),
105
+ };
106
+ for (const [key, stub] of Object.entries(GIT_ASYNC_STUBS)) {
107
+ if (typeof result[key] === 'function') result[key] = stub;
108
+ }
109
+ if (typeof result.registerLogSink === 'function') {
110
+ result.registerLogSink = () => { throw new Error('[copilot-termux] registerLogSink disabled on bionic (no tokio thread)'); };
111
+ }
112
+ if (typeof result.networkFetchGetExtraCaPems === 'function') {
113
+ result.networkFetchGetExtraCaPems = () => ({ errors: [], pems: [] });
114
+ }
115
+ if (typeof result.sessionSqliteOpen === 'function') {
116
+ result.sessionSqliteOpen = () => 1;
117
+ }
118
+ if (typeof result.sessionSqliteQuery === 'function') {
119
+ result.sessionSqliteQuery = () => ({ rows: '[]' });
120
+ }
121
+ if (typeof result.sessionSqliteRun === 'function') {
122
+ result.sessionSqliteRun = () => ({ rowsAffected: 0, lastInsertRowid: null });
123
+ }
124
+ if (typeof result.sessionSqliteFileExists === 'function') {
125
+ result.sessionSqliteFileExists = () => false;
77
126
  }
78
127
  }
79
- // git*Async: Rust tokio async functions — type-safe stubs to prevent SIGSEGV.
80
- // Returns empty/null values matching what app.js callers expect.
81
- const GIT_ASYNC_STUBS = {
82
- gitMutateAsync: async () => undefined,
83
- gitCommandAsync: async () => '',
84
- gitRemotesAsync: async () => [],
85
- gitDiffFileAsync: async () => '',
86
- gitHashFileAsync: async () => [],
87
- gitMergeBaseAsync: async () => null,
88
- gitDiffForRefAsync: async () => '',
89
- gitCurrentBranchAsync: async () => null,
90
- gitDefaultBranchAsync: async () => null,
91
- gitListWorktreesAsync: async () => [],
92
- gitBranchAndHeadAsync: async () => null,
93
- gitSubmodulePathsAsync: async () => [],
94
- gitUntrackedPathsAsync: async () => [],
95
- gitDiffNameStatusAsync: async () => '',
96
- gitStatusPorcelainAsync: async () => '',
97
- gitWorkingTreeStatusAsync: async () => ({ hasUnstagedChanges: false, hasStagedChanges: false, hasUntrackedFiles: false }),
98
- gitStatusPorcelainAllAsync: async () => null,
99
- gitCurrentBranchRemoteAsync: async () => null,
100
- gitWorkingTreeDiffStatsAsync: async () => ({ linesAdded: 0, linesRemoved: 0 }),
101
- };
102
- for (const [key, stub] of Object.entries(GIT_ASYNC_STUBS)) {
103
- if (typeof result[key] === 'function') result[key] = stub;
104
- }
105
- if (typeof result.registerLogSink === 'function') {
106
- result.registerLogSink = () => { throw new Error('[copilot-termux] registerLogSink disabled on bionic (no tokio thread)'); };
107
- }
108
- if (typeof result.networkFetchGetExtraCaPems === 'function') {
109
- result.networkFetchGetExtraCaPems = () => ({ errors: [], pems: [] });
110
- }
111
- // modelsFilterToPicker: native-first。native が空のとき(model_picker_enabled=true のモデルが
112
- // 全くない場合)のみ全インデックス fallback。
113
- // free アカウントでは全モデルが model_picker_enabled=false → native 空 → fallback で全通し
114
- //(auto モードが enterprise 向けモデルを選んで 400 になる副作用は AUTH-001 解決後に再評価)。
115
- // enterprise アカウントでは native が正しくフィルタする。
116
- if (typeof result.modelsFilterToPicker === 'function') {
117
- const _nativeModelsFilterToPicker = result.modelsFilterToPicker;
118
- result.modelsFilterToPicker = function(modelsJson) {
119
- const nativeResult = _nativeModelsFilterToPicker(modelsJson);
120
- _dbg('modelsFilterToPicker', { nativeResult, modelCount: (() => { try { return JSON.parse(modelsJson).length; } catch(_) { return -1; } })() });
121
- if (Array.isArray(nativeResult) && nativeResult.length > 0) return nativeResult;
128
+ // agentsResolveToolAliases: v1.0.65 新規追加ネイティブ関数。
129
+ // Free ユーザーのチャット時に呼ばれ、Bionic で誤ったインデックスを返すと
130
+ // tools[N].function.name undefined → Copilot API 400 になる。
131
+ // ネイティブ結果を検証し、無効なら JS 実装でフォールバックする。
132
+ if (typeof result.agentsResolveToolAliases === 'function') {
133
+ const _nativeResolveAliases = result.agentsResolveToolAliases;
134
+ result.agentsResolveToolAliases = function(allowedTools, allToolsMeta, externalToolsMeta) {
122
135
  try {
123
- const models = JSON.parse(modelsJson);
124
- return Array.isArray(models) ? models.map((_, i) => i) : [];
125
- } catch(_) { return []; }
136
+ const r = _nativeResolveAliases(allowedTools, allToolsMeta, externalToolsMeta);
137
+ if (Array.isArray(r) && r.length > 0 &&
138
+ r.every(i => typeof i === 'number' && i >= 0 && i < allToolsMeta.length)) {
139
+ return r;
140
+ }
141
+ } catch(e) {
142
+ }
143
+ // JS フォールバック
144
+ const allowed = allowedTools;
145
+ // null または ["*"] → 全ツール
146
+ if (!allowed || (allowed.length === 1 && allowed[0] === '*')) {
147
+ return allToolsMeta.map((_, i) => i);
148
+ }
149
+ // 空配列 → 空配列(許可ツールなし)
150
+ if (allowed.length === 0) {
151
+ return [];
152
+ }
153
+ // 名前マッチング
154
+ const allowedSet = new Set(allowed.map(n => (n || '').toLowerCase()));
155
+ const indices = [];
156
+ allToolsMeta.forEach((tool, i) => {
157
+ const name = (tool.name || '').toLowerCase();
158
+ const ns = (tool.namespacedName || '').toLowerCase();
159
+ if (allowedSet.has(name) || allowedSet.has(ns)) indices.push(i);
160
+ });
161
+ return indices;
126
162
  };
127
163
  }
128
164
  // authGetCopilotApiUrl: type=token/env/user/gh-cli/api-key では native が null を返す。
@@ -136,15 +172,68 @@ Module._load = function (request, parent, isMain) {
136
172
  try {
137
173
  const info = JSON.parse(authInfoJson);
138
174
  if (info && info.type !== 'hmac') {
139
- return process.env.COPILOT_API_URL || 'https://api.githubcopilot.com';
175
+ return 'https://api.githubcopilot.com';
140
176
  }
141
177
  } catch (_) {}
142
178
  return r;
143
179
  };
144
180
  }
181
+ // capiClientPrepareRequestHeaders: native は OAuth token をそのまま Bearer にする。
182
+ // Copilot 推論 API は Copilot token が必要なため、キャッシュ済み copilotToken で差し替え。
183
+ if (typeof result.capiClientPrepareRequestHeaders === 'function') {
184
+ const _nativePrepareRequestHeaders = result.capiClientPrepareRequestHeaders;
185
+ result.capiClientPrepareRequestHeaders = function(handle, ...args) {
186
+ const prepared = _nativePrepareRequestHeaders(handle, ...args);
187
+ // _authMgr から有効な copilotToken を探して Authorization を差し替え
188
+ const now = Date.now();
189
+ for (const entry of _authMgr.values()) {
190
+ if (entry.copilotToken && (entry.copilotTokenExpiry === 0 || entry.copilotTokenExpiry > now)) {
191
+ if (prepared && Array.isArray(prepared.headers)) {
192
+ const headers = prepared.headers.map(h =>
193
+ h.name && h.name.toLowerCase() === 'authorization'
194
+ ? { name: h.name, value: `Bearer ${entry.copilotToken}` }
195
+ : h
196
+ );
197
+ return { ...prepared, headers };
198
+ }
199
+ }
200
+ }
201
+ return prepared;
202
+ };
203
+ }
204
+ // modelsFilterToPicker: native は model_picker_enabled=true のモデルのみ返す。
205
+ // Free プランは全モデル model_picker_enabled=false → native が [] を返す。
206
+ // PICKER-001: fallback に gpt-4o-mini/gpt-4o を返すと TUI /model ピッカーに
207
+ // ユーティリティモデルが表示されてしまう(公式仕様違反)。
208
+ // auto モードの default 選択は modelResolverFirstAvailableDefaultFromOrder が担うため
209
+ // ここでは fallback せず [] を返す。
210
+ if (typeof result.modelsFilterToPicker === 'function') {
211
+ const _nativeModelsFilterToPicker = result.modelsFilterToPicker;
212
+ result.modelsFilterToPicker = function(...args) {
213
+ const nativeResult = _nativeModelsFilterToPicker.apply(this, args);
214
+ if (Array.isArray(nativeResult) && nativeResult.length > 0) return nativeResult;
215
+ return [];
216
+ };
217
+ }
145
218
  // capiClientListModels を Node.js fetch で実装(Rust tokio SIGSEGV 回避)
219
+ // _selectCapiUrl: COPILOT_API_URL が api.individual.githubcopilot.com(Free プラン専用 direct endpoint)の
220
+ // 場合のみそれを使う。Enterprise proxy URL(api.business.githubcopilot.com 等)では /models が 421 に
221
+ // なるため標準 CAPI に倒す。hostname 完全一致チェックで SSRF/token 漏洩を防ぐ(GPT-5.5 指摘 #1)。
222
+ const _DEFAULT_CAPI_URL = 'https://api.githubcopilot.com';
223
+ const _INDIVIDUAL_CAPI_URL = 'https://api.individual.githubcopilot.com';
224
+ function _selectCapiUrl(rawApiUrl) {
225
+ try {
226
+ const u = new URL(rawApiUrl || _DEFAULT_CAPI_URL);
227
+ if (u.protocol === 'https:' && u.hostname === 'api.individual.githubcopilot.com') {
228
+ return _INDIVIDUAL_CAPI_URL;
229
+ }
230
+ } catch (e) {
231
+ }
232
+ return _DEFAULT_CAPI_URL;
233
+ }
146
234
  if (typeof result.capiClientListModels === 'function') {
147
235
  result.capiClientListModels = async function(handle, _includeHidden, _skipCache, _applyModelLimitCaps, _networkingConfigId) {
236
+ const snapshotGen = _modelListCacheGen; // 開始時の世代をキャプチャ → 完了時に照合して stale 結果を破棄
148
237
  let authHeaders;
149
238
  try {
150
239
  const prepared = result.capiClientPrepareRequestHeaders(handle, '', []);
@@ -155,11 +244,14 @@ Module._load = function (request, parent, isMain) {
155
244
  } catch (e) {
156
245
  throw new Error(JSON.stringify({kind: 'network', message: `prepareHeaders failed: ${e.message}`}));
157
246
  }
158
- const baseUrl = process.env.COPILOT_API_URL || 'https://api.githubcopilot.com';
159
- _dbg('capiClientListModels:fetch', { baseUrl });
247
+ // fetchUrl: /models 取得URL。Free は api.individual(COPILOT_API_URL 設定時)、他は標準 CAPI。
248
+ // copilotUrl: 推論URL。常に標準 CAPI 固定(v1.0.63 と同じ)。
249
+ // BUG-NEW-2 で copilotUrl=fetchUrl にしたため api.individual への推論が発生していた(副作用修正)。
250
+ const fetchUrl = _selectCapiUrl(process.env.COPILOT_API_URL);
251
+ const copilotUrl = _DEFAULT_CAPI_URL;
160
252
  let res;
161
253
  try {
162
- res = await globalThis.fetch(`${baseUrl}/models`, {method: 'GET', headers: authHeaders});
254
+ res = await globalThis.fetch(`${fetchUrl}/models`, {method: 'GET', headers: authHeaders});
163
255
  } catch (e) {
164
256
  throw new Error(JSON.stringify({kind: 'network', message: e.message}));
165
257
  }
@@ -169,23 +261,16 @@ Module._load = function (request, parent, isMain) {
169
261
  throw new Error(JSON.stringify({kind: 'http', status: res.status, statusText: res.statusText, body, headers: hdrs, hasRequestId: res.headers.has('x-request-id')}));
170
262
  }
171
263
  const data = await res.json();
172
- const models = Array.isArray(data) ? data : (data.data ?? data.models ?? []);
264
+ const raw = Array.isArray(data) ? data : (data.data ?? data.models ?? []);
265
+ const models = Array.isArray(raw) ? raw : [];
266
+ if (_modelListCacheGen === snapshotGen) {
267
+ _modelListCache = models;
268
+ } else {
269
+ }
173
270
  const rateHeaders = [...res.headers.entries()].map(([name, value]) => ({name, value}));
174
- return {modelsJson: JSON.stringify(models), copilotUrl: baseUrl, usageRatelimitHeaders: rateHeaders, capturedAssignmentContext: undefined};
271
+ return {modelsJson: JSON.stringify(models), copilotUrl, usageRatelimitHeaders: rateHeaders, capturedAssignmentContext: undefined};
175
272
  };
176
273
  }
177
- if (typeof result.sessionSqliteOpen === 'function') {
178
- result.sessionSqliteOpen = () => 1;
179
- }
180
- if (typeof result.sessionSqliteQuery === 'function') {
181
- result.sessionSqliteQuery = () => ({ rows: '[]' });
182
- }
183
- if (typeof result.sessionSqliteRun === 'function') {
184
- result.sessionSqliteRun = () => ({ rowsAffected: 0, lastInsertRowid: null });
185
- }
186
- if (typeof result.sessionSqliteFileExists === 'function') {
187
- result.sessionSqliteFileExists = () => false;
188
- }
189
274
  // --- JS networkFetch* implementation (bionic: tokio networkFetch* are no-op'd) ---
190
275
  // B7 が QXe() から直接呼ばれる MCP 専用パスでクラッシュするため JS で代替。
191
276
  // networkFetchStreamStart → { requestId, response: Promise<{handle,url,status,statusText,headers}> }
@@ -201,6 +286,24 @@ Module._load = function (request, parent, isMain) {
201
286
  const entry = { abort: () => abortCtrl.abort(), reader: null };
202
287
  _nfMap.set(requestId, entry);
203
288
 
289
+ // Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
290
+ let body = req.body ?? undefined;
291
+ if (body) {
292
+ try {
293
+ const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
294
+ if (parsed && Array.isArray(parsed.tools)) {
295
+ const before = parsed.tools.length;
296
+ parsed.tools = parsed.tools.filter(
297
+ t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
298
+ );
299
+ if (parsed.tools.length !== before) {
300
+ if (parsed.tools.length === 0) delete parsed.tools;
301
+ body = JSON.stringify(parsed);
302
+ }
303
+ }
304
+ } catch(_) {}
305
+ }
306
+
204
307
  const headers = {};
205
308
  if (Array.isArray(req.headers)) {
206
309
  for (const { name, value } of req.headers) headers[name] = value;
@@ -214,7 +317,7 @@ Module._load = function (request, parent, isMain) {
214
317
  res = await globalThis.fetch(req.url, {
215
318
  method: req.method || 'GET',
216
319
  headers,
217
- body: req.body ?? undefined,
320
+ body: body,
218
321
  signal: abortCtrl.signal,
219
322
  redirect: req.redirect || 'follow',
220
323
  });
@@ -387,7 +490,6 @@ Module._load = function (request, parent, isMain) {
387
490
  if (typeof result.anthropicMessageStreamAccumulatorFinish === 'function') {
388
491
  result.anthropicMessageStreamAccumulatorFinish = function(id) {
389
492
  const acc = _jsAccs.get(id) || { message: null };
390
- _dbg('accumulatorFinish', { id, message: acc.message ? { stop_reason: acc.message.stop_reason, contentTypes: (acc.message.content||[]).map(b=>b.type+':'+(b.text||'').slice(0,30)) } : null });
391
493
  _jsAccs.delete(id);
392
494
  return { json: JSON.stringify({ message: acc.message }) };
393
495
  };
@@ -403,7 +505,6 @@ Module._load = function (request, parent, isMain) {
403
505
  // 4. modelHttpStreamStart → fetch full SSE body, parse events
404
506
  result.modelHttpStreamStart = async function(jsonArg) {
405
507
  const req = JSON.parse(jsonArg);
406
- _dbg('modelHttpStreamStart', { url: req.url, method: req.method });
407
508
  let body = req.body;
408
509
  if (body !== null && body !== undefined && typeof body === 'object') {
409
510
  if (body.type === 'Buffer' && Array.isArray(body.data)) {
@@ -412,11 +513,44 @@ Module._load = function (request, parent, isMain) {
412
513
  body = JSON.stringify(body);
413
514
  }
414
515
  }
516
+ // Fix 2 (MODEL-001): /responses 以外は reasoning_effort を除去(/v1/messages 等が 400 になるのを防ぐ)
517
+ if (body && req.url && !req.url.includes('/responses')) {
518
+ try {
519
+ const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
520
+ if (parsed && 'reasoning_effort' in parsed) {
521
+ delete parsed.reasoning_effort;
522
+ body = JSON.stringify(parsed);
523
+ }
524
+ } catch(_) {}
525
+ }
526
+ // Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
527
+ if (body) {
528
+ try {
529
+ const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
530
+ if (parsed && Array.isArray(parsed.tools)) {
531
+ const before = parsed.tools.length;
532
+ parsed.tools = parsed.tools.filter(
533
+ t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
534
+ );
535
+ if (parsed.tools.length !== before) {
536
+ if (parsed.tools.length === 0) delete parsed.tools;
537
+ body = JSON.stringify(parsed);
538
+ }
539
+ }
540
+ } catch(_) {}
541
+ }
542
+ // Fix (TC-6): Free account model override — Enterprise→Free 切替後に stale モデルが残る問題
543
+ body = _applyModelOverride(body, req.url, 'modelHttpStreamStart');
544
+ // body 書き換え後に content-length が古くなるのを防ぐ(reasoning_effort/tools/model 除去で長さが変わる)
545
+ const fetchHeaders = {};
546
+ for (const [k, v] of Object.entries(req.headers || {})) {
547
+ if (k.toLowerCase() !== 'content-length') fetchHeaders[k] = v;
548
+ }
415
549
  let res;
416
550
  try {
417
551
  res = await globalThis.fetch(req.url, {
418
552
  method: req.method || 'POST',
419
- headers: req.headers || {},
553
+ headers: fetchHeaders,
420
554
  body: body,
421
555
  });
422
556
  } catch(e) {
@@ -429,13 +563,11 @@ Module._load = function (request, parent, isMain) {
429
563
  return { json: JSON.stringify({ bodyText, status: res.status, statusText: res.statusText, headers, streamId: null }) };
430
564
  }
431
565
  const events = _parseAnthropicSSE(bodyText);
432
- _dbg('modelHttpStreamStart:parsed', { status: res.status, eventCount: events.length, bodySnippet: bodyText.slice(0, 300) });
433
566
  const finalMessage = _reconstructFinalMessage(events);
434
567
  const streamId = 'js-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
435
568
  _jsStreams.set(streamId, { events, index: 0, finalMessage });
436
569
  return { json: JSON.stringify({ bodyText: null, status: res.status, statusText: res.statusText, headers, streamId }) };
437
570
  };
438
-
439
571
  // Helper: convert Anthropic SSE event → chunkContext for processAnthropicStreamingChunkContext
440
572
  function _toChunkContext(event, streamId) {
441
573
  const base = { content: '', size: 0, chunkBoundary: false, messageStart: false, streamingId: streamId };
@@ -464,7 +596,6 @@ Module._load = function (request, parent, isMain) {
464
596
  return base;
465
597
  }
466
598
  }
467
-
468
599
  // 5. modelHttpStreamNextAnthropicMessageEvent → yield events one by one
469
600
  result.modelHttpStreamNextAnthropicMessageEvent = async function(streamId, accId) {
470
601
  const st = _jsStreams.get(streamId);
@@ -498,11 +629,44 @@ Module._load = function (request, parent, isMain) {
498
629
  body = JSON.stringify(body);
499
630
  }
500
631
  }
632
+ // Fix 2 (MODEL-001): /responses 以外は reasoning_effort を除去
633
+ if (body && req.url && !req.url.includes('/responses')) {
634
+ try {
635
+ const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
636
+ if (parsed && 'reasoning_effort' in parsed) {
637
+ delete parsed.reasoning_effort;
638
+ body = JSON.stringify(parsed);
639
+ }
640
+ } catch(_) {}
641
+ }
642
+ // Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
643
+ if (body) {
644
+ try {
645
+ const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
646
+ if (parsed && Array.isArray(parsed.tools)) {
647
+ const before = parsed.tools.length;
648
+ parsed.tools = parsed.tools.filter(
649
+ t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
650
+ );
651
+ if (parsed.tools.length !== before) {
652
+ if (parsed.tools.length === 0) delete parsed.tools;
653
+ body = JSON.stringify(parsed);
654
+ }
655
+ }
656
+ } catch(_) {}
657
+ }
658
+ // Fix (TC-6): Free account model override — Enterprise→Free 切替後に stale モデルが残る問題
659
+ body = _applyModelOverride(body, req.url, 'modelHttpRequest');
660
+ // body 書き換え後に content-length が古くなるのを防ぐ
661
+ const fetchHeaders = {};
662
+ for (const [k, v] of Object.entries(req.headers || {})) {
663
+ if (k.toLowerCase() !== 'content-length') fetchHeaders[k] = v;
664
+ }
501
665
  let res;
502
666
  try {
503
667
  res = await globalThis.fetch(req.url, {
504
668
  method: req.method || 'POST',
505
- headers: req.headers || {},
669
+ headers: fetchHeaders,
506
670
  body: body,
507
671
  });
508
672
  } catch(e) {
@@ -524,7 +688,6 @@ Module._load = function (request, parent, isMain) {
524
688
  if (!st) {
525
689
  throw new Error(`Native mod HTTP stream was not found: ${streamId}`);
526
690
  }
527
- _dbg('responsesStreamDrive:start', { streamId, eventCount: st.events.length, hasProcessors, sample: st.events.slice(0,2) });
528
691
  _jsStreams.delete(streamId);
529
692
  let copilotUsage = null;
530
693
  for (const event of st.events) {
@@ -535,7 +698,6 @@ Module._load = function (request, parent, isMain) {
535
698
  if (parsed && parsed.copilotUsage !== undefined && parsed.copilotUsage !== null)
536
699
  copilotUsage = parsed.copilotUsage;
537
700
  const cc = parsed && parsed.chunkContext;
538
- _dbg('responsesStreamDrive:chunk', { eventType: event.type, cc: cc ? { content: cc.content, size: cc.size, messageStart: !!cc.messageStart, chunkBoundary: !!cc.chunkBoundary } : null });
539
701
  if (hasProcessors && typeof onChunkCallback === 'function' && cc &&
540
702
  (cc.content || cc.messageStart || cc.reportIntentArguments || cc.chunkBoundary || cc.size > 0)) {
541
703
  try { onChunkCallback(JSON.stringify(cc)); } catch (_) {}
@@ -543,18 +705,141 @@ Module._load = function (request, parent, isMain) {
543
705
  }
544
706
  return { json: JSON.stringify({ kind: 'ok', copilotUsage, ttftMs: null, interTokenLatencyMs: null }) };
545
707
  };
708
+ // Helper: Free アカウント切替後に Enterprise モデルが残る問題を防ぐ model override。
709
+ // /chat/completions 系 URL にのみ適用し、_modelListCache の enabled モデルと照合。
710
+ // 対象外の場合は body をそのまま返す(破壊なし)。
711
+ function _applyModelOverride(body, url, prefix) {
712
+ try {
713
+ if (!body) return body;
714
+ const pathname = new URL(url).pathname;
715
+ // /chat/completions のみを対象にする
716
+ if (!pathname.endsWith('/chat/completions')) return body;
717
+ const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
718
+ if (!parsed || !parsed.model) return body;
719
+ if (_modelListCache && _modelListCache.length > 0) {
720
+ const enabledIds = new Set(
721
+ _modelListCache.filter(m => m?.policy?.state === 'enabled').map(m => m.id).filter(Boolean)
722
+ );
723
+ if (!enabledIds.has(parsed.model)) {
724
+ // goldeneye-free-auto のみを対象にする
725
+ const OVERRIDE_CANDIDATES = ['goldeneye-free-auto'];
726
+ let fallbackModel = null;
727
+ for (const id of OVERRIDE_CANDIDATES) {
728
+ const m = _modelListCache.find(m => m?.id === id && m?.policy?.state === 'enabled');
729
+ if (m) { fallbackModel = id; break; }
730
+ }
731
+ if (fallbackModel) {
732
+ parsed.model = fallbackModel;
733
+ return JSON.stringify(parsed);
734
+ }
735
+ }
736
+ }
737
+ } catch(_) {}
738
+ return body;
739
+ }
740
+
741
+ // 9. chatCompletionStreamDrive → OpenAI-compatible chat completion stream処理
742
+ // native は JS-side streamId("js-")を知らないため JS で代替。
743
+ result.chatCompletionStreamDrive = async function(streamId, reducerId, hasProcessors, onChunkCallback) {
744
+ const st = _jsStreams.get(streamId);
745
+ if (!st) {
746
+ throw new Error(`Native model HTTP stream was not found: ${streamId}`);
747
+ }
748
+ _jsStreams.delete(streamId);
749
+ let content = '', finishReason = null, id = null, model = null, role = 'assistant', usage = null, created = null;
750
+ const toolCallsByIndex = new Map(); // index → {id, type, function: {name, arguments}}
751
+ let functionCall = null; // 旧 delta.function_call 形式
752
+ let isFirstChunk = true;
753
+ // Fix (GPT-5.5 No-Go #1): 終端 finish_reason が来たか追跡する
754
+ // finishReason || 'stop' のデフォルトは壊れたストリームを正常完了に見せる危険がある
755
+ let seenTerminalFinishReason = false;
756
+ const TERMINAL_FINISH_REASONS = new Set(['stop', 'tool_calls', 'function_call', 'length', 'content_filter']);
757
+ for (const event of st.events) {
758
+ if (!event) continue;
759
+ if (event.id) id = event.id;
760
+ if (event.model) model = event.model;
761
+ if (event.usage) usage = event.usage;
762
+ if (event.created) created = event.created;
763
+ if (!Array.isArray(event.choices)) continue;
764
+ for (const choice of event.choices) {
765
+ if (choice.delta) {
766
+ if (choice.delta.role) role = choice.delta.role;
767
+ if (typeof choice.delta.content === 'string') content += choice.delta.content;
768
+ // tool_calls 断片を index ごとに集約
769
+ if (Array.isArray(choice.delta.tool_calls)) {
770
+ for (const tc of choice.delta.tool_calls) {
771
+ const idx = tc.index ?? 0;
772
+ if (!toolCallsByIndex.has(idx)) {
773
+ toolCallsByIndex.set(idx, { id: '', type: 'function', function: { name: '', arguments: '' } });
774
+ }
775
+ const entry = toolCallsByIndex.get(idx);
776
+ if (tc.id) entry.id = tc.id;
777
+ if (tc.type) entry.type = tc.type;
778
+ if (tc.function) {
779
+ if (tc.function.name) entry.function.name += tc.function.name;
780
+ if (typeof tc.function.arguments === 'string') entry.function.arguments += tc.function.arguments;
781
+ }
782
+ }
783
+ }
784
+ // 旧 function_call 形式
785
+ if (choice.delta.function_call) {
786
+ if (!functionCall) functionCall = { name: '', arguments: '' };
787
+ if (choice.delta.function_call.name) functionCall.name += choice.delta.function_call.name;
788
+ if (typeof choice.delta.function_call.arguments === 'string') functionCall.arguments += choice.delta.function_call.arguments;
789
+ }
790
+ }
791
+ if (choice.finish_reason) {
792
+ finishReason = choice.finish_reason;
793
+ if (TERMINAL_FINISH_REASONS.has(choice.finish_reason)) seenTerminalFinishReason = true;
794
+ }
795
+ }
796
+ if (hasProcessors && typeof onChunkCallback === 'function') {
797
+ let chunkContent = '', isBoundary = false, hasToolDelta = false;
798
+ for (const choice of event.choices) {
799
+ if (choice.delta && typeof choice.delta.content === 'string') chunkContent += choice.delta.content;
800
+ // Fix (GPT-5.5 No-Go #2): tool/function call delta も callback に通知する
801
+ if (choice.delta && (choice.delta.tool_calls || choice.delta.function_call)) hasToolDelta = true;
802
+ if (choice.finish_reason) isBoundary = true;
803
+ }
804
+ if (chunkContent || isBoundary || isFirstChunk || hasToolDelta) {
805
+ try { onChunkCallback(JSON.stringify({ content: chunkContent, size: Buffer.byteLength(chunkContent, 'utf8'), chunkBoundary: isBoundary, messageStart: isFirstChunk, streamingId: streamId })); } catch (_) {}
806
+ }
807
+ isFirstChunk = false;
808
+ }
809
+ }
810
+ const toolCalls = toolCallsByIndex.size > 0
811
+ ? Array.from(toolCallsByIndex.entries()).sort(([a], [b]) => a - b).map(([, v]) => v)
812
+ : undefined;
813
+ // tool call 系では content は null が正しい(テキストと排他)
814
+ const msgContent = (toolCalls || functionCall) ? (content || null) : content;
815
+ const message = { role, content: msgContent };
816
+ if (toolCalls) message.tool_calls = toolCalls;
817
+ if (functionCall) message.function_call = functionCall;
818
+ // Fix (GPT-5.5 No-Go #1): 終端が来ていない場合は 'stop' を補完しない
819
+ const effectiveFinishReason = seenTerminalFinishReason ? finishReason : (finishReason ?? null);
820
+ const completion = {
821
+ id: id || ('chatcmpl-' + Date.now().toString(36)),
822
+ object: 'chat.completion',
823
+ created: created || Math.floor(Date.now() / 1000),
824
+ model: model || 'gpt-4o',
825
+ choices: [{ index: 0, message, finish_reason: effectiveFinishReason, logprobs: null }],
826
+ usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
827
+ };
828
+ return { json: JSON.stringify({ kind: 'ok', completion, copilotUsage: null, ttftMs: null, interTokenLatencyMs: null }) };
829
+ };
546
830
  // === authManager* JS stubs (1.0.64: tokio thread spawn → SIGSEGV on bionic) ===
547
831
  const _authMgr = new Map(); // uuid → { cachedInfo, pendingInfo, cachedToken, cachedHost, gen }
832
+ let _modelListCache = null; // capiClientListModels が取得したモデルリストキャッシュ(modelResolver fallback 用)
833
+ let _modelListCacheGen = 0; // アカウント切替時にインクリメント → 古い /models 結果の上書きを防ぐ
548
834
 
549
835
  result.authManagerCreate = function(uuid, hostUri, userAgent, path, normSpec, header, envVar, disableAutoLogin) {
550
- _authMgr.set(uuid, { cachedInfo: null, pendingInfo: null, cachedToken: null, cachedHost: null, gen: 0 });
836
+ _authMgr.set(uuid, { cachedInfo: null, pendingInfo: null, cachedToken: null, cachedHost: null, gen: 0, copilotToken: null, copilotTokenExpiry: 0 });
551
837
  // native 非呼び出し: tokio runtime 生成を阻止
552
838
  };
553
839
 
554
840
  async function _buildAuthInfo(token, hostUri) {
555
841
  let login = null;
556
842
  let copilotUser = null;
557
- _dbg('buildAuthInfo:start', { tokenHash: _tokenHash(token), hostUri });
558
843
  try {
559
844
  const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
560
845
  const res = await globalThis.fetch(`${apiHost}/user`, {
@@ -562,9 +847,7 @@ Module._load = function (request, parent, isMain) {
562
847
  signal: AbortSignal.timeout(5000),
563
848
  });
564
849
  if (res.ok) login = (await res.json()).login;
565
- _dbg('buildAuthInfo:/user', { status: res.status, login });
566
850
  } catch (e) {
567
- _dbg('buildAuthInfo:/user:error', { err: e.message });
568
851
  }
569
852
  // copilot_internal/user から copilotUser 全体を取得して authInfo に含める。
570
853
  // app.js の Wa(authInfo) は authInfo.copilotUser.endpoints.api から CAPI base URL を導く。
@@ -573,7 +856,7 @@ Module._load = function (request, parent, isMain) {
573
856
  try {
574
857
  const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
575
858
  const r = await globalThis.fetch(`${apiHost}/copilot_internal/user`, {
576
- headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': 'copilot-chat' },
859
+ headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': process.env.GITHUB_COPILOT_INTEGRATION_ID || 'copilot-developer-cli' },
577
860
  signal: AbortSignal.timeout(5000),
578
861
  });
579
862
  if (r.ok) {
@@ -581,22 +864,35 @@ Module._load = function (request, parent, isMain) {
581
864
  copilotUser = info;
582
865
  const apiUrl = info?.endpoints?.api;
583
866
  if (apiUrl && typeof apiUrl === 'string') process.env.COPILOT_API_URL = apiUrl;
584
- _dbg('buildAuthInfo:/copilot_internal/user', {
585
- status: r.status,
586
- access_type_sku: info?.access_type_sku ?? null,
587
- copilot_plan: info?.copilot_plan ?? null,
588
- endpoints_api: apiUrl ?? null,
589
- COPILOT_API_URL: process.env.COPILOT_API_URL ?? null,
590
- });
591
867
  } else {
592
- _dbg('buildAuthInfo:/copilot_internal/user', { status: r.status, ok: false });
593
868
  }
594
869
  } catch (e) {
595
- _dbg('buildAuthInfo:/copilot_internal/user:error', { err: e.message });
870
+ }
871
+ // Copilot API token 取得(推論 API は OAuth token を受け付けないため交換が必要)
872
+ let copilotToken = null;
873
+ let copilotTokenExpiry = 0;
874
+ try {
875
+ const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
876
+ const t = await globalThis.fetch(`${apiHost}/copilot_internal/v2/token`, {
877
+ method: 'GET',
878
+ headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': process.env.GITHUB_COPILOT_INTEGRATION_ID || 'copilot-developer-cli' },
879
+ signal: AbortSignal.timeout(5000),
880
+ });
881
+ if (t.ok) {
882
+ const td = await t.json();
883
+ copilotToken = td.token || null;
884
+ copilotTokenExpiry = copilotToken ? _normalizeCopilotTokenExpiry(td.expires_at) : 0;
885
+ } else {
886
+ let body = null;
887
+ try { const raw = await t.text(); body = raw.length > 500 ? raw.slice(0, 500) + '…' : raw; } catch (_) {}
888
+ }
889
+ } catch (e) {
596
890
  }
597
891
  return JSON.stringify({
598
892
  authInfo: { type: 'token', host: hostUri, token, login, copilotUser },
599
893
  token,
894
+ copilotToken,
895
+ copilotTokenExpiry,
600
896
  });
601
897
  }
602
898
 
@@ -610,15 +906,15 @@ Module._load = function (request, parent, isMain) {
610
906
  )
611
907
  : null) || 'https://github.com').replace(/\/+$/, '');
612
908
  if (entry.cachedInfo !== null && (entry.cachedToken !== token || entry.cachedHost !== hostUri)) {
613
- _dbg('resolveOrCache:cache-invalidate', {
614
- reason: entry.cachedToken !== token ? 'token-changed' : 'host-changed',
615
- oldTokenHash: _tokenHash(entry.cachedToken), newTokenHash: _tokenHash(token),
616
- });
617
909
  entry.cachedInfo = null;
618
910
  entry.pendingInfo = null;
911
+ entry.copilotToken = null;
912
+ entry.copilotTokenExpiry = 0;
913
+ _modelListCache = null;
914
+ _modelListCacheGen++;
915
+ delete process.env.COPILOT_API_URL;
619
916
  }
620
917
  if (entry.cachedInfo !== null) {
621
- _dbg('resolveOrCache:cache-hit', { tokenHash: _tokenHash(token) });
622
918
  return entry.cachedInfo;
623
919
  }
624
920
  if (!entry.pendingInfo) {
@@ -635,6 +931,10 @@ Module._load = function (request, parent, isMain) {
635
931
  typeof parsed.authInfo.login === 'string' && parsed.authInfo.login.length > 0) {
636
932
  _loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
637
933
  }
934
+ // authManagerSwitchToAuth / authManagerLoginUser と同様に copilotToken を設定
935
+ // _resolveOrCache 経由(TUI 初回起動)でも copilot token が使われるようにする
936
+ entry.copilotToken = (parsed && parsed.copilotToken) || null;
937
+ entry.copilotTokenExpiry = (parsed && parsed.copilotTokenExpiry) || 0;
638
938
  } catch (_) {}
639
939
  return info;
640
940
  }).catch(err => {
@@ -668,18 +968,22 @@ Module._load = function (request, parent, isMain) {
668
968
  result.authManagerGetLastAuthErrors = function(uuid) { return []; };
669
969
  result.authManagerClearCache = function(uuid) {
670
970
  const e = _authMgr.get(uuid);
671
- if (e) { e.gen++; e.cachedInfo = null; e.pendingInfo = null; e.cachedToken = null; e.cachedHost = null; }
971
+ 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; }
672
972
  };
673
973
  result.authManagerSwitchToAuth = async function(uuid, authInfoJson, token) {
674
974
  const entry = _authMgr.get(uuid);
675
975
  if (!entry) return;
676
- // Clear stale enterprise endpoint and cache regardless of token presence
976
+ // Clear stale enterprise endpoint and auth cache regardless of token presence
677
977
  delete process.env.COPILOT_API_URL;
978
+ _modelListCache = null;
979
+ _modelListCacheGen++;
678
980
  const gen = ++entry.gen;
679
981
  entry.cachedInfo = null;
680
982
  entry.cachedToken = null;
681
983
  entry.cachedHost = null;
682
984
  entry.pendingInfo = null;
985
+ entry.copilotToken = null;
986
+ entry.copilotTokenExpiry = 0;
683
987
  if (!token) return;
684
988
  const hostUri = (() => {
685
989
  try { return (JSON.parse(authInfoJson)?.host || 'https://github.com').replace(/\/+$/, ''); }
@@ -691,23 +995,21 @@ Module._load = function (request, parent, isMain) {
691
995
  entry.cachedToken = token;
692
996
  entry.cachedHost = hostUri;
693
997
  entry.pendingInfo = null;
998
+ try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
694
999
  return info;
695
1000
  }).catch(err => {
696
1001
  if (entry.gen === gen) entry.pendingInfo = null;
697
- _dbg('authManagerSwitchToAuth:error', { err: err.message });
698
1002
  });
699
1003
  await entry.pendingInfo;
700
- _dbg('authManagerSwitchToAuth:done', {
701
- tokenHash: _tokenHash(token), hostUri,
702
- COPILOT_API_URL_after: process.env.COPILOT_API_URL ?? null,
703
- });
704
1004
  };
705
1005
  result.authManagerLoginUser = async function(uuid, host, login, token) {
706
- _dbg('authManagerLoginUser:start', { host, login, tokenHash: _tokenHash(token) });
707
1006
  const entry = _authMgr.get(uuid);
708
1007
  if (!entry || !token) return;
709
1008
  const hostUri = (host || 'https://github.com').replace(/\/+$/, '');
710
1009
  _loginTokens.set(`${hostUri}:${login || ''}`, token);
1010
+ delete process.env.COPILOT_API_URL;
1011
+ _modelListCache = null;
1012
+ _modelListCacheGen++;
711
1013
  const gen = ++entry.gen;
712
1014
  entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
713
1015
  if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
@@ -722,13 +1024,14 @@ Module._load = function (request, parent, isMain) {
722
1024
  _loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
723
1025
  }
724
1026
  } catch (_) {}
1027
+ try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
725
1028
  return info;
726
1029
  }).catch(err => { if (entry.gen === gen) entry.pendingInfo = null; return null; });
727
1030
  await entry.pendingInfo; // ensure cachedInfo is set before app.js continues
728
1031
  };
729
1032
  result.authManagerLogout = async function(uuid, authInfoJson) {
730
1033
  const e = _authMgr.get(uuid);
731
- if (e) { e.cachedInfo = null; e.pendingInfo = null; e.cachedToken = null; e.cachedHost = null; }
1034
+ 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; }
732
1035
  return true;
733
1036
  };
734
1037
  result.authManagerRefreshCopilotUser = async function(uuid) {
@@ -736,20 +1039,23 @@ Module._load = function (request, parent, isMain) {
736
1039
  };
737
1040
  result.authManagerDestroy = function(uuid) { _authMgr.delete(uuid); };
738
1041
 
739
- result.authResolveAuthInfoFromToken = async function(token, hostUri, skipCache, userAgent) {
740
- if (!token) return JSON.stringify(null);
741
- let login = null;
742
- try {
743
- const apiHost = (hostUri || 'https://github.com').replace('://github.com', '://api.github.com');
744
- const res = await globalThis.fetch(`${apiHost}/user`, {
745
- headers: { Authorization: `token ${token}`, 'User-Agent': userAgent || `copilot-termux/${_pkgVersion}` },
746
- signal: AbortSignal.timeout(5000),
747
- });
748
- if (res.ok) login = (await res.json()).login;
749
- } catch (_) {}
750
- const host = hostUri || 'https://github.com';
751
- return JSON.stringify({ type: 'token', host, token, login, copilotUser: null });
752
- };
1042
+ if (!isGlibcMode && typeof result.authResolveAuthInfoFromToken === 'function') {
1043
+ result.authResolveAuthInfoFromToken = async function(token, hostUri, skipCache, userAgent) {
1044
+ if (!token) return JSON.stringify(null);
1045
+ let login = null;
1046
+ try {
1047
+ const apiHost = (hostUri || 'https://github.com').replace('://github.com', '://api.github.com');
1048
+ const res = await globalThis.fetch(`${apiHost}/user`, {
1049
+ headers: { Authorization: `token ${token}`, 'User-Agent': userAgent || `copilot-termux/${_pkgVersion}` },
1050
+ signal: AbortSignal.timeout(5000),
1051
+ });
1052
+ if (res.ok) login = (await res.json()).login;
1053
+ } catch (_) {}
1054
+ const host = hostUri || 'https://github.com';
1055
+ return JSON.stringify({ type: 'token', host, token, login, copilotUser: null });
1056
+ };
1057
+ }
1058
+ // glibc mode では native authResolveAuthInfoFromToken を使う(SSL_CERT_FILE で TLS 修正済み)
753
1059
  // === end authManager* stubs ===
754
1060
  // === tokenStore* JS stubs (bionic: tokio ThreadsafeFunction crash) ===
755
1061
  // Verified tokens: set by authManagerLoginUser / _resolveOrCache after /user API check
@@ -784,6 +1090,7 @@ Module._load = function (request, parent, isMain) {
784
1090
  result.tokenStoreStoreCurrentTokenInConfig = async function() {};
785
1091
  // === end tokenStore* stubs ===
786
1092
 
1093
+ if (!isGlibcMode) {
787
1094
  // === urlManager* JS stubs ===
788
1095
  const _urlMgr = new Map();
789
1096
  let _urlSeq = 8e6;
@@ -933,6 +1240,7 @@ Module._load = function (request, parent, isMain) {
933
1240
  return { converged: true, applied: false, updated: false };
934
1241
  };
935
1242
  // === end ifcEngine* stubs ===
1243
+ }
936
1244
  // --- end JS model HTTP implementation ---
937
1245
  }
938
1246
 
@@ -962,23 +1270,18 @@ Module._load = function (request, parent, isMain) {
962
1270
  // 差し替えるのを阻止する。linuxmusl-arm64/runtime.node の Rust ネットワークスタックは
963
1271
  // bionic 上の実 I/O で動作しないため、Node.js ビルトイン fetch(動作確認済み)に固定する。
964
1272
  // GitHub OAuth token via env var or gh CLI (keychain unavailable on bionic)
965
- // --- AUTH デバッグ計装 ---
966
- // COPILOT_TERMUX_DEBUG_AUTH=1 を設定すると認証の各ステップを stderr JSON で出力する。
967
- // token はセキュリティのため先頭8文字のみ表示。
968
- const _authDebug = process.env.COPILOT_TERMUX_DEBUG_AUTH === '1';
969
- function _dbg(event, data) {
970
- if (!_authDebug) return;
971
- process.stderr.write('[copilot-termux-auth] ' + JSON.stringify({ event, ...data, ts: new Date().toISOString() }) + '\n');
1273
+
1274
+ const _COPILOT_TOKEN_DEFAULT_TTL_MS = 28 * 60 * 1000;
1275
+ function _normalizeCopilotTokenExpiry(expiresAt, now = Date.now()) {
1276
+ const parsed = expiresAt ? Date.parse(expiresAt) : NaN;
1277
+ return Number.isFinite(parsed) && parsed > now ? parsed : now + _COPILOT_TOKEN_DEFAULT_TTL_MS;
972
1278
  }
973
- function _tokenHash(t) { return t ? t.slice(0, 8) + '...' : null; }
974
- // --- end 計装 ---
975
1279
 
976
1280
  async function _readGhToken(env) {
977
1281
  const envToken =
978
1282
  (env && (env.GITHUB_TOKEN || env.GH_TOKEN || env.COPILOT_GITHUB_TOKEN)) ||
979
1283
  process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.COPILOT_GITHUB_TOKEN;
980
1284
  if (envToken) {
981
- _dbg('readGhToken', { source: 'env', tokenHash: _tokenHash(envToken) });
982
1285
  return envToken;
983
1286
  }
984
1287
  try {
@@ -988,10 +1291,8 @@ async function _readGhToken(env) {
988
1291
  resolve(err ? null : (stdout.trim() || null));
989
1292
  });
990
1293
  });
991
- _dbg('readGhToken', { source: ghToken ? 'gh-cli' : 'null', tokenHash: _tokenHash(ghToken) });
992
1294
  return ghToken;
993
1295
  } catch (_) {
994
- _dbg('readGhToken', { source: 'error', tokenHash: null });
995
1296
  return null;
996
1297
  }
997
1298
  }
@@ -1003,3 +1304,117 @@ Object.defineProperty(globalThis, 'fetch', {
1003
1304
  get() { return _nativeFetch; },
1004
1305
  set(_) { /* B7 代入を無視 */ },
1005
1306
  });
1307
+
1308
+ // UPDATE-001 / UPDATE-003: 公式 GitHub Copilot CLI (@github/copilot) の app.js に対するパッチ。
1309
+ // (1) `/update` が表示するインストールコマンド文字列を fork のパッケージ名に差し替える (UPDATE-001)
1310
+ // (2) upstream 公式リポジトリ (github/copilot-cli) のリリースチェックに基づく起動時通知バナーを無効化する (UPDATE-003)
1311
+ // app.js は ~/.copilot-termux/<version>/package.json の "type":"module" により ESM としてロードされる。
1312
+ // CJS専用の Module._extensions['.js'] はESMコンパイルに一切関与しないため機能しない
1313
+ // (2026-07-02 実機再現で確認済み)。正しい介入点は node:module の registerHooks()
1314
+ // (Node v22.15.0/v23.5.0+ で追加された同期 ESM Loader Hook)。未対応のNodeではフィーチャー検出で
1315
+ // スキップし、パッチなしでフォールバックする。
1316
+ const { fileURLToPath } = require('url');
1317
+
1318
+ function isTargetCopilotAppJsUrl(url) {
1319
+ if (typeof url !== 'string' || !url.startsWith('file://')) return false;
1320
+ let filename;
1321
+ try {
1322
+ filename = fileURLToPath(url);
1323
+ } catch (_) {
1324
+ return false;
1325
+ }
1326
+ // app.js が `.copilot-termux/<version-or-current>/app.js` に直接あることを要求する
1327
+ // (codex STEP8 指摘: basename + セグメント包含だけだと
1328
+ // `.copilot-termux/<version>/node_modules/**/app.js` のような無関係な深い階層にも
1329
+ // 誤反応しうるため、`.copilot-termux` の直後2セグメント目である場合のみ許可する)
1330
+ if (path.basename(filename) !== 'app.js') return false;
1331
+ const segments = filename.split(/[\\/]/);
1332
+ const idx = segments.indexOf('.copilot-termux');
1333
+ if (idx === -1) return false;
1334
+ return idx + 2 === segments.length - 1;
1335
+ }
1336
+
1337
+ function patchAppJsSource(source) {
1338
+ let patched = source;
1339
+
1340
+ const INSTALL_CMD_PATTERN = /`npm i -g @github\/copilot@\$\{[^}]+\}`/g;
1341
+ const installMatches = patched.match(INSTALL_CMD_PATTERN);
1342
+ if (installMatches && installMatches.length === 1) {
1343
+ patched = patched.replace(INSTALL_CMD_PATTERN, '`npm install -g @bash0816/copilot-termux`');
1344
+ } else {
1345
+ console.warn('[copilot-termux] UPDATE-001: update string pattern ' +
1346
+ (installMatches ? 'found ' + installMatches.length + ' times' : 'not found') + ', skipping patch');
1347
+ }
1348
+
1349
+ 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;
1350
+ const notifyMatches = patched.match(NOTIFY_PATTERN);
1351
+ if (notifyMatches && notifyMatches.length === 1) {
1352
+ patched = patched.replace(NOTIFY_PATTERN, 'false');
1353
+ } else {
1354
+ console.warn('[copilot-termux] UPDATE-003: upstream release notification pattern ' +
1355
+ (notifyMatches ? 'found ' + notifyMatches.length + ' times' : 'not found') + ', skipping patch');
1356
+ }
1357
+
1358
+ // UPDATE-006: DO() 内の releases/latest 取得先をフォーク自身の npm dist-tag latest に差し替える。
1359
+ // 変数名 o,n はDO()固有(a$e()は a,r を使うため誤爆しない)。
1360
+ // changelog本文の取得(nj.execute / fetchReleaseByTag、owner:"github")は一切変更しない。
1361
+ 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;
1362
+ const forkLatestMatches = patched.match(FORK_LATEST_PATTERN);
1363
+ if (forkLatestMatches && forkLatestMatches.length === 1) {
1364
+ patched = patched.replace(FORK_LATEST_PATTERN,
1365
+ 'return await(async()=>{try{' +
1366
+ 'const res=await fetch("https://registry.npmjs.org/%40bash0816%2Fcopilot-termux/latest",{signal:AbortSignal.timeout(5000)});' +
1367
+ 'if(!res.ok)throw new Error("npm registry returned "+res.status);' +
1368
+ 'const data=await res.json();' +
1369
+ 'if(!data||typeof data.version!=="string")throw new Error("npm registry response missing version");' +
1370
+ 'const ver=data.version.replace(/-\\d+$/,"");' +
1371
+ 'if(!/^\\d+\\.\\d+\\.\\d+$/.test(ver))throw new Error("invalid version: "+data.version);' +
1372
+ 'return{tag_name:"v"+ver,assets:[]};' +
1373
+ '}catch(e){return{error:String(e)};}})()'
1374
+ );
1375
+ } else {
1376
+ console.warn('[copilot-termux] UPDATE-006: fork latest pattern ' +
1377
+ (forkLatestMatches ? 'found ' + forkLatestMatches.length + ' times' : 'not found') + ', skipping patch');
1378
+ }
1379
+
1380
+ // UPDATE-006b: 「更新なし」の場合のchangelog表示をcurrent(a)からfork-latest(u)に変更する。
1381
+ // upstreamは「更新なし → nj.execute(t,[a])」(aは現在バージョン)だが、
1382
+ // フォーク自身のnpm latestを判定元にした結果、currentがfork-latestより新しい場合でも
1383
+ // aのchangelogが出続ける問題を解消する。
1384
+ // nj.execute内部(fetchReleaseByTag / owner:"github" / changelog.json)は変更しない。
1385
+ const NO_UPDATE_PATTERN = /if\(!ELt\.default\.gt\(u,a\)\)return nj\.execute\(t,\[a\]\)/g;
1386
+ const noUpdateMatches = patched.match(NO_UPDATE_PATTERN);
1387
+ if (noUpdateMatches && noUpdateMatches.length === 1) {
1388
+ patched = patched.replace(NO_UPDATE_PATTERN,
1389
+ 'if(!ELt.default.gt(u,a))return nj.execute(t,[u.replace(/^v/,"")])'
1390
+ );
1391
+ } else {
1392
+ console.warn('[copilot-termux] UPDATE-006b: no-update changelog pattern ' +
1393
+ (noUpdateMatches ? 'found ' + noUpdateMatches.length + ' times' : 'not found') + ', skipping patch');
1394
+ }
1395
+
1396
+ return patched;
1397
+ }
1398
+
1399
+ if (!globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__) {
1400
+ globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__ = true;
1401
+ const { registerHooks } = require('module');
1402
+ if (typeof registerHooks === 'function') {
1403
+ registerHooks({
1404
+ load(url, context, nextLoad) {
1405
+ const result = nextLoad(url, context);
1406
+ if (!isTargetCopilotAppJsUrl(url)) return result;
1407
+ if (result.source == null) return result;
1408
+ const wasNonString = typeof result.source !== 'string';
1409
+ const src = wasNonString ? Buffer.from(result.source).toString('utf8') : result.source;
1410
+ const patched = patchAppJsSource(src);
1411
+ return Object.assign({}, result, { source: patched });
1412
+ }
1413
+ });
1414
+ } else {
1415
+ console.warn('[copilot-termux] UPDATE-001/003: node:module registerHooks() not available on this Node version (' + process.version + '), skipping app.js patch');
1416
+ }
1417
+ }
1418
+
1419
+ module.exports.patchAppJsSource = patchAppJsSource;
1420
+ module.exports.isTargetCopilotAppJsUrl = isTargetCopilotAppJsUrl;