@algochad/archcoder 2.0.2
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 +113 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +5108 -0
- package/bin/cli.test.js +56 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +67 -0
- package/dist/assets/MultiRunWindow-BZp3MjJP.js +1 -0
- package/dist/assets/SettingsWindow-DoGYXpX7.js +1 -0
- package/dist/assets/TerminalView-BN7BR5Ff.js +3 -0
- package/dist/assets/TimelineDialog-ZQ33oVQR.js +1 -0
- package/dist/assets/ToolOutputDialog-Blv3pnug.js +16 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-CtCEGYrr.css +1 -0
- package/dist/assets/index-o_d2wtWC.js +48 -0
- package/dist/assets/main-5QGBtzdq.css +1 -0
- package/dist/assets/main-B6oiMU86.js +8033 -0
- package/dist/assets/vendor--DbVqbJpV.css +1 -0
- package/dist/assets/vendor-.bun-HTKwyaEM.js +10086 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/worker-bqd4RMrj.js +155 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +67 -0
- package/dist/index.html +533 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.svg +16 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.svg +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +22 -0
- package/dist/sw.js +1 -0
- package/package.json +107 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +67 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +67 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.svg +16 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.svg +16 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +22 -0
- package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
- package/server/index.d.ts +37 -0
- package/server/index.js +14694 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/git/DOCUMENTATION.md +146 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +110 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/service.js +3117 -0
- package/server/lib/github/DOCUMENTATION.md +170 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +478 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/installer/desktop.js +289 -0
- package/server/lib/installer/download.js +208 -0
- package/server/lib/installer/index.js +45 -0
- package/server/lib/installer/platform.js +100 -0
- package/server/lib/notifications/DOCUMENTATION.md +61 -0
- package/server/lib/notifications/index.js +1 -0
- package/server/lib/notifications/message.js +49 -0
- package/server/lib/notifications/message.test.js +59 -0
- package/server/lib/opencode/DOCUMENTATION.md +59 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth.js +81 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/mcp.js +206 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/shared.js +527 -0
- package/server/lib/opencode/skills.js +480 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/ui-auth.js +510 -0
- package/server/lib/package-manager.js +505 -0
- package/server/lib/quota/DOCUMENTATION.md +55 -0
- package/server/lib/quota/index.js +24 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +152 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-shared.js +136 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/utils/auth.js +46 -0
- package/server/lib/quota/utils/formatters.js +76 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +32 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +85 -0
- package/server/lib/terminal/DOCUMENTATION.md +114 -0
- package/server/lib/terminal/index.js +12 -0
- package/server/lib/terminal/input-ws-protocol.js +66 -0
- package/server/lib/terminal/input-ws-protocol.test.js +138 -0
- package/server/lib/tts/DOCUMENTATION.md +134 -0
- package/server/lib/tts/index.js +16 -0
- package/server/lib/tts/service.js +162 -0
- package/server/lib/tts/summarization.js +171 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/utils/lru.js +107 -0
- package/server/lib/utils/sse.js +121 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const PACKAGE_NAME = '@algochad/archcoder';
|
|
12
|
+
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
|
13
|
+
const CHANGELOG_URL = 'https://raw.githubusercontent.com/archcoder/archcoder/main/CHANGELOG.md';
|
|
14
|
+
let cachedDetectedPm = null;
|
|
15
|
+
|
|
16
|
+
function getSpawnSyncBaseOptions() {
|
|
17
|
+
return process.platform === 'win32' ? { windowsHide: true } : {};
|
|
18
|
+
}
|
|
19
|
+
const UPDATE_CHECK_URL = process.env.ARCHCODER_UPDATE_API_URL || 'https://api.archcoder.archlast.com/v1/update/check';
|
|
20
|
+
|
|
21
|
+
function getArchCoderConfigDir() {
|
|
22
|
+
if (process.platform === 'win32') {
|
|
23
|
+
const appData = process.env.APPDATA;
|
|
24
|
+
if (appData) return path.join(appData, 'archcoder');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return path.join(os.homedir(), '.config', 'archcoder');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sanitizeInstallScope(scope) {
|
|
31
|
+
if (scope === 'desktop-tauri' || scope === 'vscode' || scope === 'web') return scope;
|
|
32
|
+
return 'web';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getOrCreateInstallId(scope = 'web') {
|
|
36
|
+
const configDir = getArchCoderConfigDir();
|
|
37
|
+
const normalizedScope = sanitizeInstallScope(scope);
|
|
38
|
+
const idPath = path.join(configDir, `install-id-${normalizedScope}`);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const existing = fs.readFileSync(idPath, 'utf8').trim();
|
|
42
|
+
if (existing) return existing;
|
|
43
|
+
} catch {
|
|
44
|
+
// Generate new id.
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const installId = crypto.randomUUID();
|
|
48
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(idPath, `${installId}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
50
|
+
return installId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mapPlatform(value) {
|
|
54
|
+
if (value === 'darwin') return 'macos';
|
|
55
|
+
if (value === 'win32') return 'windows';
|
|
56
|
+
if (value === 'linux') return 'linux';
|
|
57
|
+
return 'web';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mapArch(value) {
|
|
61
|
+
if (value === 'arm64' || value === 'aarch64') return 'arm64';
|
|
62
|
+
if (value === 'x64' || value === 'amd64') return 'x64';
|
|
63
|
+
return 'unknown';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeAppType(value) {
|
|
67
|
+
if (value === 'web' || value === 'desktop-tauri' || value === 'vscode') return value;
|
|
68
|
+
return 'web';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeDeviceClass(value) {
|
|
72
|
+
if (value === 'mobile' || value === 'tablet' || value === 'desktop' || value === 'unknown') return value;
|
|
73
|
+
return 'unknown';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizePlatform(value) {
|
|
77
|
+
if (value === 'macos' || value === 'windows' || value === 'linux' || value === 'web') return value;
|
|
78
|
+
return mapPlatform(process.platform);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeArch(value) {
|
|
82
|
+
if (value === 'arm64' || value === 'x64' || value === 'unknown') return value;
|
|
83
|
+
return mapArch(process.arch);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
|
87
|
+
try {
|
|
88
|
+
const appType = normalizeAppType(options.appType);
|
|
89
|
+
const hostPlatform = mapPlatform(process.platform);
|
|
90
|
+
const hostArch = mapArch(process.arch);
|
|
91
|
+
const platform = appType === 'vscode' ? normalizePlatform(options.platform) : hostPlatform;
|
|
92
|
+
const arch = appType === 'vscode' ? normalizeArch(options.arch) : hostArch;
|
|
93
|
+
const payload = {
|
|
94
|
+
appType,
|
|
95
|
+
deviceClass: normalizeDeviceClass(options.deviceClass),
|
|
96
|
+
platform,
|
|
97
|
+
arch,
|
|
98
|
+
channel: 'stable',
|
|
99
|
+
currentVersion,
|
|
100
|
+
installId: getOrCreateInstallId(appType),
|
|
101
|
+
instanceMode: options.instanceMode || 'unknown',
|
|
102
|
+
reportUsage: options.reportUsage !== false,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const response = await fetch(UPDATE_CHECK_URL, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
Accept: 'application/json',
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify(payload),
|
|
112
|
+
signal: AbortSignal.timeout(10000),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!response.ok) return null;
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
if (typeof data?.latestVersion !== 'string') return null;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
available: Boolean(data.updateAvailable),
|
|
121
|
+
version: data.latestVersion,
|
|
122
|
+
currentVersion,
|
|
123
|
+
body: typeof data.releaseNotes === 'string' ? data.releaseNotes : undefined,
|
|
124
|
+
nextSuggestedCheckInSec:
|
|
125
|
+
typeof data.nextSuggestedCheckInSec === 'number' && Number.isFinite(data.nextSuggestedCheckInSec)
|
|
126
|
+
? data.nextSuggestedCheckInSec
|
|
127
|
+
: undefined,
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Detect which package manager was used to install this package.
|
|
136
|
+
* Strategy:
|
|
137
|
+
* 1. Check npm_config_user_agent (set during npm/pnpm/yarn/bun install)
|
|
138
|
+
* 2. Check npm_execpath for PM binary path
|
|
139
|
+
* 3. Analyze package location path for PM-specific patterns
|
|
140
|
+
* 4. Fall back to npm
|
|
141
|
+
*/
|
|
142
|
+
export function detectPackageManager() {
|
|
143
|
+
if (cachedDetectedPm) {
|
|
144
|
+
return cachedDetectedPm;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const forcedPm = process.env.OPENCHAMBER_PACKAGE_MANAGER?.trim();
|
|
148
|
+
if (forcedPm && ['npm', 'pnpm', 'yarn', 'bun'].includes(forcedPm)) {
|
|
149
|
+
const forcedPmCommand = resolvePackageManagerCommand(forcedPm);
|
|
150
|
+
if (isCommandAvailable(forcedPmCommand)) {
|
|
151
|
+
cachedDetectedPm = forcedPm;
|
|
152
|
+
return cachedDetectedPm;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Strategy 1: Detect from runtime executable path (reliable for server-side updates)
|
|
157
|
+
const runtimePm = detectPackageManagerFromRuntimePath(process.execPath);
|
|
158
|
+
if (runtimePm && isCommandAvailable(resolvePackageManagerCommand(runtimePm))) {
|
|
159
|
+
cachedDetectedPm = runtimePm;
|
|
160
|
+
return cachedDetectedPm;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Strategy 2: Check user agent (most reliable during install)
|
|
164
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
165
|
+
let hintedPm = null;
|
|
166
|
+
if (userAgent.startsWith('pnpm')) hintedPm = 'pnpm';
|
|
167
|
+
else if (userAgent.startsWith('yarn')) hintedPm = 'yarn';
|
|
168
|
+
else if (userAgent.startsWith('bun')) hintedPm = 'bun';
|
|
169
|
+
else if (userAgent.startsWith('npm')) hintedPm = 'npm';
|
|
170
|
+
|
|
171
|
+
// Strategy 3: Check execpath
|
|
172
|
+
const execPath = process.env.npm_execpath || '';
|
|
173
|
+
if (!hintedPm) {
|
|
174
|
+
if (execPath.includes('pnpm')) hintedPm = 'pnpm';
|
|
175
|
+
else if (execPath.includes('yarn')) hintedPm = 'yarn';
|
|
176
|
+
else if (execPath.includes('bun')) hintedPm = 'bun';
|
|
177
|
+
else if (execPath.includes('npm')) hintedPm = 'npm';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Strategy 4: Detect from invoked binary path (works for bun global symlink installs)
|
|
181
|
+
const invokedPm = detectPackageManagerFromInvocationPath(process.argv?.[1]);
|
|
182
|
+
if (invokedPm && isCommandAvailable(resolvePackageManagerCommand(invokedPm))) {
|
|
183
|
+
cachedDetectedPm = invokedPm;
|
|
184
|
+
return cachedDetectedPm;
|
|
185
|
+
}
|
|
186
|
+
if (!hintedPm) {
|
|
187
|
+
hintedPm = invokedPm;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Strategy 5: Analyze package location for PM-specific patterns
|
|
191
|
+
try {
|
|
192
|
+
const pkgPath = path.resolve(__dirname, '..', '..');
|
|
193
|
+
const pmFromPath = detectPackageManagerFromInstallPath(pkgPath);
|
|
194
|
+
if (pmFromPath && isCommandAvailable(resolvePackageManagerCommand(pmFromPath))) {
|
|
195
|
+
cachedDetectedPm = pmFromPath;
|
|
196
|
+
return cachedDetectedPm;
|
|
197
|
+
}
|
|
198
|
+
if (!hintedPm) {
|
|
199
|
+
hintedPm = pmFromPath;
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
// Ignore path resolution errors
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Validate the hinted PM actually owns the global install.
|
|
206
|
+
// This avoids false positives (for example running via bunx while installed with npm).
|
|
207
|
+
if (hintedPm && isCommandAvailable(resolvePackageManagerCommand(hintedPm)) && isPackageInstalledWith(hintedPm)) {
|
|
208
|
+
cachedDetectedPm = hintedPm;
|
|
209
|
+
return cachedDetectedPm;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Strategy 6: Check which PM binaries are available and preferred
|
|
213
|
+
const pmChecks = [
|
|
214
|
+
{ name: 'pnpm', check: () => isCommandAvailable(resolvePackageManagerCommand('pnpm')) },
|
|
215
|
+
{ name: 'yarn', check: () => isCommandAvailable(resolvePackageManagerCommand('yarn')) },
|
|
216
|
+
{ name: 'bun', check: () => isCommandAvailable(resolvePackageManagerCommand('bun')) },
|
|
217
|
+
{ name: 'npm', check: () => isCommandAvailable(resolvePackageManagerCommand('npm')) },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
for (const { name, check } of pmChecks) {
|
|
221
|
+
if (check()) {
|
|
222
|
+
// Verify this PM actually has the package installed globally
|
|
223
|
+
if (isPackageInstalledWith(name)) {
|
|
224
|
+
cachedDetectedPm = name;
|
|
225
|
+
return cachedDetectedPm;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
cachedDetectedPm = 'npm';
|
|
231
|
+
return cachedDetectedPm;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function detectPackageManagerFromInstallPath(pkgPath) {
|
|
235
|
+
if (!pkgPath) return null;
|
|
236
|
+
const normalized = pkgPath.replace(/\\/g, '/').toLowerCase();
|
|
237
|
+
if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/')) return 'pnpm';
|
|
238
|
+
if (normalized.includes('/.yarn/')) return 'yarn';
|
|
239
|
+
if (normalized.includes('/.bun/') || normalized.includes('/bun/install/')) return 'bun';
|
|
240
|
+
if (normalized.includes('/node_modules/')) return 'npm';
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function detectPackageManagerFromRuntimePath(runtimePath) {
|
|
245
|
+
if (!runtimePath || typeof runtimePath !== 'string') return null;
|
|
246
|
+
const normalized = runtimePath.replace(/\\/g, '/').toLowerCase();
|
|
247
|
+
if (normalized.includes('/.bun/bin/bun') || normalized.endsWith('/bun') || normalized.endsWith('/bun.exe')) {
|
|
248
|
+
return 'bun';
|
|
249
|
+
}
|
|
250
|
+
if (normalized.includes('/pnpm/')) return 'pnpm';
|
|
251
|
+
if (normalized.includes('/yarn/')) return 'yarn';
|
|
252
|
+
if (normalized.includes('/node') || normalized.endsWith('/node.exe')) return 'npm';
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function detectPackageManagerFromInvocationPath(invokedPath) {
|
|
257
|
+
if (!invokedPath || typeof invokedPath !== 'string') return null;
|
|
258
|
+
const normalized = invokedPath.replace(/\\/g, '/').toLowerCase();
|
|
259
|
+
if (normalized.includes('/.bun/bin/')) return 'bun';
|
|
260
|
+
if (normalized.includes('/.pnpm/')) return 'pnpm';
|
|
261
|
+
if (normalized.includes('/.yarn/')) return 'yarn';
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getPackageManagerCommandCandidates(pm) {
|
|
266
|
+
const candidates = [];
|
|
267
|
+
if (pm === 'bun') {
|
|
268
|
+
const bunExecutable = process.platform === 'win32' ? 'bun.exe' : 'bun';
|
|
269
|
+
if (process.env.BUN_INSTALL) {
|
|
270
|
+
candidates.push(path.join(process.env.BUN_INSTALL, 'bin', bunExecutable));
|
|
271
|
+
}
|
|
272
|
+
if (process.env.HOME) {
|
|
273
|
+
candidates.push(path.join(process.env.HOME, '.bun', 'bin', bunExecutable));
|
|
274
|
+
}
|
|
275
|
+
if (process.env.USERPROFILE) {
|
|
276
|
+
candidates.push(path.join(process.env.USERPROFILE, '.bun', 'bin', bunExecutable));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
candidates.push(pm);
|
|
280
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resolvePackageManagerCommand(pm) {
|
|
284
|
+
const candidates = getPackageManagerCommandCandidates(pm);
|
|
285
|
+
for (const candidate of candidates) {
|
|
286
|
+
if (isCommandAvailable(candidate)) {
|
|
287
|
+
return candidate;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return pm;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function quoteCommand(command) {
|
|
294
|
+
if (!command) return command;
|
|
295
|
+
if (!/\s/.test(command)) return command;
|
|
296
|
+
if (process.platform === 'win32') {
|
|
297
|
+
return `"${command.replace(/"/g, '""')}"`;
|
|
298
|
+
}
|
|
299
|
+
return `'${command.replace(/'/g, "'\\''")}'`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isCommandAvailable(command) {
|
|
303
|
+
try {
|
|
304
|
+
const result = spawnSync(command, ['--version'], {
|
|
305
|
+
encoding: 'utf8',
|
|
306
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
307
|
+
timeout: 5000,
|
|
308
|
+
...getSpawnSyncBaseOptions(),
|
|
309
|
+
});
|
|
310
|
+
return result.status === 0;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isPackageInstalledWith(pm) {
|
|
317
|
+
try {
|
|
318
|
+
const pmCommand = resolvePackageManagerCommand(pm);
|
|
319
|
+
let args;
|
|
320
|
+
switch (pm) {
|
|
321
|
+
case 'pnpm':
|
|
322
|
+
args = ['list', '-g', '--depth=0', PACKAGE_NAME];
|
|
323
|
+
break;
|
|
324
|
+
case 'yarn':
|
|
325
|
+
args = ['global', 'list', '--depth=0'];
|
|
326
|
+
break;
|
|
327
|
+
case 'bun':
|
|
328
|
+
args = ['pm', 'ls', '-g'];
|
|
329
|
+
break;
|
|
330
|
+
default:
|
|
331
|
+
args = ['list', '-g', '--depth=0', PACKAGE_NAME];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const result = spawnSync(pmCommand, args, {
|
|
335
|
+
encoding: 'utf8',
|
|
336
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
337
|
+
timeout: 10000,
|
|
338
|
+
...getSpawnSyncBaseOptions(),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (result.status !== 0) return false;
|
|
342
|
+
return result.stdout.includes(PACKAGE_NAME) || result.stdout.includes('archcoder');
|
|
343
|
+
} catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get the update command for the detected package manager
|
|
350
|
+
*/
|
|
351
|
+
export function getUpdateCommand(pm = detectPackageManager()) {
|
|
352
|
+
const pmCommand = quoteCommand(resolvePackageManagerCommand(pm));
|
|
353
|
+
switch (pm) {
|
|
354
|
+
case 'pnpm':
|
|
355
|
+
return `${pmCommand} add -g ${PACKAGE_NAME}@latest`;
|
|
356
|
+
case 'yarn':
|
|
357
|
+
return `${pmCommand} global add ${PACKAGE_NAME}@latest`;
|
|
358
|
+
case 'bun':
|
|
359
|
+
return `${pmCommand} add -g ${PACKAGE_NAME}@latest`;
|
|
360
|
+
default:
|
|
361
|
+
return `${pmCommand} install -g ${PACKAGE_NAME}@latest`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get current installed version from package.json
|
|
367
|
+
*/
|
|
368
|
+
export function getCurrentVersion() {
|
|
369
|
+
try {
|
|
370
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
371
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
372
|
+
return pkg.version || 'unknown';
|
|
373
|
+
} catch {
|
|
374
|
+
return 'unknown';
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Fetch latest version from npm registry
|
|
380
|
+
*/
|
|
381
|
+
export async function getLatestVersion() {
|
|
382
|
+
try {
|
|
383
|
+
const response = await fetch(NPM_REGISTRY_URL, {
|
|
384
|
+
headers: { Accept: 'application/json' },
|
|
385
|
+
signal: AbortSignal.timeout(10000),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
throw new Error(`Registry responded with ${response.status}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const data = await response.json();
|
|
393
|
+
return data['dist-tags']?.latest || null;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Parse semver version to numeric for comparison
|
|
401
|
+
*/
|
|
402
|
+
function parseVersion(version) {
|
|
403
|
+
const parts = version.replace(/^v/, '').split('.').map(Number);
|
|
404
|
+
return (parts[0] || 0) * 10000 + (parts[1] || 0) * 100 + (parts[2] || 0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Fetch changelog notes between versions
|
|
409
|
+
*/
|
|
410
|
+
export async function fetchChangelogNotes(fromVersion, toVersion) {
|
|
411
|
+
try {
|
|
412
|
+
const response = await fetch(CHANGELOG_URL, {
|
|
413
|
+
signal: AbortSignal.timeout(10000),
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (!response.ok) return undefined;
|
|
417
|
+
|
|
418
|
+
const changelog = await response.text();
|
|
419
|
+
const sections = changelog.split(/^## /m).slice(1);
|
|
420
|
+
|
|
421
|
+
const fromNum = parseVersion(fromVersion);
|
|
422
|
+
const toNum = parseVersion(toVersion);
|
|
423
|
+
|
|
424
|
+
const relevantSections = sections.filter((section) => {
|
|
425
|
+
const match = section.match(/^\[(\d+\.\d+\.\d+)\]/);
|
|
426
|
+
if (!match) return false;
|
|
427
|
+
const ver = parseVersion(match[1]);
|
|
428
|
+
return ver > fromNum && ver <= toNum;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (relevantSections.length === 0) return undefined;
|
|
432
|
+
|
|
433
|
+
return relevantSections
|
|
434
|
+
.map((s) => '## ' + s.trim())
|
|
435
|
+
.join('\n\n');
|
|
436
|
+
} catch {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function checkForUpdates(options = {}) {
|
|
442
|
+
const currentVersion = options.currentVersion || getCurrentVersion();
|
|
443
|
+
const pm = detectPackageManager();
|
|
444
|
+
|
|
445
|
+
if (currentVersion !== 'unknown') {
|
|
446
|
+
const remote = await checkForUpdatesFromApi(currentVersion, options);
|
|
447
|
+
if (remote) {
|
|
448
|
+
return {
|
|
449
|
+
...remote,
|
|
450
|
+
packageManager: pm,
|
|
451
|
+
updateCommand: 'archcoder update',
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const latestVersion = await getLatestVersion();
|
|
457
|
+
|
|
458
|
+
if (!latestVersion || currentVersion === 'unknown') {
|
|
459
|
+
return {
|
|
460
|
+
available: false,
|
|
461
|
+
currentVersion,
|
|
462
|
+
error: 'Unable to determine versions',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const currentNum = parseVersion(currentVersion);
|
|
467
|
+
const latestNum = parseVersion(latestVersion);
|
|
468
|
+
const available = latestNum > currentNum;
|
|
469
|
+
let changelog;
|
|
470
|
+
if (available) {
|
|
471
|
+
changelog = await fetchChangelogNotes(currentVersion, latestVersion);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
available,
|
|
476
|
+
version: latestVersion,
|
|
477
|
+
currentVersion,
|
|
478
|
+
body: changelog,
|
|
479
|
+
packageManager: pm,
|
|
480
|
+
// Show our CLI command, not raw package manager command
|
|
481
|
+
updateCommand: 'archcoder update',
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Execute the update (used by CLI)
|
|
487
|
+
*/
|
|
488
|
+
export function executeUpdate(pm = detectPackageManager(), options = {}) {
|
|
489
|
+
const command = getUpdateCommand(pm);
|
|
490
|
+
if (!options?.silent) {
|
|
491
|
+
console.log(`Updating ${PACKAGE_NAME} using ${pm}...`);
|
|
492
|
+
console.log(`Running: ${command}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const result = spawnSync(command, {
|
|
496
|
+
stdio: 'inherit',
|
|
497
|
+
shell: true,
|
|
498
|
+
...getSpawnSyncBaseOptions(),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
success: result.status === 0,
|
|
503
|
+
exitCode: result.status,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Quota Module Documentation
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
This module fetches quota and usage signals for supported providers in the web server runtime.
|
|
5
|
+
|
|
6
|
+
## Entrypoints and structure
|
|
7
|
+
- `packages/web/server/lib/quota/index.js`: public entrypoint imported by `packages/web/server/index.js`.
|
|
8
|
+
- `packages/web/server/lib/quota/providers/index.js`: provider registry, configured-provider list, and provider dispatcher.
|
|
9
|
+
- `packages/web/server/lib/quota/providers/interface.js`: JSDoc provider contract used as implementation reference.
|
|
10
|
+
- `packages/web/server/lib/quota/providers/google/`: Google-specific auth, API, and transform modules.
|
|
11
|
+
- `packages/web/server/lib/quota/utils/`: shared auth, transform, and formatting helpers.
|
|
12
|
+
|
|
13
|
+
## Supported provider IDs (dispatcher)
|
|
14
|
+
|
|
15
|
+
These provider IDs are currently dispatchable via `fetchQuotaForProvider(providerId)` in `packages/web/server/lib/quota/providers/index.js`.
|
|
16
|
+
|
|
17
|
+
| Provider ID | Display name | Module | Auth aliases/keys |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| `claude` | Claude | `providers/claude.js` | `anthropic`, `claude` |
|
|
20
|
+
| `codex` | Codex | `providers/codex.js` | `openai`, `codex`, `chatgpt` |
|
|
21
|
+
| `google` | Google | `providers/google/index.js` | `google`, `google.oauth`, Antigravity accounts file |
|
|
22
|
+
| `github-copilot` | GitHub Copilot | `providers/copilot.js` | `github-copilot`, `copilot` |
|
|
23
|
+
| `github-copilot-addon` | GitHub Copilot Add-on | `providers/copilot.js` | `github-copilot`, `copilot` |
|
|
24
|
+
| `kimi-for-coding` | Kimi for Coding | `providers/kimi.js` | `kimi-for-coding`, `kimi` |
|
|
25
|
+
| `nano-gpt` | NanoGPT | `providers/nanogpt.js` | `nano-gpt`, `nanogpt`, `nano_gpt` |
|
|
26
|
+
| `openrouter` | OpenRouter | `providers/openrouter.js` | `openrouter` |
|
|
27
|
+
| `zai-coding-plan` | z.ai | `providers/zai.js` | `zai-coding-plan`, `zai`, `z.ai` |
|
|
28
|
+
| `minimax-coding-plan` | MiniMax Coding Plan (minimax.io) | `providers/minimax-coding-plan.js` | `minimax-coding-plan` |
|
|
29
|
+
| `minimax-cn-coding-plan` | MiniMax Coding Plan (minimaxi.com) | `providers/minimax-cn-coding-plan.js` | `minimax-cn-coding-plan` |
|
|
30
|
+
| `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Cookie file at `~/.config/ollama-quota/cookie` (raw session cookie string) |
|
|
31
|
+
|
|
32
|
+
## Internal-only provider module
|
|
33
|
+
- `providers/openai.js` exists for logic parity/reuse but is intentionally not registered for dispatcher ID routing.
|
|
34
|
+
|
|
35
|
+
## Response contract
|
|
36
|
+
All providers should return results via shared helpers to preserve API shape:
|
|
37
|
+
- Required fields: `providerId`, `providerName`, `ok`, `configured`, `usage`, `fetchedAt`
|
|
38
|
+
- Optional field: `error`
|
|
39
|
+
- Unsupported provider requests should return `ok: false`, `configured: false`, `error: Unsupported provider`
|
|
40
|
+
|
|
41
|
+
## Add a new provider (quick steps)
|
|
42
|
+
1. Choose module shape based on complexity:
|
|
43
|
+
- Simple providers: create `packages/web/server/lib/quota/providers/<provider>.js`.
|
|
44
|
+
- Complex providers (multi-source auth, multiple API calls, non-trivial transforms): create `packages/web/server/lib/quota/providers/<provider>/` with split modules like Google (`index.js`, `auth.js`, `api.js`, `transforms.js`).
|
|
45
|
+
2. Export `providerId`, `providerName`, `aliases`, `isConfigured`, and `fetchQuota`.
|
|
46
|
+
3. Use shared helpers from `packages/web/server/lib/quota/utils/index.js` (`buildResult`, `toUsageWindow`, auth/conversion helpers) to keep payload shape consistent.
|
|
47
|
+
4. Register the provider in `packages/web/server/lib/quota/providers/index.js`.
|
|
48
|
+
5. If needed for direct use, export a named fetcher from `packages/web/server/lib/quota/providers/index.js` and `packages/web/server/lib/quota/index.js`.
|
|
49
|
+
6. Update this file with the new provider ID, module path, and alias/auth details.
|
|
50
|
+
7. Validate with `bun run type-check`, `bun run lint`, and `bun run build`.
|
|
51
|
+
|
|
52
|
+
## Notes for contributors
|
|
53
|
+
- Keep provider IDs stable; clients use them directly.
|
|
54
|
+
- Avoid adding alias-based dispatch in `fetchQuotaForProvider`; dispatch currently expects exact provider IDs.
|
|
55
|
+
- Keep Google behavior changes isolated and review `providers/google/*` together.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota module
|
|
3
|
+
*
|
|
4
|
+
* Provides quota usage tracking for various AI provider services.
|
|
5
|
+
* @module quota
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
listConfiguredQuotaProviders,
|
|
10
|
+
fetchQuotaForProvider,
|
|
11
|
+
fetchClaudeQuota,
|
|
12
|
+
fetchOpenaiQuota,
|
|
13
|
+
fetchGoogleQuota,
|
|
14
|
+
fetchCodexQuota,
|
|
15
|
+
fetchCopilotQuota,
|
|
16
|
+
fetchCopilotAddonQuota,
|
|
17
|
+
fetchKimiQuota,
|
|
18
|
+
fetchOpenRouterQuota,
|
|
19
|
+
fetchZaiQuota,
|
|
20
|
+
fetchNanoGptQuota,
|
|
21
|
+
fetchMinimaxCodingPlanQuota,
|
|
22
|
+
fetchMinimaxCnCodingPlanQuota,
|
|
23
|
+
fetchOllamaCloudQuota
|
|
24
|
+
} from './providers/index.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
2
|
+
import {
|
|
3
|
+
getAuthEntry,
|
|
4
|
+
normalizeAuthEntry,
|
|
5
|
+
buildResult,
|
|
6
|
+
toUsageWindow,
|
|
7
|
+
toNumber,
|
|
8
|
+
toTimestamp
|
|
9
|
+
} from '../utils/index.js';
|
|
10
|
+
|
|
11
|
+
export const providerId = 'claude';
|
|
12
|
+
export const providerName = 'Claude';
|
|
13
|
+
export const aliases = ['anthropic', 'claude'];
|
|
14
|
+
|
|
15
|
+
export const isConfigured = () => {
|
|
16
|
+
const auth = readAuthFile();
|
|
17
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
18
|
+
return Boolean(entry?.access || entry?.token);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const fetchQuota = async () => {
|
|
22
|
+
const auth = readAuthFile();
|
|
23
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
24
|
+
const accessToken = entry?.access ?? entry?.token;
|
|
25
|
+
|
|
26
|
+
if (!accessToken) {
|
|
27
|
+
return buildResult({
|
|
28
|
+
providerId,
|
|
29
|
+
providerName,
|
|
30
|
+
ok: false,
|
|
31
|
+
configured: false,
|
|
32
|
+
error: 'Not configured'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${accessToken}`,
|
|
41
|
+
'anthropic-beta': 'oauth-2025-04-20'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
return buildResult({
|
|
47
|
+
providerId,
|
|
48
|
+
providerName,
|
|
49
|
+
ok: false,
|
|
50
|
+
configured: true,
|
|
51
|
+
error: `API error: ${response.status}`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const payload = await response.json();
|
|
56
|
+
const windows = {};
|
|
57
|
+
const fiveHour = payload?.five_hour ?? null;
|
|
58
|
+
const sevenDay = payload?.seven_day ?? null;
|
|
59
|
+
const sevenDaySonnet = payload?.seven_day_sonnet ?? null;
|
|
60
|
+
const sevenDayOpus = payload?.seven_day_opus ?? null;
|
|
61
|
+
|
|
62
|
+
if (fiveHour) {
|
|
63
|
+
windows['5h'] = toUsageWindow({
|
|
64
|
+
usedPercent: toNumber(fiveHour.utilization),
|
|
65
|
+
windowSeconds: null,
|
|
66
|
+
resetAt: toTimestamp(fiveHour.resets_at)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (sevenDay) {
|
|
70
|
+
windows['7d'] = toUsageWindow({
|
|
71
|
+
usedPercent: toNumber(sevenDay.utilization),
|
|
72
|
+
windowSeconds: null,
|
|
73
|
+
resetAt: toTimestamp(sevenDay.resets_at)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (sevenDaySonnet) {
|
|
77
|
+
windows['7d-sonnet'] = toUsageWindow({
|
|
78
|
+
usedPercent: toNumber(sevenDaySonnet.utilization),
|
|
79
|
+
windowSeconds: null,
|
|
80
|
+
resetAt: toTimestamp(sevenDaySonnet.resets_at)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (sevenDayOpus) {
|
|
84
|
+
windows['7d-opus'] = toUsageWindow({
|
|
85
|
+
usedPercent: toNumber(sevenDayOpus.utilization),
|
|
86
|
+
windowSeconds: null,
|
|
87
|
+
resetAt: toTimestamp(sevenDayOpus.resets_at)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return buildResult({
|
|
92
|
+
providerId,
|
|
93
|
+
providerName,
|
|
94
|
+
ok: true,
|
|
95
|
+
configured: true,
|
|
96
|
+
usage: { windows }
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return buildResult({
|
|
100
|
+
providerId,
|
|
101
|
+
providerName,
|
|
102
|
+
ok: false,
|
|
103
|
+
configured: true,
|
|
104
|
+
error: error instanceof Error ? error.message : 'Request failed'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|