@cartanova/qgrid-cli 1.5.6 → 1.6.0

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.
Files changed (39) hide show
  1. package/bundle/dist/application/qgrid/oauth.js +1 -1
  2. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +166 -0
  3. package/bundle/dist/application/qgrid/qgrid.frame.js +28 -36
  4. package/bundle/dist/application/qgrid/qgrid.types.js +5 -16
  5. package/bundle/dist/i18n/sd.generated.js +2 -2
  6. package/bundle/dist/sonamu.config.js +6 -9
  7. package/bundle/src/application/qgrid/oauth.ts +1 -1
  8. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +184 -0
  9. package/bundle/src/application/qgrid/qgrid.frame.ts +28 -50
  10. package/bundle/src/application/qgrid/qgrid.types.ts +2 -14
  11. package/bundle/src/application/sonamu.generated.http +2 -1
  12. package/bundle/src/i18n/sd.generated.ts +1 -1
  13. package/bundle/src/sonamu.config.ts +4 -7
  14. package/bundle/web-dist/client/assets/index-D8eEjsZj.js +78 -0
  15. package/bundle/web-dist/client/assets/{logs-KoJtGoEG.js → logs-CKnJzuH6.js} +1 -1
  16. package/bundle/web-dist/client/assets/{routes-CqzIoGYg.js → routes-DQKecVGa.js} +1 -1
  17. package/bundle/web-dist/client/assets/{sd.generated-Duipc8HN.js → sd.generated-NHYHS_-0.js} +1 -1
  18. package/bundle/web-dist/client/assets/services.generated-DDVXMid7.js +1 -0
  19. package/bundle/web-dist/client/assets/{show-BuMxiSr1.js → show-D2z1qxJ5.js} +1 -1
  20. package/bundle/web-dist/client/assets/tokens-BaywaQkm.js +1 -0
  21. package/bundle/web-dist/client/index.html +2 -2
  22. package/bundle/web-dist/server/assets/{copy-BPyN_0Dp.js → copy-BVWDOV2T.js} +1 -1
  23. package/bundle/web-dist/server/assets/{lazyRouteComponent-Bmjwy0OA.js → lazyRouteComponent-BJmwxFyJ.js} +2 -2
  24. package/bundle/web-dist/server/assets/{logs-BgcF7jVg.js → logs-DgIbufNf.js} +3 -3
  25. package/bundle/web-dist/server/assets/{routes-DVA-nsZ-.js → routes-x7-hqUm0.js} +2 -2
  26. package/bundle/web-dist/server/assets/{sd.generated-CDVQy4CJ.js → sd.generated-BZPIp4uL.js} +1 -1
  27. package/bundle/web-dist/server/assets/{services.generated-DWyvb1vb.js → services.generated-BA0gbdmd.js} +5 -4
  28. package/bundle/web-dist/server/assets/{shim-CqVZY710.js → shim-DGrmZkac.js} +1 -1
  29. package/bundle/web-dist/server/assets/{show-BnKnfNFP.js → show-Bk3NayMH.js} +4 -4
  30. package/bundle/web-dist/server/assets/{tokens-7z55hPRV.js → tokens-CVSwt0x0.js} +6 -17
  31. package/bundle/web-dist/server/entry-server.generated.js +7 -7
  32. package/package.json +1 -1
  33. package/bundle/dist/application/qgrid/pool.js +0 -128
  34. package/bundle/dist/application/qgrid/worker.js +0 -189
  35. package/bundle/src/application/qgrid/pool.ts +0 -165
  36. package/bundle/src/application/qgrid/worker.ts +0 -240
  37. package/bundle/web-dist/client/assets/index-BnqlZB1V.js +0 -78
  38. package/bundle/web-dist/client/assets/services.generated-Ccs6tru4.js +0 -1
  39. package/bundle/web-dist/client/assets/tokens--z-eVto5.js +0 -1
