teamclaude-cloud 1.4.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 +320 -0
- package/package.json +38 -0
- package/src/account-manager.js +1271 -0
- package/src/cloud.js +618 -0
- package/src/config.js +148 -0
- package/src/index.js +1726 -0
- package/src/oauth.js +347 -0
- package/src/server.js +1267 -0
- package/src/tui.js +843 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export function getConfigPath() {
|
|
8
|
+
if (process.env.TEAMCLAUDE_CONFIG) return process.env.TEAMCLAUDE_CONFIG;
|
|
9
|
+
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
10
|
+
return join(configDir, 'teamclaude.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Runtime state for the running server (pid/port), written next to the config so
|
|
14
|
+
// `teamclaude status` / `stop` / `restart` can find and signal it without the
|
|
15
|
+
// user hunting for the PID. One file per config, so different configs (and ports)
|
|
16
|
+
// never collide.
|
|
17
|
+
export function getServerStatePath() {
|
|
18
|
+
return getConfigPath().replace(/\.json$/, '') + '.server.json';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function writeServerState(state) {
|
|
22
|
+
const path = getServerStatePath();
|
|
23
|
+
await mkdir(dirname(path), { recursive: true });
|
|
24
|
+
await writeFile(path, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readServerState() {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(await readFile(getServerStatePath(), 'utf-8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return null; // missing or unreadable → treat as "no recorded server"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function clearServerState() {
|
|
36
|
+
try { await rm(getServerStatePath(), { force: true }); } catch { /* best-effort */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Per-account quota snapshot (credential-free), written next to the config so a
|
|
40
|
+
// restarted server starts from the last known quota/throttle state instead of a
|
|
41
|
+
// blank dashboard. Unlike the server-state file this survives exit on purpose.
|
|
42
|
+
export function getQuotaCachePath() {
|
|
43
|
+
return getConfigPath().replace(/\.json$/, '') + '.quota.json';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function readQuotaCache() {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(getQuotaCachePath(), 'utf-8'));
|
|
49
|
+
} catch {
|
|
50
|
+
return null; // missing or unreadable → start unmeasured, exactly as before
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Synchronous on purpose: the last write happens inside a process 'exit'
|
|
56
|
+
* handler, where async I/O never completes. The snapshot is small (a few KB).
|
|
57
|
+
*/
|
|
58
|
+
export function writeQuotaCacheSync(data) {
|
|
59
|
+
try {
|
|
60
|
+
const path = getQuotaCachePath();
|
|
61
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
62
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
63
|
+
} catch { /* best-effort — a failed snapshot must never break the proxy */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createDefaultConfig() {
|
|
67
|
+
return {
|
|
68
|
+
proxy: {
|
|
69
|
+
port: 3456,
|
|
70
|
+
apiKey: 'tc-' + randomBytes(24).toString('base64url'),
|
|
71
|
+
},
|
|
72
|
+
upstream: 'https://api.anthropic.com',
|
|
73
|
+
switchThreshold: 0.98,
|
|
74
|
+
// Max simultaneous in-flight requests per account before load spreads to the
|
|
75
|
+
// next account (per-account `maxConcurrent` overrides this). Tune to just
|
|
76
|
+
// below where one account starts returning rate/concurrency 429s.
|
|
77
|
+
maxConcurrentPerAccount: 3,
|
|
78
|
+
// Keep one client connection's sequential requests on the same account so
|
|
79
|
+
// Anthropic's per-account prompt cache stays warm (a session's turns reuse
|
|
80
|
+
// the keep-alive socket). Soft: concurrent overflow still spreads to other
|
|
81
|
+
// accounts. Set false to route every request purely by use-or-lose priority.
|
|
82
|
+
sessionAffinity: true,
|
|
83
|
+
// How long (ms) a request waits for a free slot when every account is at its
|
|
84
|
+
// cap, before returning 429. 0 = never queue.
|
|
85
|
+
overflowQueueTimeoutMs: 15000,
|
|
86
|
+
// Hard caps that bound proxy memory under a request flood.
|
|
87
|
+
overflowQueueMaxDepth: 256, // max queued requests before 429
|
|
88
|
+
maxRequestBytes: 33554432, // 32 MiB max buffered request body, else 413
|
|
89
|
+
accounts: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function loadConfig() {
|
|
94
|
+
const path = getConfigPath();
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (err.code === 'ENOENT') return null;
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function loadOrCreateConfig() {
|
|
104
|
+
let config = await loadConfig();
|
|
105
|
+
if (!config) {
|
|
106
|
+
config = createDefaultConfig();
|
|
107
|
+
await saveConfig(config);
|
|
108
|
+
console.log(`Created config at ${getConfigPath()}`);
|
|
109
|
+
}
|
|
110
|
+
return config;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function saveConfig(config) {
|
|
114
|
+
const path = getConfigPath();
|
|
115
|
+
await mkdir(dirname(path), { recursive: true });
|
|
116
|
+
await writeFile(path, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Atomically update the config: re-reads from disk, calls updater(config),
|
|
121
|
+
* then saves. Returns the updated config. This prevents overwriting changes
|
|
122
|
+
* made by other processes (e.g. `teamclaude import` while the server runs).
|
|
123
|
+
*
|
|
124
|
+
* Calls are SERIALIZED within this process (via the chain below): atomicConfigUpdate
|
|
125
|
+
* re-reads the whole file, mutates, and writes it all back, so two concurrent callers
|
|
126
|
+
* — e.g. a background token refresh and a TUI save/delete — would each read the same
|
|
127
|
+
* snapshot and the later write would clobber the earlier one's change (resurrecting a
|
|
128
|
+
* just-deleted account, or dropping a freshly-refreshed token). Chaining makes each
|
|
129
|
+
* cycle observe the previous cycle's write. Cross-PROCESS races remain (the
|
|
130
|
+
* single-proxy design assumption); a CLI write while the server runs is reconciled on
|
|
131
|
+
* the next reload.
|
|
132
|
+
*/
|
|
133
|
+
let _configWriteChain = Promise.resolve();
|
|
134
|
+
|
|
135
|
+
export function atomicConfigUpdate(updater) {
|
|
136
|
+
const run = async () => {
|
|
137
|
+
const config = await loadConfig() || createDefaultConfig();
|
|
138
|
+
await updater(config);
|
|
139
|
+
await saveConfig(config);
|
|
140
|
+
return config;
|
|
141
|
+
};
|
|
142
|
+
// Run after the previous cycle settles (success OR failure) so one failed update
|
|
143
|
+
// can't stall the queue; keep the chain itself non-rejecting and surface the
|
|
144
|
+
// result/error only to this caller.
|
|
145
|
+
const result = _configWriteChain.then(run, run);
|
|
146
|
+
_configWriteChain = result.then(() => {}, () => {});
|
|
147
|
+
return result;
|
|
148
|
+
}
|