@bash0816/copilot-termux 1.0.65 → 1.0.68-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 +4 -4
- package/bin/copilot +55 -26
- package/bin/copilot-termux +4 -1
- package/config/copilot-termux-release-manifest.json +15 -0
- package/config/manifest.json +6 -2
- package/lib/check-updates.js +174 -0
- package/lib/platform-patch.js +570 -140
- package/lib/setup.js +141 -1
- package/package.json +2 -1
package/lib/platform-patch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -54,6 +56,16 @@ Module._resolveFilename = function (request, parent, isMain, options) {
|
|
|
54
56
|
return origResolve(request, parent, isMain, options);
|
|
55
57
|
};
|
|
56
58
|
|
|
59
|
+
// [glibc mode の設計方針]
|
|
60
|
+
// glibc mode(COPILOT_TERMUX_GLIBC_MODE=1)は「native実装に全面的に任せるモード」ではない。
|
|
61
|
+
// 正しくは「glibc addon のロードを可能にしつつ、Copilot-Termux が検証済みの
|
|
62
|
+
// JS通信/認証/モデル解決の経路は両モード共通で維持するモード」である。
|
|
63
|
+
// 常時適用されるJSスタブ(isGlibcMode分岐がない箇所)は、bionic回避だけでなく
|
|
64
|
+
// Copilot token交換・Free/Enterprise endpoint補正・モデルstale対策・
|
|
65
|
+
// tools payload補正・stream互換維持を兼ねているため、glibc modeでも意図的に残す。
|
|
66
|
+
// native実装に戻す変更は、スタブ単位ではなくauth/network/model/streamの連鎖単位で
|
|
67
|
+
// 実機検証してから行う(2026-07-06 GPT-5.5レビュー結論)。
|
|
68
|
+
|
|
57
69
|
// [Android bionic 対応] linuxmusl-arm64/runtime.node は Rust tokio を使う。
|
|
58
70
|
// bionic 上で musl pthread ABI でスレッドを生成すると TUI 起動時に SIGSEGV。
|
|
59
71
|
// runtime.node ロード後に Rust async 初期化関数を no-op に差し替えて阻止する。
|
|
@@ -64,65 +76,99 @@ Module._load = function (request, parent, isMain) {
|
|
|
64
76
|
path.basename(request) === 'runtime.node') {
|
|
65
77
|
if (result.__copilotTermuxPatched) return result;
|
|
66
78
|
result.__copilotTermuxPatched = true;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
const isGlibcMode = !!process.env.COPILOT_TERMUX_GLIBC_MODE;
|
|
80
|
+
if (!isGlibcMode) {
|
|
81
|
+
// Rust tokio を使う関数群を no-op に差し替え。
|
|
82
|
+
// sessionStore*/sessionSqlite* は非同期 SQLite (tokio)、
|
|
83
|
+
// modelHttp*/networkFetch*/ahpRelay*/websocketResponses* は Rust HTTP (tokio)。
|
|
84
|
+
// jsonrpcServer* は拡張 JSON-RPC サーバー (ThreadsafeFunction)、
|
|
85
|
+
// lspClient* は LSP クライアント (ThreadsafeFunction)。
|
|
86
|
+
// featureFlagService* は同期 Rust のため除外(no-op にすると .handle クラッシュ)。
|
|
87
|
+
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)/;
|
|
88
|
+
for (const key of Object.keys(result)) {
|
|
89
|
+
if (TOKIO_PATTERN.test(key) && typeof result[key] === 'function') {
|
|
90
|
+
result[key] = () => undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// git*Async: Rust tokio async functions — type-safe stubs to prevent SIGSEGV.
|
|
94
|
+
// Returns empty/null values matching what app.js callers expect.
|
|
95
|
+
const GIT_ASYNC_STUBS = {
|
|
96
|
+
gitMutateAsync: async () => undefined,
|
|
97
|
+
gitCommandAsync: async () => '',
|
|
98
|
+
gitRemotesAsync: async () => [],
|
|
99
|
+
gitDiffFileAsync: async () => '',
|
|
100
|
+
gitHashFileAsync: async () => [],
|
|
101
|
+
gitMergeBaseAsync: async () => null,
|
|
102
|
+
gitDiffForRefAsync: async () => '',
|
|
103
|
+
gitCurrentBranchAsync: async () => null,
|
|
104
|
+
gitDefaultBranchAsync: async () => null,
|
|
105
|
+
gitListWorktreesAsync: async () => [],
|
|
106
|
+
gitBranchAndHeadAsync: async () => null,
|
|
107
|
+
gitSubmodulePathsAsync: async () => [],
|
|
108
|
+
gitUntrackedPathsAsync: async () => [],
|
|
109
|
+
gitDiffNameStatusAsync: async () => '',
|
|
110
|
+
gitStatusPorcelainAsync: async () => '',
|
|
111
|
+
gitWorkingTreeStatusAsync: async () => ({ hasUnstagedChanges: false, hasStagedChanges: false, hasUntrackedFiles: false }),
|
|
112
|
+
gitStatusPorcelainAllAsync: async () => null,
|
|
113
|
+
gitCurrentBranchRemoteAsync: async () => null,
|
|
114
|
+
gitWorkingTreeDiffStatsAsync: async () => ({ linesAdded: 0, linesRemoved: 0 }),
|
|
115
|
+
};
|
|
116
|
+
for (const [key, stub] of Object.entries(GIT_ASYNC_STUBS)) {
|
|
117
|
+
if (typeof result[key] === 'function') result[key] = stub;
|
|
118
|
+
}
|
|
119
|
+
if (typeof result.registerLogSink === 'function') {
|
|
120
|
+
result.registerLogSink = () => { throw new Error('[copilot-termux] registerLogSink disabled on bionic (no tokio thread)'); };
|
|
121
|
+
}
|
|
122
|
+
if (typeof result.networkFetchGetExtraCaPems === 'function') {
|
|
123
|
+
result.networkFetchGetExtraCaPems = () => ({ errors: [], pems: [] });
|
|
124
|
+
}
|
|
125
|
+
if (typeof result.sessionSqliteOpen === 'function') {
|
|
126
|
+
result.sessionSqliteOpen = () => 1;
|
|
127
|
+
}
|
|
128
|
+
if (typeof result.sessionSqliteQuery === 'function') {
|
|
129
|
+
result.sessionSqliteQuery = () => ({ rows: '[]' });
|
|
130
|
+
}
|
|
131
|
+
if (typeof result.sessionSqliteRun === 'function') {
|
|
132
|
+
result.sessionSqliteRun = () => ({ rowsAffected: 0, lastInsertRowid: null });
|
|
133
|
+
}
|
|
134
|
+
if (typeof result.sessionSqliteFileExists === 'function') {
|
|
135
|
+
result.sessionSqliteFileExists = () => false;
|
|
77
136
|
}
|
|
78
137
|
}
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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;
|
|
138
|
+
// agentsResolveToolAliases: v1.0.65 新規追加ネイティブ関数。
|
|
139
|
+
// Free ユーザーのチャット時に呼ばれ、Bionic で誤ったインデックスを返すと
|
|
140
|
+
// tools[N].function.name が undefined → Copilot API 400 になる。
|
|
141
|
+
// ネイティブ結果を検証し、無効なら JS 実装でフォールバックする。
|
|
142
|
+
if (typeof result.agentsResolveToolAliases === 'function') {
|
|
143
|
+
const _nativeResolveAliases = result.agentsResolveToolAliases;
|
|
144
|
+
result.agentsResolveToolAliases = function(allowedTools, allToolsMeta, externalToolsMeta) {
|
|
122
145
|
try {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
const r = _nativeResolveAliases(allowedTools, allToolsMeta, externalToolsMeta);
|
|
147
|
+
if (Array.isArray(r) && r.length > 0 &&
|
|
148
|
+
r.every(i => typeof i === 'number' && i >= 0 && i < allToolsMeta.length)) {
|
|
149
|
+
return r;
|
|
150
|
+
}
|
|
151
|
+
} catch(e) {
|
|
152
|
+
}
|
|
153
|
+
// JS フォールバック
|
|
154
|
+
const allowed = allowedTools;
|
|
155
|
+
// null または ["*"] → 全ツール
|
|
156
|
+
if (!allowed || (allowed.length === 1 && allowed[0] === '*')) {
|
|
157
|
+
return allToolsMeta.map((_, i) => i);
|
|
158
|
+
}
|
|
159
|
+
// 空配列 → 空配列(許可ツールなし)
|
|
160
|
+
if (allowed.length === 0) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
// 名前マッチング
|
|
164
|
+
const allowedSet = new Set(allowed.map(n => (n || '').toLowerCase()));
|
|
165
|
+
const indices = [];
|
|
166
|
+
allToolsMeta.forEach((tool, i) => {
|
|
167
|
+
const name = (tool.name || '').toLowerCase();
|
|
168
|
+
const ns = (tool.namespacedName || '').toLowerCase();
|
|
169
|
+
if (allowedSet.has(name) || allowedSet.has(ns)) indices.push(i);
|
|
170
|
+
});
|
|
171
|
+
return indices;
|
|
126
172
|
};
|
|
127
173
|
}
|
|
128
174
|
// authGetCopilotApiUrl: type=token/env/user/gh-cli/api-key では native が null を返す。
|
|
@@ -136,15 +182,68 @@ Module._load = function (request, parent, isMain) {
|
|
|
136
182
|
try {
|
|
137
183
|
const info = JSON.parse(authInfoJson);
|
|
138
184
|
if (info && info.type !== 'hmac') {
|
|
139
|
-
return
|
|
185
|
+
return 'https://api.githubcopilot.com';
|
|
140
186
|
}
|
|
141
187
|
} catch (_) {}
|
|
142
188
|
return r;
|
|
143
189
|
};
|
|
144
190
|
}
|
|
191
|
+
// capiClientPrepareRequestHeaders: native は OAuth token をそのまま Bearer にする。
|
|
192
|
+
// Copilot 推論 API は Copilot token が必要なため、キャッシュ済み copilotToken で差し替え。
|
|
193
|
+
if (typeof result.capiClientPrepareRequestHeaders === 'function') {
|
|
194
|
+
const _nativePrepareRequestHeaders = result.capiClientPrepareRequestHeaders;
|
|
195
|
+
result.capiClientPrepareRequestHeaders = function(handle, ...args) {
|
|
196
|
+
const prepared = _nativePrepareRequestHeaders(handle, ...args);
|
|
197
|
+
// _authMgr から有効な copilotToken を探して Authorization を差し替え
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
for (const entry of _authMgr.values()) {
|
|
200
|
+
if (entry.copilotToken && (entry.copilotTokenExpiry === 0 || entry.copilotTokenExpiry > now)) {
|
|
201
|
+
if (prepared && Array.isArray(prepared.headers)) {
|
|
202
|
+
const headers = prepared.headers.map(h =>
|
|
203
|
+
h.name && h.name.toLowerCase() === 'authorization'
|
|
204
|
+
? { name: h.name, value: `Bearer ${entry.copilotToken}` }
|
|
205
|
+
: h
|
|
206
|
+
);
|
|
207
|
+
return { ...prepared, headers };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return prepared;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// modelsFilterToPicker: native は model_picker_enabled=true のモデルのみ返す。
|
|
215
|
+
// Free プランは全モデル model_picker_enabled=false → native が [] を返す。
|
|
216
|
+
// PICKER-001: fallback に gpt-4o-mini/gpt-4o を返すと TUI /model ピッカーに
|
|
217
|
+
// ユーティリティモデルが表示されてしまう(公式仕様違反)。
|
|
218
|
+
// auto モードの default 選択は modelResolverFirstAvailableDefaultFromOrder が担うため
|
|
219
|
+
// ここでは fallback せず [] を返す。
|
|
220
|
+
if (typeof result.modelsFilterToPicker === 'function') {
|
|
221
|
+
const _nativeModelsFilterToPicker = result.modelsFilterToPicker;
|
|
222
|
+
result.modelsFilterToPicker = function(...args) {
|
|
223
|
+
const nativeResult = _nativeModelsFilterToPicker.apply(this, args);
|
|
224
|
+
if (Array.isArray(nativeResult) && nativeResult.length > 0) return nativeResult;
|
|
225
|
+
return [];
|
|
226
|
+
};
|
|
227
|
+
}
|
|
145
228
|
// capiClientListModels を Node.js fetch で実装(Rust tokio SIGSEGV 回避)
|
|
229
|
+
// _selectCapiUrl: COPILOT_API_URL が api.individual.githubcopilot.com(Free プラン専用 direct endpoint)の
|
|
230
|
+
// 場合のみそれを使う。Enterprise proxy URL(api.business.githubcopilot.com 等)では /models が 421 に
|
|
231
|
+
// なるため標準 CAPI に倒す。hostname 完全一致チェックで SSRF/token 漏洩を防ぐ(GPT-5.5 指摘 #1)。
|
|
232
|
+
const _DEFAULT_CAPI_URL = 'https://api.githubcopilot.com';
|
|
233
|
+
const _INDIVIDUAL_CAPI_URL = 'https://api.individual.githubcopilot.com';
|
|
234
|
+
function _selectCapiUrl(rawApiUrl) {
|
|
235
|
+
try {
|
|
236
|
+
const u = new URL(rawApiUrl || _DEFAULT_CAPI_URL);
|
|
237
|
+
if (u.protocol === 'https:' && u.hostname === 'api.individual.githubcopilot.com') {
|
|
238
|
+
return _INDIVIDUAL_CAPI_URL;
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
}
|
|
242
|
+
return _DEFAULT_CAPI_URL;
|
|
243
|
+
}
|
|
146
244
|
if (typeof result.capiClientListModels === 'function') {
|
|
147
245
|
result.capiClientListModels = async function(handle, _includeHidden, _skipCache, _applyModelLimitCaps, _networkingConfigId) {
|
|
246
|
+
const snapshotGen = _modelListCacheGen; // 開始時の世代をキャプチャ → 完了時に照合して stale 結果を破棄
|
|
148
247
|
let authHeaders;
|
|
149
248
|
try {
|
|
150
249
|
const prepared = result.capiClientPrepareRequestHeaders(handle, '', []);
|
|
@@ -155,11 +254,14 @@ Module._load = function (request, parent, isMain) {
|
|
|
155
254
|
} catch (e) {
|
|
156
255
|
throw new Error(JSON.stringify({kind: 'network', message: `prepareHeaders failed: ${e.message}`}));
|
|
157
256
|
}
|
|
158
|
-
|
|
159
|
-
|
|
257
|
+
// fetchUrl: /models 取得URL。Free は api.individual(COPILOT_API_URL 設定時)、他は標準 CAPI。
|
|
258
|
+
// copilotUrl: 推論URL。常に標準 CAPI 固定(v1.0.63 と同じ)。
|
|
259
|
+
// BUG-NEW-2 で copilotUrl=fetchUrl にしたため api.individual への推論が発生していた(副作用修正)。
|
|
260
|
+
const fetchUrl = _selectCapiUrl(process.env.COPILOT_API_URL);
|
|
261
|
+
const copilotUrl = _DEFAULT_CAPI_URL;
|
|
160
262
|
let res;
|
|
161
263
|
try {
|
|
162
|
-
res = await globalThis.fetch(`${
|
|
264
|
+
res = await globalThis.fetch(`${fetchUrl}/models`, {method: 'GET', headers: authHeaders});
|
|
163
265
|
} catch (e) {
|
|
164
266
|
throw new Error(JSON.stringify({kind: 'network', message: e.message}));
|
|
165
267
|
}
|
|
@@ -169,23 +271,16 @@ Module._load = function (request, parent, isMain) {
|
|
|
169
271
|
throw new Error(JSON.stringify({kind: 'http', status: res.status, statusText: res.statusText, body, headers: hdrs, hasRequestId: res.headers.has('x-request-id')}));
|
|
170
272
|
}
|
|
171
273
|
const data = await res.json();
|
|
172
|
-
const
|
|
274
|
+
const raw = Array.isArray(data) ? data : (data.data ?? data.models ?? []);
|
|
275
|
+
const models = Array.isArray(raw) ? raw : [];
|
|
276
|
+
if (_modelListCacheGen === snapshotGen) {
|
|
277
|
+
_modelListCache = models;
|
|
278
|
+
} else {
|
|
279
|
+
}
|
|
173
280
|
const rateHeaders = [...res.headers.entries()].map(([name, value]) => ({name, value}));
|
|
174
|
-
return {modelsJson: JSON.stringify(models), copilotUrl
|
|
281
|
+
return {modelsJson: JSON.stringify(models), copilotUrl, usageRatelimitHeaders: rateHeaders, capturedAssignmentContext: undefined};
|
|
175
282
|
};
|
|
176
283
|
}
|
|
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
284
|
// --- JS networkFetch* implementation (bionic: tokio networkFetch* are no-op'd) ---
|
|
190
285
|
// B7 が QXe() から直接呼ばれる MCP 専用パスでクラッシュするため JS で代替。
|
|
191
286
|
// networkFetchStreamStart → { requestId, response: Promise<{handle,url,status,statusText,headers}> }
|
|
@@ -201,6 +296,24 @@ Module._load = function (request, parent, isMain) {
|
|
|
201
296
|
const entry = { abort: () => abortCtrl.abort(), reader: null };
|
|
202
297
|
_nfMap.set(requestId, entry);
|
|
203
298
|
|
|
299
|
+
// Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
|
|
300
|
+
let body = req.body ?? undefined;
|
|
301
|
+
if (body) {
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
|
|
304
|
+
if (parsed && Array.isArray(parsed.tools)) {
|
|
305
|
+
const before = parsed.tools.length;
|
|
306
|
+
parsed.tools = parsed.tools.filter(
|
|
307
|
+
t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
|
|
308
|
+
);
|
|
309
|
+
if (parsed.tools.length !== before) {
|
|
310
|
+
if (parsed.tools.length === 0) delete parsed.tools;
|
|
311
|
+
body = JSON.stringify(parsed);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch(_) {}
|
|
315
|
+
}
|
|
316
|
+
|
|
204
317
|
const headers = {};
|
|
205
318
|
if (Array.isArray(req.headers)) {
|
|
206
319
|
for (const { name, value } of req.headers) headers[name] = value;
|
|
@@ -214,7 +327,7 @@ Module._load = function (request, parent, isMain) {
|
|
|
214
327
|
res = await globalThis.fetch(req.url, {
|
|
215
328
|
method: req.method || 'GET',
|
|
216
329
|
headers,
|
|
217
|
-
body:
|
|
330
|
+
body: body,
|
|
218
331
|
signal: abortCtrl.signal,
|
|
219
332
|
redirect: req.redirect || 'follow',
|
|
220
333
|
});
|
|
@@ -387,7 +500,6 @@ Module._load = function (request, parent, isMain) {
|
|
|
387
500
|
if (typeof result.anthropicMessageStreamAccumulatorFinish === 'function') {
|
|
388
501
|
result.anthropicMessageStreamAccumulatorFinish = function(id) {
|
|
389
502
|
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
503
|
_jsAccs.delete(id);
|
|
392
504
|
return { json: JSON.stringify({ message: acc.message }) };
|
|
393
505
|
};
|
|
@@ -403,7 +515,6 @@ Module._load = function (request, parent, isMain) {
|
|
|
403
515
|
// 4. modelHttpStreamStart → fetch full SSE body, parse events
|
|
404
516
|
result.modelHttpStreamStart = async function(jsonArg) {
|
|
405
517
|
const req = JSON.parse(jsonArg);
|
|
406
|
-
_dbg('modelHttpStreamStart', { url: req.url, method: req.method });
|
|
407
518
|
let body = req.body;
|
|
408
519
|
if (body !== null && body !== undefined && typeof body === 'object') {
|
|
409
520
|
if (body.type === 'Buffer' && Array.isArray(body.data)) {
|
|
@@ -412,11 +523,44 @@ Module._load = function (request, parent, isMain) {
|
|
|
412
523
|
body = JSON.stringify(body);
|
|
413
524
|
}
|
|
414
525
|
}
|
|
526
|
+
// Fix 2 (MODEL-001): /responses 以外は reasoning_effort を除去(/v1/messages 等が 400 になるのを防ぐ)
|
|
527
|
+
if (body && req.url && !req.url.includes('/responses')) {
|
|
528
|
+
try {
|
|
529
|
+
const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
|
|
530
|
+
if (parsed && 'reasoning_effort' in parsed) {
|
|
531
|
+
delete parsed.reasoning_effort;
|
|
532
|
+
body = JSON.stringify(parsed);
|
|
533
|
+
}
|
|
534
|
+
} catch(_) {}
|
|
535
|
+
}
|
|
536
|
+
// Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
|
|
537
|
+
if (body) {
|
|
538
|
+
try {
|
|
539
|
+
const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
|
|
540
|
+
if (parsed && Array.isArray(parsed.tools)) {
|
|
541
|
+
const before = parsed.tools.length;
|
|
542
|
+
parsed.tools = parsed.tools.filter(
|
|
543
|
+
t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
|
|
544
|
+
);
|
|
545
|
+
if (parsed.tools.length !== before) {
|
|
546
|
+
if (parsed.tools.length === 0) delete parsed.tools;
|
|
547
|
+
body = JSON.stringify(parsed);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch(_) {}
|
|
551
|
+
}
|
|
552
|
+
// Fix (TC-6): Free account model override — Enterprise→Free 切替後に stale モデルが残る問題
|
|
553
|
+
body = _applyModelOverride(body, req.url, 'modelHttpStreamStart');
|
|
554
|
+
// body 書き換え後に content-length が古くなるのを防ぐ(reasoning_effort/tools/model 除去で長さが変わる)
|
|
555
|
+
const fetchHeaders = {};
|
|
556
|
+
for (const [k, v] of Object.entries(req.headers || {})) {
|
|
557
|
+
if (k.toLowerCase() !== 'content-length') fetchHeaders[k] = v;
|
|
558
|
+
}
|
|
415
559
|
let res;
|
|
416
560
|
try {
|
|
417
561
|
res = await globalThis.fetch(req.url, {
|
|
418
562
|
method: req.method || 'POST',
|
|
419
|
-
headers:
|
|
563
|
+
headers: fetchHeaders,
|
|
420
564
|
body: body,
|
|
421
565
|
});
|
|
422
566
|
} catch(e) {
|
|
@@ -429,13 +573,11 @@ Module._load = function (request, parent, isMain) {
|
|
|
429
573
|
return { json: JSON.stringify({ bodyText, status: res.status, statusText: res.statusText, headers, streamId: null }) };
|
|
430
574
|
}
|
|
431
575
|
const events = _parseAnthropicSSE(bodyText);
|
|
432
|
-
_dbg('modelHttpStreamStart:parsed', { status: res.status, eventCount: events.length, bodySnippet: bodyText.slice(0, 300) });
|
|
433
576
|
const finalMessage = _reconstructFinalMessage(events);
|
|
434
577
|
const streamId = 'js-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
|
|
435
578
|
_jsStreams.set(streamId, { events, index: 0, finalMessage });
|
|
436
579
|
return { json: JSON.stringify({ bodyText: null, status: res.status, statusText: res.statusText, headers, streamId }) };
|
|
437
580
|
};
|
|
438
|
-
|
|
439
581
|
// Helper: convert Anthropic SSE event → chunkContext for processAnthropicStreamingChunkContext
|
|
440
582
|
function _toChunkContext(event, streamId) {
|
|
441
583
|
const base = { content: '', size: 0, chunkBoundary: false, messageStart: false, streamingId: streamId };
|
|
@@ -464,7 +606,6 @@ Module._load = function (request, parent, isMain) {
|
|
|
464
606
|
return base;
|
|
465
607
|
}
|
|
466
608
|
}
|
|
467
|
-
|
|
468
609
|
// 5. modelHttpStreamNextAnthropicMessageEvent → yield events one by one
|
|
469
610
|
result.modelHttpStreamNextAnthropicMessageEvent = async function(streamId, accId) {
|
|
470
611
|
const st = _jsStreams.get(streamId);
|
|
@@ -498,11 +639,44 @@ Module._load = function (request, parent, isMain) {
|
|
|
498
639
|
body = JSON.stringify(body);
|
|
499
640
|
}
|
|
500
641
|
}
|
|
642
|
+
// Fix 2 (MODEL-001): /responses 以外は reasoning_effort を除去
|
|
643
|
+
if (body && req.url && !req.url.includes('/responses')) {
|
|
644
|
+
try {
|
|
645
|
+
const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
|
|
646
|
+
if (parsed && 'reasoning_effort' in parsed) {
|
|
647
|
+
delete parsed.reasoning_effort;
|
|
648
|
+
body = JSON.stringify(parsed);
|
|
649
|
+
}
|
|
650
|
+
} catch(_) {}
|
|
651
|
+
}
|
|
652
|
+
// Fix (TOOLS-002): tools[].function.name が空/undefined のエントリをフィルタリング
|
|
653
|
+
if (body) {
|
|
654
|
+
try {
|
|
655
|
+
const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
|
|
656
|
+
if (parsed && Array.isArray(parsed.tools)) {
|
|
657
|
+
const before = parsed.tools.length;
|
|
658
|
+
parsed.tools = parsed.tools.filter(
|
|
659
|
+
t => t && t.function && typeof t.function.name === 'string' && t.function.name.length > 0
|
|
660
|
+
);
|
|
661
|
+
if (parsed.tools.length !== before) {
|
|
662
|
+
if (parsed.tools.length === 0) delete parsed.tools;
|
|
663
|
+
body = JSON.stringify(parsed);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} catch(_) {}
|
|
667
|
+
}
|
|
668
|
+
// Fix (TC-6): Free account model override — Enterprise→Free 切替後に stale モデルが残る問題
|
|
669
|
+
body = _applyModelOverride(body, req.url, 'modelHttpRequest');
|
|
670
|
+
// body 書き換え後に content-length が古くなるのを防ぐ
|
|
671
|
+
const fetchHeaders = {};
|
|
672
|
+
for (const [k, v] of Object.entries(req.headers || {})) {
|
|
673
|
+
if (k.toLowerCase() !== 'content-length') fetchHeaders[k] = v;
|
|
674
|
+
}
|
|
501
675
|
let res;
|
|
502
676
|
try {
|
|
503
677
|
res = await globalThis.fetch(req.url, {
|
|
504
678
|
method: req.method || 'POST',
|
|
505
|
-
headers:
|
|
679
|
+
headers: fetchHeaders,
|
|
506
680
|
body: body,
|
|
507
681
|
});
|
|
508
682
|
} catch(e) {
|
|
@@ -524,7 +698,6 @@ Module._load = function (request, parent, isMain) {
|
|
|
524
698
|
if (!st) {
|
|
525
699
|
throw new Error(`Native mod HTTP stream was not found: ${streamId}`);
|
|
526
700
|
}
|
|
527
|
-
_dbg('responsesStreamDrive:start', { streamId, eventCount: st.events.length, hasProcessors, sample: st.events.slice(0,2) });
|
|
528
701
|
_jsStreams.delete(streamId);
|
|
529
702
|
let copilotUsage = null;
|
|
530
703
|
for (const event of st.events) {
|
|
@@ -535,7 +708,6 @@ Module._load = function (request, parent, isMain) {
|
|
|
535
708
|
if (parsed && parsed.copilotUsage !== undefined && parsed.copilotUsage !== null)
|
|
536
709
|
copilotUsage = parsed.copilotUsage;
|
|
537
710
|
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
711
|
if (hasProcessors && typeof onChunkCallback === 'function' && cc &&
|
|
540
712
|
(cc.content || cc.messageStart || cc.reportIntentArguments || cc.chunkBoundary || cc.size > 0)) {
|
|
541
713
|
try { onChunkCallback(JSON.stringify(cc)); } catch (_) {}
|
|
@@ -543,18 +715,142 @@ Module._load = function (request, parent, isMain) {
|
|
|
543
715
|
}
|
|
544
716
|
return { json: JSON.stringify({ kind: 'ok', copilotUsage, ttftMs: null, interTokenLatencyMs: null }) };
|
|
545
717
|
};
|
|
718
|
+
// Helper: Free アカウント切替後に Enterprise モデルが残る問題を防ぐ model override。
|
|
719
|
+
// /chat/completions 系 URL にのみ適用し、_modelListCache の enabled モデルと照合。
|
|
720
|
+
// 対象外の場合は body をそのまま返す(破壊なし)。
|
|
721
|
+
function _applyModelOverride(body, url, prefix) {
|
|
722
|
+
try {
|
|
723
|
+
if (!body) return body;
|
|
724
|
+
const pathname = new URL(url).pathname;
|
|
725
|
+
// /chat/completions のみを対象にする
|
|
726
|
+
if (!pathname.endsWith('/chat/completions')) return body;
|
|
727
|
+
const parsed = JSON.parse(typeof body === 'string' ? body : body.toString());
|
|
728
|
+
if (!parsed || !parsed.model) return body;
|
|
729
|
+
if (_modelListCache && _modelListCache.length > 0) {
|
|
730
|
+
const enabledIds = new Set(
|
|
731
|
+
_modelListCache.filter(m => m?.policy?.state === 'enabled').map(m => m.id).filter(Boolean)
|
|
732
|
+
);
|
|
733
|
+
if (!enabledIds.has(parsed.model)) {
|
|
734
|
+
// goldeneye-free-auto のみを対象にする
|
|
735
|
+
const OVERRIDE_CANDIDATES = ['goldeneye-free-auto'];
|
|
736
|
+
let fallbackModel = null;
|
|
737
|
+
for (const id of OVERRIDE_CANDIDATES) {
|
|
738
|
+
const m = _modelListCache.find(m => m?.id === id && m?.policy?.state === 'enabled');
|
|
739
|
+
if (m) { fallbackModel = id; break; }
|
|
740
|
+
}
|
|
741
|
+
if (fallbackModel) {
|
|
742
|
+
parsed.model = fallbackModel;
|
|
743
|
+
return JSON.stringify(parsed);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} catch(_) {}
|
|
748
|
+
return body;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// 9. chatCompletionStreamDrive → OpenAI-compatible chat completion stream処理
|
|
752
|
+
// native は JS-side streamId("js-")を知らないため JS で代替。
|
|
753
|
+
result.chatCompletionStreamDrive = async function(streamId, reducerId, hasProcessors, onChunkCallback) {
|
|
754
|
+
const st = _jsStreams.get(streamId);
|
|
755
|
+
if (!st) {
|
|
756
|
+
throw new Error(`Native model HTTP stream was not found: ${streamId}`);
|
|
757
|
+
}
|
|
758
|
+
_jsStreams.delete(streamId);
|
|
759
|
+
let content = '', finishReason = null, id = null, model = null, role = 'assistant', usage = null, created = null;
|
|
760
|
+
const toolCallsByIndex = new Map(); // index → {id, type, function: {name, arguments}}
|
|
761
|
+
let functionCall = null; // 旧 delta.function_call 形式
|
|
762
|
+
let isFirstChunk = true;
|
|
763
|
+
// Fix (GPT-5.5 No-Go #1): 終端 finish_reason が来たか追跡する
|
|
764
|
+
// finishReason || 'stop' のデフォルトは壊れたストリームを正常完了に見せる危険がある
|
|
765
|
+
let seenTerminalFinishReason = false;
|
|
766
|
+
const TERMINAL_FINISH_REASONS = new Set(['stop', 'tool_calls', 'function_call', 'length', 'content_filter']);
|
|
767
|
+
for (const event of st.events) {
|
|
768
|
+
if (!event) continue;
|
|
769
|
+
if (event.id) id = event.id;
|
|
770
|
+
if (event.model) model = event.model;
|
|
771
|
+
if (event.usage) usage = event.usage;
|
|
772
|
+
if (event.created) created = event.created;
|
|
773
|
+
if (!Array.isArray(event.choices)) continue;
|
|
774
|
+
for (const choice of event.choices) {
|
|
775
|
+
if (choice.delta) {
|
|
776
|
+
if (choice.delta.role) role = choice.delta.role;
|
|
777
|
+
if (typeof choice.delta.content === 'string') content += choice.delta.content;
|
|
778
|
+
// tool_calls 断片を index ごとに集約
|
|
779
|
+
if (Array.isArray(choice.delta.tool_calls)) {
|
|
780
|
+
for (const tc of choice.delta.tool_calls) {
|
|
781
|
+
const idx = tc.index ?? 0;
|
|
782
|
+
if (!toolCallsByIndex.has(idx)) {
|
|
783
|
+
toolCallsByIndex.set(idx, { id: '', type: 'function', function: { name: '', arguments: '' } });
|
|
784
|
+
}
|
|
785
|
+
const entry = toolCallsByIndex.get(idx);
|
|
786
|
+
if (tc.id) entry.id = tc.id;
|
|
787
|
+
if (tc.type) entry.type = tc.type;
|
|
788
|
+
if (tc.function) {
|
|
789
|
+
if (tc.function.name) entry.function.name += tc.function.name;
|
|
790
|
+
if (typeof tc.function.arguments === 'string') entry.function.arguments += tc.function.arguments;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// 旧 function_call 形式
|
|
795
|
+
if (choice.delta.function_call) {
|
|
796
|
+
if (!functionCall) functionCall = { name: '', arguments: '' };
|
|
797
|
+
if (choice.delta.function_call.name) functionCall.name += choice.delta.function_call.name;
|
|
798
|
+
if (typeof choice.delta.function_call.arguments === 'string') functionCall.arguments += choice.delta.function_call.arguments;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (choice.finish_reason) {
|
|
802
|
+
finishReason = choice.finish_reason;
|
|
803
|
+
if (TERMINAL_FINISH_REASONS.has(choice.finish_reason)) seenTerminalFinishReason = true;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (hasProcessors && typeof onChunkCallback === 'function') {
|
|
807
|
+
let chunkContent = '', isBoundary = false, hasToolDelta = false;
|
|
808
|
+
for (const choice of event.choices) {
|
|
809
|
+
if (choice.delta && typeof choice.delta.content === 'string') chunkContent += choice.delta.content;
|
|
810
|
+
// Fix (GPT-5.5 No-Go #2): tool/function call delta も callback に通知する
|
|
811
|
+
if (choice.delta && (choice.delta.tool_calls || choice.delta.function_call)) hasToolDelta = true;
|
|
812
|
+
if (choice.finish_reason) isBoundary = true;
|
|
813
|
+
}
|
|
814
|
+
if (chunkContent || isBoundary || isFirstChunk || hasToolDelta) {
|
|
815
|
+
try { onChunkCallback(JSON.stringify({ content: chunkContent, size: Buffer.byteLength(chunkContent, 'utf8'), chunkBoundary: isBoundary, messageStart: isFirstChunk, streamingId: streamId })); } catch (_) {}
|
|
816
|
+
}
|
|
817
|
+
isFirstChunk = false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const toolCalls = toolCallsByIndex.size > 0
|
|
821
|
+
? Array.from(toolCallsByIndex.entries()).sort(([a], [b]) => a - b).map(([, v]) => v)
|
|
822
|
+
: undefined;
|
|
823
|
+
// tool call 系では content は null が正しい(テキストと排他)
|
|
824
|
+
const msgContent = (toolCalls || functionCall) ? (content || null) : content;
|
|
825
|
+
const message = { role, content: msgContent };
|
|
826
|
+
if (toolCalls) message.tool_calls = toolCalls;
|
|
827
|
+
if (functionCall) message.function_call = functionCall;
|
|
828
|
+
// Fix (GPT-5.5 No-Go #1): 終端が来ていない場合は 'stop' を補完しない
|
|
829
|
+
const effectiveFinishReason = seenTerminalFinishReason ? finishReason : (finishReason ?? null);
|
|
830
|
+
const completion = {
|
|
831
|
+
id: id || ('chatcmpl-' + Date.now().toString(36)),
|
|
832
|
+
object: 'chat.completion',
|
|
833
|
+
created: created || Math.floor(Date.now() / 1000),
|
|
834
|
+
model: model || 'gpt-4o',
|
|
835
|
+
choices: [{ index: 0, message, finish_reason: effectiveFinishReason, logprobs: null }],
|
|
836
|
+
usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
837
|
+
};
|
|
838
|
+
return { json: JSON.stringify({ kind: 'ok', completion, copilotUsage: null, ttftMs: null, interTokenLatencyMs: null }) };
|
|
839
|
+
};
|
|
840
|
+
// glibc modeでも維持(isGlibcMode分岐なし): bionic回避に加えCopilot token交換・アカウント切替キャッシュ管理を兼ねるため
|
|
546
841
|
// === authManager* JS stubs (1.0.64: tokio thread spawn → SIGSEGV on bionic) ===
|
|
547
842
|
const _authMgr = new Map(); // uuid → { cachedInfo, pendingInfo, cachedToken, cachedHost, gen }
|
|
843
|
+
let _modelListCache = null; // capiClientListModels が取得したモデルリストキャッシュ(modelResolver fallback 用)
|
|
844
|
+
let _modelListCacheGen = 0; // アカウント切替時にインクリメント → 古い /models 結果の上書きを防ぐ
|
|
548
845
|
|
|
549
846
|
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 });
|
|
847
|
+
_authMgr.set(uuid, { cachedInfo: null, pendingInfo: null, cachedToken: null, cachedHost: null, gen: 0, copilotToken: null, copilotTokenExpiry: 0 });
|
|
551
848
|
// native 非呼び出し: tokio runtime 生成を阻止
|
|
552
849
|
};
|
|
553
850
|
|
|
554
851
|
async function _buildAuthInfo(token, hostUri) {
|
|
555
852
|
let login = null;
|
|
556
853
|
let copilotUser = null;
|
|
557
|
-
_dbg('buildAuthInfo:start', { tokenHash: _tokenHash(token), hostUri });
|
|
558
854
|
try {
|
|
559
855
|
const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
|
|
560
856
|
const res = await globalThis.fetch(`${apiHost}/user`, {
|
|
@@ -562,9 +858,7 @@ Module._load = function (request, parent, isMain) {
|
|
|
562
858
|
signal: AbortSignal.timeout(5000),
|
|
563
859
|
});
|
|
564
860
|
if (res.ok) login = (await res.json()).login;
|
|
565
|
-
_dbg('buildAuthInfo:/user', { status: res.status, login });
|
|
566
861
|
} catch (e) {
|
|
567
|
-
_dbg('buildAuthInfo:/user:error', { err: e.message });
|
|
568
862
|
}
|
|
569
863
|
// copilot_internal/user から copilotUser 全体を取得して authInfo に含める。
|
|
570
864
|
// app.js の Wa(authInfo) は authInfo.copilotUser.endpoints.api から CAPI base URL を導く。
|
|
@@ -573,7 +867,7 @@ Module._load = function (request, parent, isMain) {
|
|
|
573
867
|
try {
|
|
574
868
|
const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
|
|
575
869
|
const r = await globalThis.fetch(`${apiHost}/copilot_internal/user`, {
|
|
576
|
-
headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': 'copilot-
|
|
870
|
+
headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': process.env.GITHUB_COPILOT_INTEGRATION_ID || 'copilot-developer-cli' },
|
|
577
871
|
signal: AbortSignal.timeout(5000),
|
|
578
872
|
});
|
|
579
873
|
if (r.ok) {
|
|
@@ -581,22 +875,35 @@ Module._load = function (request, parent, isMain) {
|
|
|
581
875
|
copilotUser = info;
|
|
582
876
|
const apiUrl = info?.endpoints?.api;
|
|
583
877
|
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
878
|
} else {
|
|
592
|
-
_dbg('buildAuthInfo:/copilot_internal/user', { status: r.status, ok: false });
|
|
593
879
|
}
|
|
594
880
|
} catch (e) {
|
|
595
|
-
|
|
881
|
+
}
|
|
882
|
+
// Copilot API token 取得(推論 API は OAuth token を受け付けないため交換が必要)
|
|
883
|
+
let copilotToken = null;
|
|
884
|
+
let copilotTokenExpiry = 0;
|
|
885
|
+
try {
|
|
886
|
+
const apiHost = hostUri.replace('https://github.com', 'https://api.github.com');
|
|
887
|
+
const t = await globalThis.fetch(`${apiHost}/copilot_internal/v2/token`, {
|
|
888
|
+
method: 'GET',
|
|
889
|
+
headers: { Authorization: `token ${token}`, 'User-Agent': `copilot-termux/${_pkgVersion}`, 'Copilot-Integration-Id': process.env.GITHUB_COPILOT_INTEGRATION_ID || 'copilot-developer-cli' },
|
|
890
|
+
signal: AbortSignal.timeout(5000),
|
|
891
|
+
});
|
|
892
|
+
if (t.ok) {
|
|
893
|
+
const td = await t.json();
|
|
894
|
+
copilotToken = td.token || null;
|
|
895
|
+
copilotTokenExpiry = copilotToken ? _normalizeCopilotTokenExpiry(td.expires_at) : 0;
|
|
896
|
+
} else {
|
|
897
|
+
let body = null;
|
|
898
|
+
try { const raw = await t.text(); body = raw.length > 500 ? raw.slice(0, 500) + '…' : raw; } catch (_) {}
|
|
899
|
+
}
|
|
900
|
+
} catch (e) {
|
|
596
901
|
}
|
|
597
902
|
return JSON.stringify({
|
|
598
903
|
authInfo: { type: 'token', host: hostUri, token, login, copilotUser },
|
|
599
904
|
token,
|
|
905
|
+
copilotToken,
|
|
906
|
+
copilotTokenExpiry,
|
|
600
907
|
});
|
|
601
908
|
}
|
|
602
909
|
|
|
@@ -610,15 +917,15 @@ Module._load = function (request, parent, isMain) {
|
|
|
610
917
|
)
|
|
611
918
|
: null) || 'https://github.com').replace(/\/+$/, '');
|
|
612
919
|
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
920
|
entry.cachedInfo = null;
|
|
618
921
|
entry.pendingInfo = null;
|
|
922
|
+
entry.copilotToken = null;
|
|
923
|
+
entry.copilotTokenExpiry = 0;
|
|
924
|
+
_modelListCache = null;
|
|
925
|
+
_modelListCacheGen++;
|
|
926
|
+
delete process.env.COPILOT_API_URL;
|
|
619
927
|
}
|
|
620
928
|
if (entry.cachedInfo !== null) {
|
|
621
|
-
_dbg('resolveOrCache:cache-hit', { tokenHash: _tokenHash(token) });
|
|
622
929
|
return entry.cachedInfo;
|
|
623
930
|
}
|
|
624
931
|
if (!entry.pendingInfo) {
|
|
@@ -635,6 +942,10 @@ Module._load = function (request, parent, isMain) {
|
|
|
635
942
|
typeof parsed.authInfo.login === 'string' && parsed.authInfo.login.length > 0) {
|
|
636
943
|
_loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
|
|
637
944
|
}
|
|
945
|
+
// authManagerSwitchToAuth / authManagerLoginUser と同様に copilotToken を設定
|
|
946
|
+
// _resolveOrCache 経由(TUI 初回起動)でも copilot token が使われるようにする
|
|
947
|
+
entry.copilotToken = (parsed && parsed.copilotToken) || null;
|
|
948
|
+
entry.copilotTokenExpiry = (parsed && parsed.copilotTokenExpiry) || 0;
|
|
638
949
|
} catch (_) {}
|
|
639
950
|
return info;
|
|
640
951
|
}).catch(err => {
|
|
@@ -668,18 +979,22 @@ Module._load = function (request, parent, isMain) {
|
|
|
668
979
|
result.authManagerGetLastAuthErrors = function(uuid) { return []; };
|
|
669
980
|
result.authManagerClearCache = function(uuid) {
|
|
670
981
|
const e = _authMgr.get(uuid);
|
|
671
|
-
if (e) { e.gen++; e.cachedInfo = null; e.pendingInfo = null; e.cachedToken = null; e.cachedHost = null; }
|
|
982
|
+
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
983
|
};
|
|
673
984
|
result.authManagerSwitchToAuth = async function(uuid, authInfoJson, token) {
|
|
674
985
|
const entry = _authMgr.get(uuid);
|
|
675
986
|
if (!entry) return;
|
|
676
|
-
// Clear stale enterprise endpoint and cache regardless of token presence
|
|
987
|
+
// Clear stale enterprise endpoint and auth cache regardless of token presence
|
|
677
988
|
delete process.env.COPILOT_API_URL;
|
|
989
|
+
_modelListCache = null;
|
|
990
|
+
_modelListCacheGen++;
|
|
678
991
|
const gen = ++entry.gen;
|
|
679
992
|
entry.cachedInfo = null;
|
|
680
993
|
entry.cachedToken = null;
|
|
681
994
|
entry.cachedHost = null;
|
|
682
995
|
entry.pendingInfo = null;
|
|
996
|
+
entry.copilotToken = null;
|
|
997
|
+
entry.copilotTokenExpiry = 0;
|
|
683
998
|
if (!token) return;
|
|
684
999
|
const hostUri = (() => {
|
|
685
1000
|
try { return (JSON.parse(authInfoJson)?.host || 'https://github.com').replace(/\/+$/, ''); }
|
|
@@ -691,23 +1006,21 @@ Module._load = function (request, parent, isMain) {
|
|
|
691
1006
|
entry.cachedToken = token;
|
|
692
1007
|
entry.cachedHost = hostUri;
|
|
693
1008
|
entry.pendingInfo = null;
|
|
1009
|
+
try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
|
|
694
1010
|
return info;
|
|
695
1011
|
}).catch(err => {
|
|
696
1012
|
if (entry.gen === gen) entry.pendingInfo = null;
|
|
697
|
-
_dbg('authManagerSwitchToAuth:error', { err: err.message });
|
|
698
1013
|
});
|
|
699
1014
|
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
1015
|
};
|
|
705
1016
|
result.authManagerLoginUser = async function(uuid, host, login, token) {
|
|
706
|
-
_dbg('authManagerLoginUser:start', { host, login, tokenHash: _tokenHash(token) });
|
|
707
1017
|
const entry = _authMgr.get(uuid);
|
|
708
1018
|
if (!entry || !token) return;
|
|
709
1019
|
const hostUri = (host || 'https://github.com').replace(/\/+$/, '');
|
|
710
1020
|
_loginTokens.set(`${hostUri}:${login || ''}`, token);
|
|
1021
|
+
delete process.env.COPILOT_API_URL;
|
|
1022
|
+
_modelListCache = null;
|
|
1023
|
+
_modelListCacheGen++;
|
|
711
1024
|
const gen = ++entry.gen;
|
|
712
1025
|
entry.pendingInfo = _buildAuthInfo(token, hostUri).then(info => {
|
|
713
1026
|
if (entry.gen !== gen) return info; // stale: a newer switch superseded this fetch
|
|
@@ -722,13 +1035,14 @@ Module._load = function (request, parent, isMain) {
|
|
|
722
1035
|
_loginTokens.set(`${hostUri}:${parsed.authInfo.login}`, token);
|
|
723
1036
|
}
|
|
724
1037
|
} catch (_) {}
|
|
1038
|
+
try { const p = JSON.parse(info); entry.copilotToken = p.copilotToken || null; entry.copilotTokenExpiry = p.copilotTokenExpiry || 0; } catch(_) {}
|
|
725
1039
|
return info;
|
|
726
1040
|
}).catch(err => { if (entry.gen === gen) entry.pendingInfo = null; return null; });
|
|
727
1041
|
await entry.pendingInfo; // ensure cachedInfo is set before app.js continues
|
|
728
1042
|
};
|
|
729
1043
|
result.authManagerLogout = async function(uuid, authInfoJson) {
|
|
730
1044
|
const e = _authMgr.get(uuid);
|
|
731
|
-
if (e) { e.cachedInfo = null; e.pendingInfo = null; e.cachedToken = null; e.cachedHost = null; }
|
|
1045
|
+
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
1046
|
return true;
|
|
733
1047
|
};
|
|
734
1048
|
result.authManagerRefreshCopilotUser = async function(uuid) {
|
|
@@ -736,20 +1050,23 @@ Module._load = function (request, parent, isMain) {
|
|
|
736
1050
|
};
|
|
737
1051
|
result.authManagerDestroy = function(uuid) { _authMgr.delete(uuid); };
|
|
738
1052
|
|
|
739
|
-
result.authResolveAuthInfoFromToken
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1053
|
+
if (!isGlibcMode && typeof result.authResolveAuthInfoFromToken === 'function') {
|
|
1054
|
+
result.authResolveAuthInfoFromToken = async function(token, hostUri, skipCache, userAgent) {
|
|
1055
|
+
if (!token) return JSON.stringify(null);
|
|
1056
|
+
let login = null;
|
|
1057
|
+
try {
|
|
1058
|
+
const apiHost = (hostUri || 'https://github.com').replace('://github.com', '://api.github.com');
|
|
1059
|
+
const res = await globalThis.fetch(`${apiHost}/user`, {
|
|
1060
|
+
headers: { Authorization: `token ${token}`, 'User-Agent': userAgent || `copilot-termux/${_pkgVersion}` },
|
|
1061
|
+
signal: AbortSignal.timeout(5000),
|
|
1062
|
+
});
|
|
1063
|
+
if (res.ok) login = (await res.json()).login;
|
|
1064
|
+
} catch (_) {}
|
|
1065
|
+
const host = hostUri || 'https://github.com';
|
|
1066
|
+
return JSON.stringify({ type: 'token', host, token, login, copilotUser: null });
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
// glibc mode では native authResolveAuthInfoFromToken を使う(SSL_CERT_FILE で TLS 修正済み)
|
|
753
1070
|
// === end authManager* stubs ===
|
|
754
1071
|
// === tokenStore* JS stubs (bionic: tokio ThreadsafeFunction crash) ===
|
|
755
1072
|
// Verified tokens: set by authManagerLoginUser / _resolveOrCache after /user API check
|
|
@@ -784,6 +1101,7 @@ Module._load = function (request, parent, isMain) {
|
|
|
784
1101
|
result.tokenStoreStoreCurrentTokenInConfig = async function() {};
|
|
785
1102
|
// === end tokenStore* stubs ===
|
|
786
1103
|
|
|
1104
|
+
if (!isGlibcMode) {
|
|
787
1105
|
// === urlManager* JS stubs ===
|
|
788
1106
|
const _urlMgr = new Map();
|
|
789
1107
|
let _urlSeq = 8e6;
|
|
@@ -933,6 +1251,7 @@ Module._load = function (request, parent, isMain) {
|
|
|
933
1251
|
return { converged: true, applied: false, updated: false };
|
|
934
1252
|
};
|
|
935
1253
|
// === end ifcEngine* stubs ===
|
|
1254
|
+
}
|
|
936
1255
|
// --- end JS model HTTP implementation ---
|
|
937
1256
|
}
|
|
938
1257
|
|
|
@@ -962,23 +1281,22 @@ Module._load = function (request, parent, isMain) {
|
|
|
962
1281
|
// 差し替えるのを阻止する。linuxmusl-arm64/runtime.node の Rust ネットワークスタックは
|
|
963
1282
|
// bionic 上の実 I/O で動作しないため、Node.js ビルトイン fetch(動作確認済み)に固定する。
|
|
964
1283
|
// GitHub OAuth token via env var or gh CLI (keychain unavailable on bionic)
|
|
965
|
-
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1284
|
+
|
|
1285
|
+
// glibc modeでも維持(isGlibcMode分岐なし、かつファイルスコープのためそもそも分岐不可):
|
|
1286
|
+
// JS実装のnetworkFetch*/modelHttp*/capiClientListModelsがglobalThis.fetchに依存しているため、
|
|
1287
|
+
// ここだけglibc modeでnative fetchに戻すと、JSスタブがRust fetch経由になり前提が崩れる。
|
|
1288
|
+
|
|
1289
|
+
const _COPILOT_TOKEN_DEFAULT_TTL_MS = 28 * 60 * 1000;
|
|
1290
|
+
function _normalizeCopilotTokenExpiry(expiresAt, now = Date.now()) {
|
|
1291
|
+
const parsed = expiresAt ? Date.parse(expiresAt) : NaN;
|
|
1292
|
+
return Number.isFinite(parsed) && parsed > now ? parsed : now + _COPILOT_TOKEN_DEFAULT_TTL_MS;
|
|
972
1293
|
}
|
|
973
|
-
function _tokenHash(t) { return t ? t.slice(0, 8) + '...' : null; }
|
|
974
|
-
// --- end 計装 ---
|
|
975
1294
|
|
|
976
1295
|
async function _readGhToken(env) {
|
|
977
1296
|
const envToken =
|
|
978
1297
|
(env && (env.GITHUB_TOKEN || env.GH_TOKEN || env.COPILOT_GITHUB_TOKEN)) ||
|
|
979
1298
|
process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.COPILOT_GITHUB_TOKEN;
|
|
980
1299
|
if (envToken) {
|
|
981
|
-
_dbg('readGhToken', { source: 'env', tokenHash: _tokenHash(envToken) });
|
|
982
1300
|
return envToken;
|
|
983
1301
|
}
|
|
984
1302
|
try {
|
|
@@ -988,10 +1306,8 @@ async function _readGhToken(env) {
|
|
|
988
1306
|
resolve(err ? null : (stdout.trim() || null));
|
|
989
1307
|
});
|
|
990
1308
|
});
|
|
991
|
-
_dbg('readGhToken', { source: ghToken ? 'gh-cli' : 'null', tokenHash: _tokenHash(ghToken) });
|
|
992
1309
|
return ghToken;
|
|
993
1310
|
} catch (_) {
|
|
994
|
-
_dbg('readGhToken', { source: 'error', tokenHash: null });
|
|
995
1311
|
return null;
|
|
996
1312
|
}
|
|
997
1313
|
}
|
|
@@ -1003,3 +1319,117 @@ Object.defineProperty(globalThis, 'fetch', {
|
|
|
1003
1319
|
get() { return _nativeFetch; },
|
|
1004
1320
|
set(_) { /* B7 代入を無視 */ },
|
|
1005
1321
|
});
|
|
1322
|
+
|
|
1323
|
+
// UPDATE-001 / UPDATE-003: 公式 GitHub Copilot CLI (@github/copilot) の app.js に対するパッチ。
|
|
1324
|
+
// (1) `/update` が表示するインストールコマンド文字列を fork のパッケージ名に差し替える (UPDATE-001)
|
|
1325
|
+
// (2) upstream 公式リポジトリ (github/copilot-cli) のリリースチェックに基づく起動時通知バナーを無効化する (UPDATE-003)
|
|
1326
|
+
// app.js は ~/.copilot-termux/<version>/package.json の "type":"module" により ESM としてロードされる。
|
|
1327
|
+
// CJS専用の Module._extensions['.js'] はESMコンパイルに一切関与しないため機能しない
|
|
1328
|
+
// (2026-07-02 実機再現で確認済み)。正しい介入点は node:module の registerHooks()
|
|
1329
|
+
// (Node v22.15.0/v23.5.0+ で追加された同期 ESM Loader Hook)。未対応のNodeではフィーチャー検出で
|
|
1330
|
+
// スキップし、パッチなしでフォールバックする。
|
|
1331
|
+
const { fileURLToPath } = require('url');
|
|
1332
|
+
|
|
1333
|
+
function isTargetCopilotAppJsUrl(url) {
|
|
1334
|
+
if (typeof url !== 'string' || !url.startsWith('file://')) return false;
|
|
1335
|
+
let filename;
|
|
1336
|
+
try {
|
|
1337
|
+
filename = fileURLToPath(url);
|
|
1338
|
+
} catch (_) {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
// app.js が `.copilot-termux/<version-or-current>/app.js` に直接あることを要求する
|
|
1342
|
+
// (codex STEP8 指摘: basename + セグメント包含だけだと
|
|
1343
|
+
// `.copilot-termux/<version>/node_modules/**/app.js` のような無関係な深い階層にも
|
|
1344
|
+
// 誤反応しうるため、`.copilot-termux` の直後2セグメント目である場合のみ許可する)
|
|
1345
|
+
if (path.basename(filename) !== 'app.js') return false;
|
|
1346
|
+
const segments = filename.split(/[\\/]/);
|
|
1347
|
+
const idx = segments.indexOf('.copilot-termux');
|
|
1348
|
+
if (idx === -1) return false;
|
|
1349
|
+
return idx + 2 === segments.length - 1;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function patchAppJsSource(source) {
|
|
1353
|
+
let patched = source;
|
|
1354
|
+
|
|
1355
|
+
const INSTALL_CMD_PATTERN = /`npm i -g @github\/copilot@\$\{[^}]+\}`/g;
|
|
1356
|
+
const installMatches = patched.match(INSTALL_CMD_PATTERN);
|
|
1357
|
+
if (installMatches && installMatches.length === 1) {
|
|
1358
|
+
patched = patched.replace(INSTALL_CMD_PATTERN, '`npm install -g @bash0816/copilot-termux`');
|
|
1359
|
+
} else {
|
|
1360
|
+
console.warn('[copilot-termux] UPDATE-001: update string pattern ' +
|
|
1361
|
+
(installMatches ? 'found ' + installMatches.length + ' times' : 'not found') + ', skipping patch');
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
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;
|
|
1365
|
+
const notifyMatches = patched.match(NOTIFY_PATTERN);
|
|
1366
|
+
if (notifyMatches && notifyMatches.length === 1) {
|
|
1367
|
+
patched = patched.replace(NOTIFY_PATTERN, 'false');
|
|
1368
|
+
} else {
|
|
1369
|
+
console.warn('[copilot-termux] UPDATE-003: upstream release notification pattern ' +
|
|
1370
|
+
(notifyMatches ? 'found ' + notifyMatches.length + ' times' : 'not found') + ', skipping patch');
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// UPDATE-006: DO() 内の releases/latest 取得先をフォーク自身の npm dist-tag latest に差し替える。
|
|
1374
|
+
// 変数名 o,n はDO()固有(a$e()は a,r を使うため誤爆しない)。
|
|
1375
|
+
// changelog本文の取得(nj.execute / fetchReleaseByTag、owner:"github")は一切変更しない。
|
|
1376
|
+
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;
|
|
1377
|
+
const forkLatestMatches = patched.match(FORK_LATEST_PATTERN);
|
|
1378
|
+
if (forkLatestMatches && forkLatestMatches.length === 1) {
|
|
1379
|
+
patched = patched.replace(FORK_LATEST_PATTERN,
|
|
1380
|
+
'return await(async()=>{try{' +
|
|
1381
|
+
'const res=await fetch("https://registry.npmjs.org/%40bash0816%2Fcopilot-termux/latest",{signal:AbortSignal.timeout(5000)});' +
|
|
1382
|
+
'if(!res.ok)throw new Error("npm registry returned "+res.status);' +
|
|
1383
|
+
'const data=await res.json();' +
|
|
1384
|
+
'if(!data||typeof data.version!=="string")throw new Error("npm registry response missing version");' +
|
|
1385
|
+
'const ver=data.version.replace(/-\\d+$/,"");' +
|
|
1386
|
+
'if(!/^\\d+\\.\\d+\\.\\d+$/.test(ver))throw new Error("invalid version: "+data.version);' +
|
|
1387
|
+
'return{tag_name:"v"+ver,assets:[]};' +
|
|
1388
|
+
'}catch(e){return{error:String(e)};}})()'
|
|
1389
|
+
);
|
|
1390
|
+
} else {
|
|
1391
|
+
console.warn('[copilot-termux] UPDATE-006: fork latest pattern ' +
|
|
1392
|
+
(forkLatestMatches ? 'found ' + forkLatestMatches.length + ' times' : 'not found') + ', skipping patch');
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// UPDATE-006b: 「更新なし」の場合のchangelog表示をcurrent(a)からfork-latest(u)に変更する。
|
|
1396
|
+
// upstreamは「更新なし → nj.execute(t,[a])」(aは現在バージョン)だが、
|
|
1397
|
+
// フォーク自身のnpm latestを判定元にした結果、currentがfork-latestより新しい場合でも
|
|
1398
|
+
// aのchangelogが出続ける問題を解消する。
|
|
1399
|
+
// nj.execute内部(fetchReleaseByTag / owner:"github" / changelog.json)は変更しない。
|
|
1400
|
+
const NO_UPDATE_PATTERN = /if\(!ELt\.default\.gt\(u,a\)\)return nj\.execute\(t,\[a\]\)/g;
|
|
1401
|
+
const noUpdateMatches = patched.match(NO_UPDATE_PATTERN);
|
|
1402
|
+
if (noUpdateMatches && noUpdateMatches.length === 1) {
|
|
1403
|
+
patched = patched.replace(NO_UPDATE_PATTERN,
|
|
1404
|
+
'if(!ELt.default.gt(u,a))return nj.execute(t,[u.replace(/^v/,"")])'
|
|
1405
|
+
);
|
|
1406
|
+
} else {
|
|
1407
|
+
console.warn('[copilot-termux] UPDATE-006b: no-update changelog pattern ' +
|
|
1408
|
+
(noUpdateMatches ? 'found ' + noUpdateMatches.length + ' times' : 'not found') + ', skipping patch');
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return patched;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
if (!globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__) {
|
|
1415
|
+
globalThis.__COPILOT_TERMUX_ESM_PATCH_REGISTERED__ = true;
|
|
1416
|
+
const { registerHooks } = require('module');
|
|
1417
|
+
if (typeof registerHooks === 'function') {
|
|
1418
|
+
registerHooks({
|
|
1419
|
+
load(url, context, nextLoad) {
|
|
1420
|
+
const result = nextLoad(url, context);
|
|
1421
|
+
if (!isTargetCopilotAppJsUrl(url)) return result;
|
|
1422
|
+
if (result.source == null) return result;
|
|
1423
|
+
const wasNonString = typeof result.source !== 'string';
|
|
1424
|
+
const src = wasNonString ? Buffer.from(result.source).toString('utf8') : result.source;
|
|
1425
|
+
const patched = patchAppJsSource(src);
|
|
1426
|
+
return Object.assign({}, result, { source: patched });
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
} else {
|
|
1430
|
+
console.warn('[copilot-termux] UPDATE-001/003: node:module registerHooks() not available on this Node version (' + process.version + '), skipping app.js patch');
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
module.exports.patchAppJsSource = patchAppJsSource;
|
|
1435
|
+
module.exports.isTargetCopilotAppJsUrl = isTargetCopilotAppJsUrl;
|