@@ -1,165 +0,0 @@
1
- /**
2
- * ClaudePool — 멀티 토큰 프로세스 풀.
3
- *
4
- * 2-layer 라우팅: 토큰 선택 (least-total-depth + round-robin) → 워커 선택
5
- * - Map<token, Worker[]>로 토큰별 워커 관리
6
- * - 토큰 간 quota 균등 소진
7
- */
8
- import { type CliResult, type PoolConfig, type QueryInput, type TokenStats } from "./qgrid.types";
9
- import { QuotaError } from "./qgrid.types";
10
- import { Worker, type WorkerConfig } from "./worker";
11
-
12
- export class ClaudePool {
13
- workers = new Map<string, Worker[]>();
14
- tokenNames = new Map<string, string>();
15
- quotaExhausted = new Set<string>();
16
- requestCounts = new Map<string, number>();
17
- lastUsedToken = "";
18
- lastUsedWorkerIndex = 0;
19
- size: number;
20
- model: string;
21
- timeout: number;
22
- cwd: string;
23
- maxCalls: number;
24
-
25
- constructor(config: PoolConfig) {
26
- this.size = config.size ?? 3;
27
- this.model = config.model ?? "sonnet";
28
- this.timeout = config.timeout ?? 600_000;
29
- this.cwd = config.cwd ?? "/tmp/qgrid";
30
- this.maxCalls = config.maxCalls ?? 500;
31
-
32
- config.tokens.forEach(({ token, name }) => {
33
- this.createWorkers(token, name);
34
- });
35
- }
36
-
37
- getLastUsedWorkerName(): string {
38
- const baseName = this.tokenNames.get(this.lastUsedToken) ?? "Unknown";
39
- return `${baseName}-${this.lastUsedWorkerIndex}`;
40
- }
41
-
42
- getStats(): TokenStats[] {
43
- return [...this.workers.keys()].map((token) => ({
44
- token,
45
- name: this.tokenNames.get(token) ?? "Unknown",
46
- requests: this.requestCounts.get(token) ?? 0,
47
- active: !this.quotaExhausted.has(token),
48
- }));
49
- }
50
-
51
- private tokenRrIndex = 0;
52
- private workerRrIndexes = new Map<string, number>();
53
-
54
- selectWorker(): Worker | null {
55
- const availableTokens = [...this.workers.entries()].filter(
56
- ([token]) => !this.quotaExhausted.has(token),
57
- );
58
-
59
- if (availableTokens.length === 0) return null;
60
-
61
- // 1단계: 토큰별 총 큐 depth가 가장 낮은 토큰 선택
62
- const tokenDepths = availableTokens.map(([token, workers]) => ({
63
- token,
64
- workers,
65
- totalDepth: workers.reduce((sum, w) => sum + w.getQueueDepth(), 0),
66
- }));
67
- const minTokenDepth = Math.min(...tokenDepths.map((t) => t.totalDepth));
68
- const idleTokens = tokenDepths.filter((t) => t.totalDepth === minTokenDepth);
69
- const selected = idleTokens[this.tokenRrIndex % idleTokens.length]!;
70
- this.tokenRrIndex++;
71
-
72
- // 2단계: 토큰 내에서 least-depth 워커, 동률이면 round-robin
73
- const minDepth = Math.min(...selected.workers.map((w) => w.getQueueDepth()));
74
- const idleWorkers = selected.workers.filter((w) => w.getQueueDepth() === minDepth);
75
- const wrIdx = this.workerRrIndexes.get(selected.token) ?? 0;
76
- const picked = idleWorkers[wrIdx % idleWorkers.length]!;
77
- this.workerRrIndexes.set(selected.token, wrIdx + 1);
78
-
79
- return picked;
80
- }
81
-
82
- async query(input: QueryInput, timeoutMs?: number): Promise<CliResult> {
83
- const triedTokens = new Set<string>();
84
-
85
- while (true) {
86
- const worker = this.selectWorker();
87
- if (!worker) {
88
- throw new QuotaError("All tokens exhausted");
89
- }
90
-
91
- if (triedTokens.has(worker.tokenId)) {
92
- throw new QuotaError("All tokens exhausted");
93
- }
94
-
95
- const workerName = this.tokenNames.get(worker.tokenId) ?? "Unknown";
96
- const tokenWorkers = this.workers.get(worker.tokenId);
97
- const workerIdx = tokenWorkers ? tokenWorkers.indexOf(worker) + 1 : 0;
98
- console.log(`[qgrid] → ${workerName}-${workerIdx} (queue: ${worker.getQueueDepth()})`);
99
-
100
- try {
101
- const result = await worker.query(input, timeoutMs);
102
- this.lastUsedToken = worker.tokenId;
103
- this.lastUsedWorkerIndex = workerIdx;
104
- this.requestCounts.set(worker.tokenId, (this.requestCounts.get(worker.tokenId) ?? 0) + 1);
105
-
106
- return result;
107
- } catch (err) {
108
- if (err instanceof QuotaError) {
109
- this.quotaExhausted.add(worker.tokenId);
110
- triedTokens.add(worker.tokenId);
111
- continue;
112
- }
113
- throw err;
114
- }
115
- }
116
- }
117
-
118
- kill(): void {
119
- [...this.workers.values()].flat().forEach((w) => {
120
- w.kill();
121
- });
122
- }
123
-
124
- createWorkers(token: string, name: string): void {
125
- if (this.workers.has(token)) return;
126
-
127
- this.tokenNames.set(token, name);
128
- const workerConfig: WorkerConfig = {
129
- token,
130
- model: this.model,
131
- timeout: this.timeout,
132
- cwd: this.cwd,
133
- maxCalls: this.maxCalls,
134
- };
135
- const workers = Array.from({ length: this.size }, () => new Worker(workerConfig));
136
- this.workers.set(token, workers);
137
- this.requestCounts.set(token, 0);
138
- }
139
-
140
- destroyWorkers(token: string): void {
141
- const workers = this.workers.get(token);
142
- if (!workers) return;
143
-
144
- workers.forEach((w) => {
145
- w.kill();
146
- });
147
- this.workers.delete(token);
148
- this.tokenNames.delete(token);
149
- this.quotaExhausted.delete(token);
150
- this.requestCounts.delete(token);
151
- }
152
- }
153
-
154
- // Pool 싱글턴 관리
155
- let pool: ClaudePool | null = null;
156
-
157
- export function initPool(tokens: { token: string; name: string }[]): ClaudePool {
158
- pool = new ClaudePool({ tokens });
159
- return pool;
160
- }
161
-
162
- export function getPool(): ClaudePool {
163
- if (!pool) throw new Error("Pool not initialized. Call initPool() first.");
164
- return pool;
165
- }
@@ -1,240 +0,0 @@
1
- /**
2
- * Worker — Claude CLI stream-json 프로세스 하나를 관리.
3
- *
4
- * 기존 SocratsAI ClaudeCliService에서 추출 + 리팩터:
5
- * - 토큰/모델을 생성자에서 주입
6
- * - env allowlist (PATH, TMPDIR, CLAUDE_CODE_OAUTH_TOKEN + CLAUDE_CODE_DISABLE_* )
7
- * - callCount 추적 + maxCalls 도달 시 프로세스 재활용
8
- * - QuotaError/TimeoutError/ProcessError 구분
9
- */
10
- import { type ChildProcess, spawn } from "node:child_process";
11
- import { mkdirSync } from "node:fs";
12
-
13
- import { type CliResult, type QueryInput } from "./qgrid.types";
14
- import { maskToken, ProcessError, QuotaError, TimeoutError } from "./qgrid.types";
15
-
16
- type PendingRequest = {
17
- resolve: (value: CliResult) => void;
18
- reject: (reason: Error) => void;
19
- timer: ReturnType<typeof setTimeout>;
20
- };
21
-
22
- export type WorkerConfig = {
23
- token: string;
24
- model: string;
25
- timeout: number;
26
- cwd: string;
27
- maxCalls: number;
28
- };
29
-
30
- export class Worker {
31
- tokenId: string;
32
- config: WorkerConfig;
33
- process: ChildProcess | null = null;
34
- buffer = "";
35
- pending: PendingRequest | null = null;
36
- sessionId: string | null = null;
37
- queue: Array<() => void> = [];
38
- cwdCreated = false;
39
- callCount = 0;
40
- needsRecycle = false;
41
-
42
- constructor(config: WorkerConfig) {
43
- this.config = config;
44
- this.tokenId = config.token;
45
- }
46
-
47
- getQueueDepth(): number {
48
- return this.queue.length + (this.pending ? 1 : 0);
49
- }
50
-
51
- rejectPending(err: Error): void {
52
- if (!this.pending) return;
53
- this.pending.reject(err);
54
- clearTimeout(this.pending.timer);
55
- this.pending = null;
56
- }
57
-
58
- ensureProcess(): ChildProcess {
59
- if (this.process && this.process.exitCode === null && !this.needsRecycle) {
60
- return this.process;
61
- }
62
-
63
- if (this.process && this.process.exitCode === null) {
64
- this.process.kill();
65
- this.process = null;
66
- }
67
-
68
- const env: NodeJS.ProcessEnv = {
69
- PATH: process.env.PATH,
70
- TMPDIR: process.env.TMPDIR,
71
- CLAUDE_CODE_OAUTH_TOKEN: this.config.token,
72
- CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
73
- CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING: "1",
74
- CLAUDE_CODE_DISABLE_1M_CONTEXT: "1",
75
- };
76
-
77
- const { cwd } = this.config;
78
- if (!this.cwdCreated) {
79
- mkdirSync(cwd, { recursive: true });
80
- this.cwdCreated = true;
81
- }
82
-
83
- const child = spawn(
84
- "claude",
85
- [
86
- "-p",
87
- "--input-format",
88
- "stream-json",
89
- "--output-format",
90
- "stream-json",
91
- "--verbose",
92
- "--max-turns",
93
- "1",
94
- "--permission-mode",
95
- "bypassPermissions",
96
- "--setting-sources",
97
- "project",
98
- "--model",
99
- this.config.model,
100
- ],
101
- { stdio: ["pipe", "pipe", "ignore"], env, cwd },
102
- );
103
-
104
- child.stdout?.on("data", (d: Buffer) => {
105
- this.buffer += d.toString();
106
- this.processBuffer();
107
- });
108
-
109
- child.on("close", () => {
110
- this.process = null;
111
- this.rejectPending(
112
- new ProcessError(`CLI process closed (token: ${maskToken(this.tokenId)})`),
113
- );
114
- this.drainQueue();
115
- });
116
-
117
- child.on("error", (err) => {
118
- this.process = null;
119
- this.rejectPending(
120
- new ProcessError(`CLI process error: ${err.message} (token: ${maskToken(this.tokenId)})`),
121
- );
122
- this.drainQueue();
123
- });
124
-
125
- this.process = child;
126
- this.buffer = "";
127
- this.sessionId = null;
128
- this.callCount = 0;
129
- this.needsRecycle = false;
130
- return child;
131
- }
132
-
133
- processBuffer(): void {
134
- const lines = this.buffer.split("\n");
135
- this.buffer = lines.pop() ?? "";
136
-
137
- lines
138
- .filter((line) => line.trim() !== "")
139
- .forEach((line) => {
140
- try {
141
- const j = JSON.parse(line);
142
-
143
- if (j.type === "system" && j.subtype === "init" && j.session_id) {
144
- this.sessionId = j.session_id;
145
- }
146
-
147
- if (j.type === "result" && this.pending) {
148
- let text: string = j.result ?? "";
149
- text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
150
-
151
- if (text.startsWith("You've hit")) {
152
- this.pending.reject(
153
- new QuotaError(`Quota exhausted (token: ${maskToken(this.tokenId)})`),
154
- );
155
- clearTimeout(this.pending.timer);
156
- this.pending = null;
157
- this.drainQueue();
158
- return;
159
- }
160
-
161
- const u = j.usage ?? {};
162
- this.pending.resolve({
163
- text,
164
- usage: {
165
- input_tokens: u.input_tokens ?? 0,
166
- output_tokens: u.output_tokens ?? 0,
167
- cache_creation_input_tokens: u.cache_creation_input_tokens ?? 0,
168
- cache_read_input_tokens: u.cache_read_input_tokens ?? 0,
169
- },
170
- durationMs: j.duration_ms ?? 0,
171
- costUsd: j.total_cost_usd ?? 0,
172
- });
173
- clearTimeout(this.pending.timer);
174
- this.pending = null;
175
- this.drainQueue();
176
- }
177
- } catch {
178
- // skip non-json lines
179
- }
180
- });
181
- }
182
-
183
- drainQueue(): void {
184
- if (this.pending) return;
185
- const next = this.queue.shift();
186
- if (next) next();
187
- }
188
-
189
- async query(input: QueryInput, timeoutMs?: number): Promise<CliResult> {
190
- if (this.pending) {
191
- await new Promise<void>((resolve) => {
192
- this.queue.push(resolve);
193
- });
194
- }
195
-
196
- const child = this.ensureProcess();
197
- const timeout = timeoutMs ?? this.config.timeout;
198
-
199
- this.callCount++;
200
- if (this.callCount >= this.config.maxCalls) {
201
- this.needsRecycle = true;
202
- }
203
-
204
- const prompt = input.system ? `${input.system}\n\n${input.prompt}` : input.prompt;
205
-
206
- return new Promise<CliResult>((resolve, reject) => {
207
- const timer = setTimeout(() => {
208
- this.pending = null;
209
- reject(
210
- new TimeoutError(`Timeout after ${timeout / 1000}s (token: ${maskToken(this.tokenId)})`),
211
- );
212
- this.kill();
213
- }, timeout);
214
-
215
- this.pending = { resolve, reject, timer };
216
-
217
- const msg = JSON.stringify({
218
- type: "user",
219
- message: { role: "user", content: prompt },
220
- session_id: this.sessionId ?? "default",
221
- parent_tool_use_id: null,
222
- });
223
-
224
- child.stdin?.write(`${msg}\n`);
225
- });
226
- }
227
-
228
- kill(): void {
229
- if (this.process && this.process.exitCode === null) {
230
- this.process.kill();
231
- }
232
- this.process = null;
233
- this.buffer = "";
234
- this.sessionId = null;
235
- this.callCount = 0;
236
- this.needsRecycle = false;
237
- this.rejectPending(new ProcessError(`Worker killed (token: ${maskToken(this.tokenId)})`));
238
- this.drainQueue();
239
- }
240
- }