@bash0816/copilot-termux 1.0.63 → 1.0.65-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/bin/copilot +30 -26
- package/bin/copilot-termux +4 -1
- package/config/copilot-termux-release-manifest.json +15 -0
- package/config/manifest.json +7 -2
- package/lib/bionic-compat.so +0 -0
- package/lib/check-updates.js +183 -0
- package/lib/platform-patch.js +964 -33
- package/lib/setup.js +79 -5
- package/package.json +29 -5
- package/scripts/bionic-compat.c +3 -0
package/lib/platform-patch.js
CHANGED
|
@@ -19,6 +19,10 @@ const Module = require('module');
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
|
|
22
|
+
const _pkgVersion = (() => {
|
|
23
|
+
try { return require(path.join(__dirname, '..', 'package.json')).version; } catch (_) { return '1.0.65'; }
|
|
24
|
+
})();
|
|
25
|
+
|
|
22
26
|
// Bundled Termux-native pty.node (built against bionic, not glibc).
|
|
23
27
|
const NATIVE_PTY = path.join(__dirname, 'native', 'pty.node');
|
|
24
28
|
|
|
@@ -28,11 +32,13 @@ Module._resolveFilename = function (request, parent, isMain, options) {
|
|
|
28
32
|
// Redirect pty.node to our bundled Termux-native build.
|
|
29
33
|
if (path.basename(request) === 'pty.node' &&
|
|
30
34
|
(request.includes('linux-arm64') || request.includes('linuxmusl-arm64'))) {
|
|
31
|
-
|
|
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;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
// Redirect other linux-arm64 addons to linuxmusl-arm64 variants.
|
|
35
|
-
|
|
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) {
|
|
36
42
|
const muslReq = request.replace(/linux-arm64/g, 'linuxmusl-arm64');
|
|
37
43
|
try {
|
|
38
44
|
const resolved = origResolve(muslReq, parent, isMain, options);
|
|
@@ -58,22 +64,176 @@ Module._load = function (request, parent, isMain) {
|
|
|
58
64
|
const result = origLoad(request, parent, isMain);
|
|
59
65
|
if (typeof request === 'string' &&
|
|
60
66
|
path.basename(request) === 'runtime.node') {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
if (result.__copilotTermuxPatched) return result;
|
|
68
|
+
result.__copilotTermuxPatched = true;
|
|
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 = /^(modelHttp|networkFetch|ahpRelay|websocketResponses|sessionStore|sessionSqlite|jsonrpcServer|lspClient)/;
|
|
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;
|
|
69
126
|
}
|
|
70
127
|
}
|
|
71
|
-
|
|
72
|
-
|
|
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) {
|
|
135
|
+
try {
|
|
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;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// authGetCopilotApiUrl: type=token/env/user/gh-cli/api-key では native が null を返す。
|
|
165
|
+
// _b() はこれを見て models=[] を返しモデル選択が "No supported model" になる。
|
|
166
|
+
// OAuth token でも標準 copilot API URL は固定のため、null 時はデフォルト URL を返す。
|
|
167
|
+
if (typeof result.authGetCopilotApiUrl === 'function') {
|
|
168
|
+
const _nativeGetCopilotApiUrl = result.authGetCopilotApiUrl;
|
|
169
|
+
result.authGetCopilotApiUrl = function(authInfoJson, token) {
|
|
170
|
+
const r = _nativeGetCopilotApiUrl(authInfoJson, token);
|
|
171
|
+
if (r != null) return r;
|
|
172
|
+
try {
|
|
173
|
+
const info = JSON.parse(authInfoJson);
|
|
174
|
+
if (info && info.type !== 'hmac') {
|
|
175
|
+
return 'https://api.githubcopilot.com';
|
|
176
|
+
}
|
|
177
|
+
} catch (_) {}
|
|
178
|
+
return r;
|
|
179
|
+
};
|
|
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
|
+
};
|
|
73
217
|
}
|
|
74
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
|
+
}
|
|
75
234
|
if (typeof result.capiClientListModels === 'function') {
|
|
76
235
|
result.capiClientListModels = async function(handle, _includeHidden, _skipCache, _applyModelLimitCaps, _networkingConfigId) {
|
|
236
|
+
const snapshotGen = _modelListCacheGen; // 開始時の世代をキャプチャ → 完了時に照合して stale 結果を破棄
|
|
77
237
|
let authHeaders;
|
|
78
238
|
try {
|
|
79
239
|
const prepared = result.capiClientPrepareRequestHeaders(handle, '', []);
|
|
@@ -84,10 +244,14 @@ Module._load = function (request, parent, isMain) {
|
|
|
84
244
|
} catch (e) {
|
|
85
245
|
throw new Error(JSON.stringify({kind: 'network', message: `prepareHeaders failed: ${e.message}`}));
|
|
86
246
|
}
|
|
87
|
-
|
|
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;
|
|
88
252
|
let res;
|
|
89
253
|
try {
|
|
90
|
-
res = await globalThis.fetch(`${
|
|
254
|
+
res = await globalThis.fetch(`${fetchUrl}/models`, {method: 'GET', headers: authHeaders});
|
|
91
255
|
} catch (e) {
|
|
92
256
|
throw new Error(JSON.stringify({kind: 'network', message: e.message}));
|
|
93
257
|
}
|
|
@@ -97,23 +261,16 @@ Module._load = function (request, parent, isMain) {
|
|
|
97
261
|
throw new Error(JSON.stringify({kind: 'http', status: res.status, statusText: res.statusText, body, headers: hdrs, hasRequestId: res.headers.has('x-request-id')}));
|
|
98
262
|
}
|
|
99
263
|
const data = await res.json();
|
|
100
|
-
const
|
|
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
|
+
}
|
|
101
270
|
const rateHeaders = [...res.headers.entries()].map(([name, value]) => ({name, value}));
|
|
102
|
-
return {modelsJson: JSON.stringify(models), copilotUrl
|
|
271
|
+
return {modelsJson: JSON.stringify(models), copilotUrl, usageRatelimitHeaders: rateHeaders, capturedAssignmentContext: undefined};
|
|
103
272
|
};
|
|
104
273
|
}
|
|
105
|
-
if (typeof result.sessionSqliteOpen === 'function') {
|
|
106
|
-
result.sessionSqliteOpen = () => 1;
|
|
107
|
-
}
|
|
108
|
-
if (typeof result.sessionSqliteQuery === 'function') {
|
|
109
|
-
result.sessionSqliteQuery = () => ({ rows: '[]' });
|
|
110
|
-
}
|
|
111
|
-
if (typeof result.sessionSqliteRun === 'function') {
|
|
112
|
-
result.sessionSqliteRun = () => ({ rowsAffected: 0, lastInsertRowid: null });
|
|
113
|
-
}
|
|
114
|
-
if (typeof result.sessionSqliteFileExists === 'function') {
|
|
115
|
-
result.sessionSqliteFileExists = () => false;
|
|
116
|
-
}
|
|
117
274
|
// --- JS networkFetch* implementation (bionic: tokio networkFetch* are no-op'd) ---
|
|
118
275
|
// B7 が QXe() から直接呼ばれる MCP 専用パスでクラッシュするため JS で代替。
|
|
119
276
|
// networkFetchStreamStart → { requestId, response: Promise<{handle,url,status,statusText,headers}> }
|
|
@@ -129,6 +286,24 @@ Module._load = function (request, parent, isMain) {
|
|
|
129
286
|
const entry = { abort: () => abortCtrl.abort(), reader: null };
|
|
130
287
|
_nfMap.set(requestId, entry);
|
|
131
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
|
+
|
|
132
307
|
const headers = {};
|
|
133
308
|
if (Array.isArray(req.headers)) {
|
|
134
309
|
for (const { name, value } of req.headers) headers[name] = value;
|
|
@@ -142,7 +317,7 @@ Module._load = function (request, parent, isMain) {
|
|
|
142
317
|
res = await globalThis.fetch(req.url, {
|
|
143
318
|
method: req.method || 'GET',
|
|
144
319
|
headers,
|
|
145
|
-
body:
|
|
320
|
+
body: body,
|
|
146
321
|
signal: abortCtrl.signal,
|
|
147
322
|
redirect: req.redirect || 'follow',
|
|
148
323
|
});
|
|
@@ -338,11 +513,44 @@ Module._load = function (request, parent, isMain) {
|
|
|
338
513
|
body = JSON.stringify(body);
|
|
339
514
|
}
|
|
340
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
|
+
}
|
|
341
549
|
let res;
|
|
342
550
|
try {
|
|
343
551
|
res = await globalThis.fetch(req.url, {
|
|
344
552
|
method: req.method || 'POST',
|
|
345
|
-
headers:
|
|
553
|
+
headers: fetchHeaders,
|
|
346
554
|
body: body,
|
|
347
555
|
});
|
|
348
556
|
} catch(e) {
|
|
@@ -360,7 +568,34 @@ Module._load = function (request, parent, isMain) {
|
|
|
360
568
|
_jsStreams.set(streamId, { events, index: 0, finalMessage });
|
|
361
569
|
return { json: JSON.stringify({ bodyText: null, status: res.status, statusText: res.statusText, headers, streamId }) };
|
|
362
570
|
};
|
|
363
|
-
|
|
571
|
+
// Helper: convert Anthropic SSE event → chunkContext for processAnthropicStreamingChunkContext
|
|
572
|
+
function _toChunkContext(event, streamId) {
|
|
573
|
+
const base = { content: '', size: 0, chunkBoundary: false, messageStart: false, streamingId: streamId };
|
|
574
|
+
if (!event || !event.type) return base;
|
|
575
|
+
switch (event.type) {
|
|
576
|
+
case 'message_start':
|
|
577
|
+
return { ...base, messageStart: true };
|
|
578
|
+
case 'content_block_delta': {
|
|
579
|
+
const d = event.delta;
|
|
580
|
+
if (!d) return base;
|
|
581
|
+
if (d.type === 'text_delta') {
|
|
582
|
+
const t = d.text || '';
|
|
583
|
+
return { ...base, content: t, size: Buffer.byteLength(t, 'utf8') };
|
|
584
|
+
}
|
|
585
|
+
if (d.type === 'thinking_delta') {
|
|
586
|
+
const r = d.thinking || '';
|
|
587
|
+
return { ...base, reasoningContent: r, size: Buffer.byteLength(r, 'utf8') };
|
|
588
|
+
}
|
|
589
|
+
return base;
|
|
590
|
+
}
|
|
591
|
+
case 'content_block_stop':
|
|
592
|
+
case 'message_delta':
|
|
593
|
+
case 'message_stop':
|
|
594
|
+
return { ...base, chunkBoundary: true };
|
|
595
|
+
default:
|
|
596
|
+
return base;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
364
599
|
// 5. modelHttpStreamNextAnthropicMessageEvent → yield events one by one
|
|
365
600
|
result.modelHttpStreamNextAnthropicMessageEvent = async function(streamId, accId) {
|
|
366
601
|
const st = _jsStreams.get(streamId);
|
|
@@ -372,7 +607,10 @@ Module._load = function (request, parent, isMain) {
|
|
|
372
607
|
}
|
|
373
608
|
return null;
|
|
374
609
|
}
|
|
375
|
-
|
|
610
|
+
const event = st.events[st.index++];
|
|
611
|
+
const chunkContext = _toChunkContext(event, streamId);
|
|
612
|
+
const tokenEvent = event.type === 'content_block_delta' && !!(event.delta && event.delta.type === 'text_delta');
|
|
613
|
+
return { json: JSON.stringify({ kind: 'ok', processResult: { event, chunkContext, tokenEvent, copilotUsage: null } }) };
|
|
376
614
|
};
|
|
377
615
|
|
|
378
616
|
// 6. modelHttpStreamCancel → cleanup
|
|
@@ -391,11 +629,44 @@ Module._load = function (request, parent, isMain) {
|
|
|
391
629
|
body = JSON.stringify(body);
|
|
392
630
|
}
|
|
393
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
|
+
}
|
|
394
665
|
let res;
|
|
395
666
|
try {
|
|
396
667
|
res = await globalThis.fetch(req.url, {
|
|
397
668
|
method: req.method || 'POST',
|
|
398
|
-
headers:
|
|
669
|
+
headers: fetchHeaders,
|
|
399
670
|
body: body,
|
|
400
671
|
});
|
|
401
672
|
} catch(e) {
|
|
@@ -434,14 +705,598 @@ Module._load = function (request, parent, isMain) {
|
|
|
434
705
|
}
|
|
435
706
|
return { json: JSON.stringify({ kind: 'ok', copilotUsage, ttftMs: null, interTokenLatencyMs: null }) };
|
|
436
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
|
+
};
|
|
830
|
+
// === authManager* JS stubs (1.0.64: tokio thread spawn → SIGSEGV on bionic) ===
|
|
831
|
+
const _authMgr = new Map(); // uuid → { cachedInfo, pendingInfo, cachedToken, cachedHost, gen }
|
|
832
|
+
let _modelListCache = null; // capiClientListModels が取得したモデルリストキャッシュ(modelResolver fallback 用)
|
|
833
|
+
let _modelListCacheGen = 0; // アカウント切替時にインクリメント → 古い /models 結果の上書きを防ぐ
|
|
834
|
+
|
|
835
|
+
result.authManagerCreate = function(uuid, hostUri, userAgent, path, normSpec, header, envVar, disableAutoLogin) {
|
|
836
|
+
_authMgr.set(uuid, { cachedInfo: null, pendingInfo: null, cachedToken: null, cachedHost: null, gen: 0, copilotToken: null, copilotTokenExpiry: 0 });
|
|
837
|
+
// native 非呼び出し: tokio runtime 生成を阻止
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
async function _buildAuthInfo(token, hostUri) {
|
|
841
|
+
let login = null;
|
|
842
|
+
let copilotUser = null;
|
|
843
|
+
try {
|
|
844
|
+
const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
|
|
845
|
+
const res = await globalThis.fetch(`${apiHost}/user`, {
|
|
846
|
+
headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}` },
|
|
847
|
+
signal: AbortSignal.timeout(5000),
|
|
848
|
+
});
|
|
849
|
+
if (res.ok) login = (await res.json()).login;
|
|
850
|
+
} catch (e) {
|
|
851
|
+
}
|
|
852
|
+
// copilot_internal/user から copilotUser 全体を取得して authInfo に含める。
|
|
853
|
+
// app.js の Wa(authInfo) は authInfo.copilotUser.endpoints.api から CAPI base URL を導く。
|
|
854
|
+
// copilotUser: null のままでは is_mcp_enabled・quota・plan 情報も失われる。
|
|
855
|
+
// env var は後続の capiClientListModels stub / authGetCopilotApiUrl stub でも参照するため並記する。
|
|
856
|
+
try {
|
|
857
|
+
const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
|
|
858
|
+
const r = await globalThis.fetch(`${apiHost}/copilot_internal/user`, {
|
|
859
|
+
headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': process.env.GITHUB_COPILOT_INTEGRATION_ID || 'copilot-developer-cli' },
|
|
860
|
+
signal: AbortSignal.timeout(5000),
|
|
861
|
+
});
|
|
862
|
+
if (r.ok) {
|
|
863
|
+
const info = await r.json();
|
|
864
|
+
copilotUser = info;
|
|
865
|
+
const apiUrl = info?.endpoints?.api;
|
|
866
|
+
if (apiUrl && typeof apiUrl === 'string') process.env.COPILOT_API_URL = apiUrl;
|
|
867
|
+
} else {
|
|
868
|
+
}
|
|
869
|
+
} catch (e) {
|
|
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) {
|
|
890
|
+
}
|
|
891
|
+
return JSON.stringify({
|
|
892
|
+
authInfo: { type: 'token', host: hostUri, token, login, copilotUser },
|
|
893
|
+
token,
|
|
894
|
+
copilotToken,
|
|
895
|
+
copilotTokenExpiry,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function _resolveOrCache(uuid, token, env) {
|
|
900
|
+
const entry = _authMgr.get(uuid);
|
|
901
|
+
if (!entry) return null;
|
|
902
|
+
const hostUri = ((typeof result.githubGetUri === 'function'
|
|
903
|
+
? result.githubGetUri(
|
|
904
|
+
(env && env.COPILOT_GH_HOST) || undefined,
|
|
905
|
+
(env && env.GH_HOST) || undefined
|
|
906
|
+
)
|
|
907
|
+
: null) || 'https://github.com').replace(/\/+$/, '');
|
|
908
|
+
if (entry.cachedInfo !== null && (entry.cachedToken !== token || entry.cachedHost !== hostUri)) {
|
|
909
|
+
entry.cachedInfo = null;
|
|
910
|
+
entry.pendingInfo = null;
|
|
911
|
+
entry.copilotToken = null;
|
|
912
|
+
entry.copilotTokenExpiry = 0;
|
|
913
|
+
_modelListCache = null;
|
|
914
|
+
_modelListCacheGen++;
|
|
915
|
+
delete process.env.COPILOT_API_URL;
|
|
916
|
+
}
|
|
917
|
+
if (entry.cachedInfo !== null) {
|
|
918
|
+
return entry.cachedInfo;
|
|
919
|
+
}
|
|
920
|
+
if (!entry.pendingInfo) {
|
|
921
|
+
const gen = entry.gen;
|
|
922
|
+
entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
|
|
923
|
+
if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
|
|
924
|
+
entry.cachedInfo = info;
|
|
925
|
+
entry.cachedToken = token;
|
|
926
|
+
entry.cachedHost = hostUri;
|
|
927
|
+
entry.pendingInfo = null;
|
|
928
|
+
try {
|
|
929
|
+
const parsed = JSON.parse(info);
|
|
930
|
+
if (parsed && parsed.authInfo &&
|
|
931
|
+
typeof parsed.authInfo.login === 'string' && parsed.authInfo.login.length > 0) {
|
|
932
|
+
_loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
|
|
933
|
+
}
|
|
934
|
+
// authManagerSwitchToAuth / authManagerLoginUser と同様に copilotToken を設定
|
|
935
|
+
// _resolveOrCache 経由(TUI 初回起動)でも copilot token が使われるようにする
|
|
936
|
+
entry.copilotToken = (parsed && parsed.copilotToken) || null;
|
|
937
|
+
entry.copilotTokenExpiry = (parsed && parsed.copilotTokenExpiry) || 0;
|
|
938
|
+
} catch (_) {}
|
|
939
|
+
return info;
|
|
940
|
+
}).catch(err => {
|
|
941
|
+
if (entry.gen === gen) entry.pendingInfo = null;
|
|
942
|
+
throw err;
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
return entry.pendingInfo;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
result.authManagerLoadAuthInfo = async function(uuid, env, _storeTokenPlaintext) {
|
|
949
|
+
const token = await _readGhToken(env);
|
|
950
|
+
if (!token) return null;
|
|
951
|
+
return _resolveOrCache(uuid, token, env);
|
|
952
|
+
};
|
|
953
|
+
result.authManagerGetCurrentAuthInfo = async function(uuid, env, _storeTokenPlaintext) {
|
|
954
|
+
const entry = _authMgr.get(uuid);
|
|
955
|
+
if (!entry) return null;
|
|
956
|
+
if (entry.cachedInfo !== null) return entry.cachedInfo;
|
|
957
|
+
if (entry.pendingInfo) return entry.pendingInfo;
|
|
958
|
+
const token = await _readGhToken(env);
|
|
959
|
+
if (!token) return null;
|
|
960
|
+
return _resolveOrCache(uuid, token, env);
|
|
961
|
+
};
|
|
962
|
+
result.authManagerGetAllAuthAvailable = async function(uuid, env, _storeTokenPlaintext) {
|
|
963
|
+
const token = await _readGhToken(env);
|
|
964
|
+
if (!token) return [];
|
|
965
|
+
const info = await _resolveOrCache(uuid, token, env);
|
|
966
|
+
return info ? [info] : [];
|
|
967
|
+
};
|
|
968
|
+
result.authManagerGetLastAuthErrors = function(uuid) { return []; };
|
|
969
|
+
result.authManagerClearCache = function(uuid) {
|
|
970
|
+
const e = _authMgr.get(uuid);
|
|
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; }
|
|
972
|
+
};
|
|
973
|
+
result.authManagerSwitchToAuth = async function(uuid, authInfoJson, token) {
|
|
974
|
+
const entry = _authMgr.get(uuid);
|
|
975
|
+
if (!entry) return;
|
|
976
|
+
// Clear stale enterprise endpoint and auth cache regardless of token presence
|
|
977
|
+
delete process.env.COPILOT_API_URL;
|
|
978
|
+
_modelListCache = null;
|
|
979
|
+
_modelListCacheGen++;
|
|
980
|
+
const gen = ++entry.gen;
|
|
981
|
+
entry.cachedInfo = null;
|
|
982
|
+
entry.cachedToken = null;
|
|
983
|
+
entry.cachedHost = null;
|
|
984
|
+
entry.pendingInfo = null;
|
|
985
|
+
entry.copilotToken = null;
|
|
986
|
+
entry.copilotTokenExpiry = 0;
|
|
987
|
+
if (!token) return;
|
|
988
|
+
const hostUri = (() => {
|
|
989
|
+
try { return (JSON.parse(authInfoJson)?.host || 'https://github.com').replace(/\/+$/, ''); }
|
|
990
|
+
catch (_) { return 'https://github.com'; }
|
|
991
|
+
})();
|
|
992
|
+
entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
|
|
993
|
+
if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
|
|
994
|
+
entry.cachedInfo = info;
|
|
995
|
+
entry.cachedToken = token;
|
|
996
|
+
entry.cachedHost = hostUri;
|
|
997
|
+
entry.pendingInfo = null;
|
|
998
|
+
try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
|
|
999
|
+
return info;
|
|
1000
|
+
}).catch(err => {
|
|
1001
|
+
if (entry.gen === gen) entry.pendingInfo = null;
|
|
1002
|
+
});
|
|
1003
|
+
await entry.pendingInfo;
|
|
1004
|
+
};
|
|
1005
|
+
result.authManagerLoginUser = async function(uuid, host, login, token) {
|
|
1006
|
+
const entry = _authMgr.get(uuid);
|
|
1007
|
+
if (!entry || !token) return;
|
|
1008
|
+
const hostUri = (host || 'https://github.com').replace(/\/+$/, '');
|
|
1009
|
+
_loginTokens.set(`${hostUri}:${login || ''}`, token);
|
|
1010
|
+
delete process.env.COPILOT_API_URL;
|
|
1011
|
+
_modelListCache = null;
|
|
1012
|
+
_modelListCacheGen++;
|
|
1013
|
+
const gen = ++entry.gen;
|
|
1014
|
+
entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
|
|
1015
|
+
if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
|
|
1016
|
+
entry.cachedInfo = info;
|
|
1017
|
+
entry.cachedToken = token;
|
|
1018
|
+
entry.cachedHost = hostUri;
|
|
1019
|
+
entry.pendingInfo = null;
|
|
1020
|
+
try {
|
|
1021
|
+
const parsed = JSON.parse(info);
|
|
1022
|
+
if (parsed && parsed.authInfo &&
|
|
1023
|
+
typeof parsed.authInfo.login === 'string' && parsed.authInfo.login.length > 0) {
|
|
1024
|
+
_loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
|
|
1025
|
+
}
|
|
1026
|
+
} catch (_) {}
|
|
1027
|
+
try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
|
|
1028
|
+
return info;
|
|
1029
|
+
}).catch(err => { if (entry.gen === gen) entry.pendingInfo = null; return null; });
|
|
1030
|
+
await entry.pendingInfo; // ensure cachedInfo is set before app.js continues
|
|
1031
|
+
};
|
|
1032
|
+
result.authManagerLogout = async function(uuid, authInfoJson) {
|
|
1033
|
+
const e = _authMgr.get(uuid);
|
|
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; }
|
|
1035
|
+
return true;
|
|
1036
|
+
};
|
|
1037
|
+
result.authManagerRefreshCopilotUser = async function(uuid) {
|
|
1038
|
+
return null; // null → JS側 authInfoWithTokenPromise フォールバック
|
|
1039
|
+
};
|
|
1040
|
+
result.authManagerDestroy = function(uuid) { _authMgr.delete(uuid); };
|
|
1041
|
+
|
|
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 修正済み)
|
|
1059
|
+
// === end authManager* stubs ===
|
|
1060
|
+
// === tokenStore* JS stubs (bionic: tokio ThreadsafeFunction crash) ===
|
|
1061
|
+
// Verified tokens: set by authManagerLoginUser / _resolveOrCache after /user API check
|
|
1062
|
+
const _loginTokens = new Map(); // "host:login" → oauthToken
|
|
1063
|
+
|
|
1064
|
+
const _tokStore = new Map();
|
|
1065
|
+
let _tokSeq = 9e6;
|
|
1066
|
+
result.tokenStoreCreate = function() { const id = ++_tokSeq; _tokStore.set(id, new Map()); return id; };
|
|
1067
|
+
result.tokenStoreDestroy = function(h) { _tokStore.delete(h); };
|
|
1068
|
+
result.tokenStoreGetToken = async function(h, host, login) {
|
|
1069
|
+
const m = _tokStore.get(h);
|
|
1070
|
+
const key = `${(host || 'https://github.com').replace(/\/+$/, '')}:${login || ''}`;
|
|
1071
|
+
if (m) { const v = m.get(key); if (v != null) return v; }
|
|
1072
|
+
const lt = _loginTokens.get(key);
|
|
1073
|
+
if (lt != null) { if (m) m.set(key, lt); return lt; }
|
|
1074
|
+
if (login) return null; // login specified: refuse unverified fallback
|
|
1075
|
+
return _readGhToken(null);
|
|
1076
|
+
};
|
|
1077
|
+
result.tokenStoreStoreToken = function(h, token, host, login) {
|
|
1078
|
+
const m = _tokStore.get(h);
|
|
1079
|
+
if (m && token) m.set(`${(host || 'https://github.com').replace(/\/+$/, '')}:${login || ''}`, token);
|
|
1080
|
+
};
|
|
1081
|
+
result.tokenStoreRemoveToken = function(h, host, login) {
|
|
1082
|
+
const m = _tokStore.get(h);
|
|
1083
|
+
if (m) m.delete(`${(host || 'https://github.com').replace(/\/+$/, '')}:${login || ''}`);
|
|
1084
|
+
};
|
|
1085
|
+
result.tokenStoreGetAnyToken = async function(h) {
|
|
1086
|
+
const m = _tokStore.get(h);
|
|
1087
|
+
if (m && m.size > 0) return [...m.values()][0];
|
|
1088
|
+
return _readGhToken(null);
|
|
1089
|
+
};
|
|
1090
|
+
result.tokenStoreStoreCurrentTokenInConfig = async function() {};
|
|
1091
|
+
// === end tokenStore* stubs ===
|
|
1092
|
+
|
|
1093
|
+
if (!isGlibcMode) {
|
|
1094
|
+
// === urlManager* JS stubs ===
|
|
1095
|
+
const _urlMgr = new Map();
|
|
1096
|
+
let _urlSeq = 8e6;
|
|
1097
|
+
result.urlManagerCreate = function(urls, unrestricted) {
|
|
1098
|
+
const id = ++_urlSeq;
|
|
1099
|
+
_urlMgr.set(id, { urls: Array.isArray(urls) ? [...urls] : [], unrestricted: !!unrestricted });
|
|
1100
|
+
return id;
|
|
1101
|
+
};
|
|
1102
|
+
result.urlManagerAddUrl = function(h, url) { const e = _urlMgr.get(h); if (e) e.urls.push(url); };
|
|
1103
|
+
result.urlManagerDispose = function(h) { _urlMgr.delete(h); };
|
|
1104
|
+
result.urlManagerGetUrls = function(h) { return (_urlMgr.get(h) || {}).urls || []; };
|
|
1105
|
+
result.urlManagerIsUrlAllowed = function(h, url) { return true; };
|
|
1106
|
+
result.urlManagerIsUnrestrictedMode = function(h) { return true; };
|
|
1107
|
+
result.urlManagerSetUnrestrictedMode = function(h, v) { const e = _urlMgr.get(h); if (e) e.unrestricted = v; };
|
|
1108
|
+
// === end urlManager* stubs ===
|
|
1109
|
+
|
|
1110
|
+
// === pathManager* JS stubs ===
|
|
1111
|
+
const _pathMgr = new Map();
|
|
1112
|
+
let _pathSeq = 7e6;
|
|
1113
|
+
result.pathManagerCreateRestricted = async function(dirs, primary) {
|
|
1114
|
+
const id = ++_pathSeq;
|
|
1115
|
+
_pathMgr.set(id, { dirs: Array.isArray(dirs) ? [...dirs] : [], primary: primary || null });
|
|
1116
|
+
return id;
|
|
1117
|
+
};
|
|
1118
|
+
result.pathManagerCreateUnrestricted = async function(primary) {
|
|
1119
|
+
const id = ++_pathSeq;
|
|
1120
|
+
_pathMgr.set(id, { dirs: [], primary: primary || null });
|
|
1121
|
+
return id;
|
|
1122
|
+
};
|
|
1123
|
+
result.pathManagerDispose = function(h) { _pathMgr.delete(h); };
|
|
1124
|
+
result.pathManagerAddDirectory = function(h, dir) { const e = _pathMgr.get(h); if (e) e.dirs.push(dir); };
|
|
1125
|
+
result.pathManagerGetDirectories = function(h) { return (_pathMgr.get(h) || {}).dirs || []; };
|
|
1126
|
+
result.pathManagerGetPrimaryDirectory = function(h) { return (_pathMgr.get(h) || {}).primary || null; };
|
|
1127
|
+
result.pathManagerIsPathWithinWorkspace = function(h, p) {
|
|
1128
|
+
const e = _pathMgr.get(h);
|
|
1129
|
+
if (!e || !e.primary) return true;
|
|
1130
|
+
return p === e.primary || p.startsWith(e.primary + '/');
|
|
1131
|
+
};
|
|
1132
|
+
result.pathManagerUpdatePrimaryDirectory = function(h, dir) { const e = _pathMgr.get(h); if (e) e.primary = dir; };
|
|
1133
|
+
result.pathManagerIsPathWithinAllowedDirectories = function(h, p) {
|
|
1134
|
+
const e = _pathMgr.get(h);
|
|
1135
|
+
if (!e || e.dirs.length === 0) return true;
|
|
1136
|
+
return e.dirs.some(dir => p === dir || p.startsWith(dir + '/'));
|
|
1137
|
+
};
|
|
1138
|
+
// === end pathManager* stubs ===
|
|
1139
|
+
|
|
1140
|
+
// === telemetryQueue* JS stubs ===
|
|
1141
|
+
let _telSeq = 6e6;
|
|
1142
|
+
result.telemetryQueueCreate = function(name, _config) { return ++_telSeq; };
|
|
1143
|
+
result.telemetryQueueDispose = function(h) {};
|
|
1144
|
+
result.telemetryQueueEnqueue = function(h, event) {};
|
|
1145
|
+
result.telemetryQueueSetDebugLogPayload = function(h, v) {};
|
|
1146
|
+
// === end telemetryQueue* stubs ===
|
|
1147
|
+
|
|
1148
|
+
// === telemetryAppInsightsServiceState* JS stubs (1.0.65+: Azure AppInsights HTTP I/O → tokio SIGSEGV on bionic) ===
|
|
1149
|
+
if (typeof result.telemetryAppInsightsServiceStateCreate === 'function') {
|
|
1150
|
+
result.telemetryAppInsightsServiceStateCreate = function() { return ++_telSeq; };
|
|
1151
|
+
result.telemetryAppInsightsServiceStateDispose = function(h) {};
|
|
1152
|
+
result.telemetryAppInsightsServiceStateEnqueue = function(h, e, n, r) { return JSON.stringify({}); };
|
|
1153
|
+
result.telemetryAppInsightsServiceStateAuthSucceeded = function(h, e) { return JSON.stringify({}); };
|
|
1154
|
+
result.telemetryAppInsightsServiceStateLogout = function(h) { return JSON.stringify({}); };
|
|
1155
|
+
}
|
|
1156
|
+
// === end telemetryAppInsightsServiceState* stubs ===
|
|
1157
|
+
|
|
1158
|
+
// === telemetryDelegatingSender* JS stubs (1.0.65+: fire-and-forget HTTP send → tokio SIGSEGV on bionic) ===
|
|
1159
|
+
if (typeof result.telemetryDelegatingSenderCreate === 'function') {
|
|
1160
|
+
result.telemetryDelegatingSenderCreate = function() { return ++_telSeq; };
|
|
1161
|
+
result.telemetryDelegatingSenderDispose = function(h) { return JSON.stringify({ disposeDelegate: false }); };
|
|
1162
|
+
result.telemetryDelegatingSenderConfigure = function(h, hasDelegate) {
|
|
1163
|
+
return JSON.stringify({ disposePreviousDelegate: false, disposeNewDelegate: false });
|
|
1164
|
+
};
|
|
1165
|
+
result.telemetryDelegatingSenderIsConfigured = function(h) { return false; };
|
|
1166
|
+
result.telemetryDelegatingSenderRequiresDelegate = function(h) {};
|
|
1167
|
+
result.telemetryDelegatingSenderSetInternalCorrelationIds = function(h, ids) {
|
|
1168
|
+
return JSON.stringify({ applyToDelegate: false });
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
// === end telemetryDelegatingSender* stubs ===
|
|
1172
|
+
|
|
1173
|
+
// === telemetrySessionTelemetryState* JS stubs (1.0.65+) ===
|
|
1174
|
+
if (typeof result.telemetrySessionTelemetryStateCreate === 'function') {
|
|
1175
|
+
result.telemetrySessionTelemetryStateCreate = function(h, e) { return ++_telSeq; };
|
|
1176
|
+
result.telemetrySessionTelemetryStateDispose = function(h) {};
|
|
1177
|
+
result.telemetrySessionTelemetryStateSnapshot = function(h) { return JSON.stringify({ telemetryEvents: [] }); };
|
|
1178
|
+
result.telemetrySessionTelemetryStateProcessSessionEvent = function(h, e, n) {
|
|
1179
|
+
return JSON.stringify({ telemetryEvents: [] });
|
|
1180
|
+
};
|
|
1181
|
+
result.telemetrySessionTelemetryStateProcessToolsUpdated = function(h, e) {};
|
|
1182
|
+
}
|
|
1183
|
+
// === end telemetrySessionTelemetryState* stubs ===
|
|
1184
|
+
|
|
1185
|
+
// === telemetryLegacyUsageHandler* JS stubs (1.0.65+) ===
|
|
1186
|
+
if (typeof result.telemetryLegacyUsageHandlerCreate === 'function') {
|
|
1187
|
+
result.telemetryLegacyUsageHandlerCreate = function(h, e) { return ++_telSeq; };
|
|
1188
|
+
result.telemetryLegacyUsageHandlerDispose = function(h) {};
|
|
1189
|
+
result.telemetryLegacyUsageHandlerProcessEvent = function(h, e) { return JSON.stringify([]); };
|
|
1190
|
+
}
|
|
1191
|
+
// === end telemetryLegacyUsageHandler* stubs ===
|
|
1192
|
+
|
|
1193
|
+
// === permissionService* JS stubs ===
|
|
1194
|
+
const _permSvc = new Map();
|
|
1195
|
+
let _permSeq = 5e6;
|
|
1196
|
+
result.permissionServiceCreate = function(config) {
|
|
1197
|
+
const id = ++_permSeq;
|
|
1198
|
+
_permSvc.set(id, { approveAll: !!(config && config.approveAllTool) });
|
|
1199
|
+
return id;
|
|
1200
|
+
};
|
|
1201
|
+
result.permissionServiceDispose = function(h) { _permSvc.delete(h); };
|
|
1202
|
+
result.permissionServiceRequest = async function(h, reqJson) {
|
|
1203
|
+
return JSON.stringify({ kind: 'approved' });
|
|
1204
|
+
};
|
|
1205
|
+
result.permissionServiceComplete = function(h, token, ok, resultJson) {};
|
|
1206
|
+
result.permissionServiceConfigure = function(h, approveAllTool, approveAllRead, approvedRules, deniedRules, pathMgr, urlMgr) {};
|
|
1207
|
+
result.permissionServiceAddApprovedRules = function(h, rules) {};
|
|
1208
|
+
result.permissionServiceGetApproveAllTool = function(h) { return (_permSvc.get(h) || {}).approveAll ?? false; };
|
|
1209
|
+
result.permissionServiceSetApproveAllTool = function(h, v) { const e = _permSvc.get(h); if (e) e.approveAll = v; };
|
|
1210
|
+
result.permissionServiceRemoveApprovedRules = function(h, rules) {};
|
|
1211
|
+
result.permissionServiceCheckSamplingApproval = function(h, k) { return true; };
|
|
1212
|
+
result.permissionServiceResetSessionApprovals = function(h) {};
|
|
1213
|
+
result.permissionServiceAddLocationApprovedRules = function(h, rules) {};
|
|
1214
|
+
result.permissionServiceRemoveLocationApprovedRules = function(h, rules) {};
|
|
1215
|
+
// === end permissionService* stubs ===
|
|
1216
|
+
|
|
1217
|
+
// === lspManager* JS stubs ===
|
|
1218
|
+
let _lspMgrSeq = 4e6;
|
|
1219
|
+
result.lspManagerCreate = function() { return ++_lspMgrSeq; };
|
|
1220
|
+
result.lspManagerClear = function(h) {};
|
|
1221
|
+
result.lspManagerPlanForFile = function(h, file, lang, config) { return null; };
|
|
1222
|
+
result.lspManagerRemoveClient = function(h, key) {};
|
|
1223
|
+
result.lspManagerShutdownKeys = function(h) { return []; };
|
|
1224
|
+
result.lspManagerPlanForServerId = function(h, id) { return null; };
|
|
1225
|
+
result.lspManagerCachedClientCount = function(h) { return 0; };
|
|
1226
|
+
result.lspManagerRelevantServerIds = function(h, file, lang) { return []; };
|
|
1227
|
+
// === end lspManager* stubs ===
|
|
1228
|
+
|
|
1229
|
+
// === ifcEngine* JS stubs ===
|
|
1230
|
+
const _ifc = new Map();
|
|
1231
|
+
let _ifcSeq = 3e6;
|
|
1232
|
+
result.ifcEngineCreate = function(config) { const id = ++_ifcSeq; _ifc.set(id, {}); return id; };
|
|
1233
|
+
result.ifcEngineDispose = function(h) { _ifc.delete(h); };
|
|
1234
|
+
result.ifcEngineToJson = function(h) { return '{}'; };
|
|
1235
|
+
result.ifcEnginePreToolHook = async function(h, tool, argsJson, fetchHandler) { return { converged: true }; };
|
|
1236
|
+
result.ifcEngineFetchComplete = function(h, resp) {};
|
|
1237
|
+
result.ifcEngineGetContextLabel = function(h) { return null; };
|
|
1238
|
+
result.ifcEngineSetContextLabel = function(h, label) {};
|
|
1239
|
+
result.ifcEnginePostToolExecution = async function(h, tool, argsJson, fetchHandler) {
|
|
1240
|
+
return { converged: true, applied: false, updated: false };
|
|
1241
|
+
};
|
|
1242
|
+
// === end ifcEngine* stubs ===
|
|
1243
|
+
}
|
|
437
1244
|
// --- end JS model HTTP implementation ---
|
|
438
1245
|
}
|
|
1246
|
+
|
|
1247
|
+
// === cli-native.node stubs (1.0.64+: color scheme fns use Rust tokio → SIGSEGV on bionic) ===
|
|
1248
|
+
if (typeof request === 'string' && path.basename(request) === 'cli-native.node') {
|
|
1249
|
+
if (!result.__copilotTermuxCliPatched) {
|
|
1250
|
+
result.__copilotTermuxCliPatched = true;
|
|
1251
|
+
// Rust tokio background thread → SIGSEGV on bionic. Return null so callers
|
|
1252
|
+
// fall back to "unspecified" color scheme (uses default theme).
|
|
1253
|
+
if (typeof result.getColorScheme === 'function') {
|
|
1254
|
+
result.getColorScheme = () => null;
|
|
1255
|
+
}
|
|
1256
|
+
if (typeof result.startColorSchemeListener === 'function') {
|
|
1257
|
+
result.startColorSchemeListener = (_cb) => undefined;
|
|
1258
|
+
}
|
|
1259
|
+
if (typeof result.stopColorSchemeListener === 'function') {
|
|
1260
|
+
result.stopColorSchemeListener = () => undefined;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// === end cli-native.node stubs ===
|
|
1265
|
+
|
|
439
1266
|
return result;
|
|
440
1267
|
};
|
|
441
1268
|
|
|
442
1269
|
// [Android bionic 対応] app.js の oPt() が globalThis.fetch を Rust ベースの B7 に
|
|
443
1270
|
// 差し替えるのを阻止する。linuxmusl-arm64/runtime.node の Rust ネットワークスタックは
|
|
444
1271
|
// bionic 上の実 I/O で動作しないため、Node.js ビルトイン fetch(動作確認済み)に固定する。
|
|
1272
|
+
// GitHub OAuth token via env var or gh CLI (keychain unavailable on bionic)
|
|
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;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function _readGhToken(env) {
|
|
1281
|
+
const envToken =
|
|
1282
|
+
(env && (env.GITHUB_TOKEN || env.GH_TOKEN || env.COPILOT_GITHUB_TOKEN)) ||
|
|
1283
|
+
process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.COPILOT_GITHUB_TOKEN;
|
|
1284
|
+
if (envToken) {
|
|
1285
|
+
return envToken;
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
const { execFile } = require('child_process');
|
|
1289
|
+
const ghToken = await new Promise(resolve => {
|
|
1290
|
+
execFile('gh', ['auth', 'token'], { encoding: 'utf8', timeout: 5000 }, (err, stdout) => {
|
|
1291
|
+
resolve(err ? null : (stdout.trim() || null));
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
return ghToken;
|
|
1295
|
+
} catch (_) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
445
1300
|
const _nativeFetch = globalThis.fetch;
|
|
446
1301
|
Object.defineProperty(globalThis, 'fetch', {
|
|
447
1302
|
configurable: true,
|
|
@@ -449,3 +1304,79 @@ Object.defineProperty(globalThis, 'fetch', {
|
|
|
449
1304
|
get() { return _nativeFetch; },
|
|
450
1305
|
set(_) { /* B7 代入を無視 */ },
|
|
451
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
|
+
return patched;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (!globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__) {
|
|
1362
|
+
globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__ = true;
|
|
1363
|
+
const { registerHooks } = require('module');
|
|
1364
|
+
if (typeof registerHooks === 'function') {
|
|
1365
|
+
registerHooks({
|
|
1366
|
+
load(url, context, nextLoad) {
|
|
1367
|
+
const result = nextLoad(url, context);
|
|
1368
|
+
if (!isTargetCopilotAppJsUrl(url)) return result;
|
|
1369
|
+
if (result.source == null) return result;
|
|
1370
|
+
const wasNonString = typeof result.source !== 'string';
|
|
1371
|
+
const src = wasNonString ? Buffer.from(result.source).toString('utf8') : result.source;
|
|
1372
|
+
const patched = patchAppJsSource(src);
|
|
1373
|
+
return Object.assign({}, result, { source: patched });
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
} else {
|
|
1377
|
+
console.warn('[copilot-termux] UPDATE-001/003: node:module registerHooks() not available on this Node version (' + process.version + '), skipping app.js patch');
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
module.exports.patchAppJsSource = patchAppJsSource;
|
|
1382
|
+
module.exports.isTargetCopilotAppJsUrl = isTargetCopilotAppJsUrl;
|