@jsonstudio/rcc 0.89.1083 → 0.89.1121

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 (67) hide show
  1. package/dist/build-info.js +2 -2
  2. package/dist/cli.js +75 -45
  3. package/dist/cli.js.map +1 -1
  4. package/dist/docs/daemon-admin-ui.html +21 -4
  5. package/dist/index.js +42 -9
  6. package/dist/index.js.map +1 -1
  7. package/dist/manager/modules/quota/index.d.ts +18 -0
  8. package/dist/manager/modules/quota/index.js +270 -18
  9. package/dist/manager/modules/quota/index.js.map +1 -1
  10. package/dist/manager/quota/provider-quota-center.d.ts +3 -0
  11. package/dist/manager/quota/provider-quota-center.js +88 -24
  12. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  13. package/dist/manager/quota/provider-quota-store.js +5 -2
  14. package/dist/manager/quota/provider-quota-store.js.map +1 -1
  15. package/dist/providers/core/runtime/gemini-cli-http-provider.js +6 -0
  16. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  17. package/dist/providers/core/utils/http-client.js +10 -1
  18. package/dist/providers/core/utils/http-client.js.map +1 -1
  19. package/dist/server/handlers/handler-utils.js +78 -0
  20. package/dist/server/handlers/handler-utils.js.map +1 -1
  21. package/dist/server/handlers/responses-handler.js +24 -2
  22. package/dist/server/handlers/responses-handler.js.map +1 -1
  23. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.d.ts +1 -1
  24. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +10 -20
  25. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  26. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +21 -46
  27. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  28. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +42 -10
  29. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
  30. package/dist/server/runtime/http-server/daemon-admin/restart-handler.d.ts +3 -0
  31. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js +21 -0
  32. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js.map +1 -0
  33. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +97 -5
  34. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
  35. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +15 -0
  36. package/dist/server/runtime/http-server/daemon-admin-routes.js +22 -0
  37. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  38. package/dist/server/runtime/http-server/index.d.ts +6 -1
  39. package/dist/server/runtime/http-server/index.js +131 -5
  40. package/dist/server/runtime/http-server/index.js.map +1 -1
  41. package/dist/server/runtime/http-server/middleware.d.ts +2 -1
  42. package/dist/server/runtime/http-server/middleware.js +7 -6
  43. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  44. package/dist/server/runtime/http-server/provider-utils.d.ts +1 -1
  45. package/dist/server/runtime/http-server/provider-utils.js +19 -2
  46. package/dist/server/runtime/http-server/provider-utils.js.map +1 -1
  47. package/dist/server/runtime/http-server/routes.d.ts +5 -0
  48. package/dist/server/runtime/http-server/routes.js +5 -1
  49. package/dist/server/runtime/http-server/routes.js.map +1 -1
  50. package/dist/server/runtime/http-server/types.d.ts +5 -0
  51. package/dist/server/utils/http-error-mapper.js +17 -0
  52. package/dist/server/utils/http-error-mapper.js.map +1 -1
  53. package/dist/token-daemon/token-daemon.js +7 -1
  54. package/dist/token-daemon/token-daemon.js.map +1 -1
  55. package/dist/utils/is-direct-execution.d.ts +1 -0
  56. package/dist/utils/is-direct-execution.js +15 -0
  57. package/dist/utils/is-direct-execution.js.map +1 -0
  58. package/dist/utils/windows-netstat.d.ts +1 -0
  59. package/dist/utils/windows-netstat.js +34 -0
  60. package/dist/utils/windows-netstat.js.map +1 -0
  61. package/package.json +6 -4
  62. package/scripts/scan-apply-patch-samples.mjs +221 -0
  63. package/scripts/scan-exec-command-samples.mjs +269 -0
  64. package/scripts/scan-tool-shape-samples.mjs +291 -0
  65. package/scripts/tools/sync-apply-patch-regressions.mjs +86 -0
  66. package/scripts/verify-apply-patch-regressions.mjs +119 -0
  67. package/scripts/verify-tool-arguments.mjs +1 -2
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Scan exec_command tool call shapes in:
5
+ * 1) repo samples: samples/ci-goldens/**
6
+ * 2) user samples: ~/.routecodex/codex-samples/** and ~/.routecodex/codex samples/**
7
+ *
8
+ * Goal: identify remaining shape failures and (via llmswitch-core) capture failures into:
9
+ * ~/.routecodex/errorsamples/exec_command/<error-type>/
10
+ *
11
+ * Usage:
12
+ * node scripts/scan-exec-command-samples.mjs
13
+ * node scripts/scan-exec-command-samples.mjs --user-all
14
+ */
15
+
16
+ import fs from 'node:fs/promises';
17
+ import path from 'node:path';
18
+ import os from 'node:os';
19
+ import { fileURLToPath, pathToFileURL } from 'node:url';
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const repoRoot = path.resolve(__dirname, '..');
23
+
24
+ const coreLoaderPath = path.join(repoRoot, 'dist', 'modules', 'llmswitch', 'core-loader.js');
25
+ const coreLoaderUrl = pathToFileURL(coreLoaderPath).href;
26
+
27
+ const HOME = os.homedir();
28
+ const USER_CODEX_ROOT = path.join(HOME, '.routecodex', 'codex-samples');
29
+ const USER_CODEX_ROOT_ALT = path.join(HOME, '.routecodex', 'codex samples');
30
+ const USER_CODEX_PENDING = path.join(USER_CODEX_ROOT, 'openai-chat', '__pending__');
31
+ const REPO_GOLDENS_ROOT = path.join(repoRoot, 'samples', 'ci-goldens');
32
+
33
+ async function fileExists(p) {
34
+ try {
35
+ await fs.access(p);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ async function* walkJsonFiles(root) {
43
+ const stack = [root];
44
+ while (stack.length) {
45
+ const current = stack.pop();
46
+ let entries;
47
+ try {
48
+ entries = await fs.readdir(current, { withFileTypes: true });
49
+ } catch {
50
+ continue;
51
+ }
52
+ for (const entry of entries) {
53
+ const full = path.join(current, entry.name);
54
+ if (entry.isDirectory()) {
55
+ stack.push(full);
56
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
57
+ yield full;
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ function extractExecCommandArgs(doc, { allowRegressionOriginalArgs } = { allowRegressionOriginalArgs: false }) {
64
+ const out = [];
65
+
66
+ function visit(node) {
67
+ if (!node) return;
68
+ if (Array.isArray(node)) {
69
+ for (const item of node) visit(item);
70
+ return;
71
+ }
72
+ if (typeof node !== 'object') return;
73
+
74
+ const any = node;
75
+
76
+ // OpenAI chat tool_calls: { function: { name, arguments } }
77
+ if (any.function && typeof any.function === 'object') {
78
+ const fn = any.function;
79
+ if (fn && fn.name === 'exec_command') {
80
+ const args = fn.arguments;
81
+ if (typeof args === 'string' && args.trim()) {
82
+ out.push(args);
83
+ }
84
+ }
85
+ }
86
+
87
+ // Responses output: { type:"function_call", name:"exec_command", arguments:"..." }
88
+ if (any.type === 'function_call' && any.name === 'exec_command') {
89
+ const args = any.arguments;
90
+ if (typeof args === 'string' && args.trim()) {
91
+ out.push(args);
92
+ }
93
+ }
94
+
95
+ // Regression sample shape: { originalArgs: "...", errorType: "..." }
96
+ if (allowRegressionOriginalArgs) {
97
+ if (
98
+ typeof any.originalArgs === 'string' &&
99
+ any.originalArgs.trim() &&
100
+ typeof any.errorType === 'string' &&
101
+ any.errorType.trim()
102
+ ) {
103
+ out.push(any.originalArgs);
104
+ }
105
+ }
106
+
107
+ for (const value of Object.values(any)) visit(value);
108
+ }
109
+
110
+ visit(doc);
111
+ return out;
112
+ }
113
+
114
+ async function loadValidator() {
115
+ if (!(await fileExists(coreLoaderPath))) {
116
+ throw new Error(`core-loader missing at ${coreLoaderPath} (run npm run build:dev first)`);
117
+ }
118
+ const { importCoreModule } = await import(coreLoaderUrl);
119
+ const { validateToolCall } = await importCoreModule('tools/tool-registry');
120
+ if (typeof validateToolCall !== 'function') {
121
+ throw new Error('validateToolCall not found in llmswitch-core tools/tool-registry');
122
+ }
123
+ return { validateToolCall };
124
+ }
125
+
126
+ async function scanRoot(label, rootDir, validateToolCall) {
127
+ if (!(await fileExists(rootDir))) {
128
+ return { label, rootDir, files: 0, calls: 0, ok: 0, byReason: new Map(), examples: [] };
129
+ }
130
+
131
+ const byReason = new Map();
132
+ const examples = [];
133
+ let files = 0;
134
+ let calls = 0;
135
+ let ok = 0;
136
+
137
+ for await (const filePath of walkJsonFiles(rootDir)) {
138
+ files += 1;
139
+ let raw;
140
+ try {
141
+ raw = await fs.readFile(filePath, 'utf-8');
142
+ } catch {
143
+ continue;
144
+ }
145
+ // Fast prefilter: skip JSON that doesn't even mention exec_command.
146
+ if (!raw.includes('exec_command')) continue;
147
+
148
+ let doc;
149
+ try {
150
+ doc = JSON.parse(raw);
151
+ } catch {
152
+ continue;
153
+ }
154
+
155
+ const isRegressionFile = filePath.includes(`${path.sep}_regressions${path.sep}`);
156
+ const argsList = extractExecCommandArgs(doc, { allowRegressionOriginalArgs: isRegressionFile });
157
+ if (!argsList.length) continue;
158
+
159
+ const dedup = Array.from(new Set(argsList));
160
+ for (const args of dedup) {
161
+ calls += 1;
162
+ const res = validateToolCall('exec_command', args);
163
+ if (res && res.ok) {
164
+ ok += 1;
165
+ continue;
166
+ }
167
+ const reason = (res && res.reason) || 'unknown';
168
+ const cur = byReason.get(reason) || { count: 0 };
169
+ cur.count += 1;
170
+ byReason.set(reason, cur);
171
+ if (examples.length < 12) {
172
+ examples.push({ file: filePath, reason });
173
+ }
174
+ }
175
+ }
176
+
177
+ return { label, rootDir, files, calls, ok, byReason, examples };
178
+ }
179
+
180
+ function mergeReports(a, b) {
181
+ const byReason = new Map(a.byReason);
182
+ for (const [reason, v] of b.byReason.entries()) {
183
+ const cur = byReason.get(reason) || { count: 0 };
184
+ cur.count += v.count;
185
+ byReason.set(reason, cur);
186
+ }
187
+ const examples = [...a.examples, ...b.examples].slice(0, 12);
188
+ return {
189
+ label: `${a.label} + ${b.label}`,
190
+ rootDir: `${a.rootDir} | ${b.rootDir}`,
191
+ files: a.files + b.files,
192
+ calls: a.calls + b.calls,
193
+ ok: a.ok + b.ok,
194
+ byReason,
195
+ examples
196
+ };
197
+ }
198
+
199
+ function printReport(report) {
200
+ const failures = report.calls - report.ok;
201
+ console.log(`\n[scan] ${report.label}`);
202
+ console.log(`root: ${report.rootDir}`);
203
+ console.log(`files: ${report.files} exec_command_calls: ${report.calls} ok: ${report.ok} fail: ${failures}`);
204
+ const entries = Array.from(report.byReason.entries()).sort((a, b) => b[1].count - a[1].count);
205
+ if (entries.length) {
206
+ console.log('failures_by_reason:');
207
+ for (const [reason, v] of entries.slice(0, 20)) {
208
+ console.log(` - ${reason}: ${v.count}`);
209
+ }
210
+ }
211
+ if (report.examples.length) {
212
+ console.log('examples:');
213
+ for (const ex of report.examples) {
214
+ console.log(` - ${path.relative(repoRoot, ex.file)} reason=${ex.reason}`);
215
+ }
216
+ }
217
+ }
218
+
219
+ async function main() {
220
+ const { validateToolCall } = await loadValidator();
221
+
222
+ const repo = await scanRoot('repo samples/ci-goldens', REPO_GOLDENS_ROOT, validateToolCall);
223
+ printReport(repo);
224
+
225
+ const userAll = process.argv.slice(2).includes('--user-all');
226
+ if (!userAll) {
227
+ const userRoot = (await fileExists(USER_CODEX_PENDING)) ? USER_CODEX_PENDING : USER_CODEX_ROOT;
228
+ const user = await scanRoot('user ~/.routecodex/codex-samples/openai-chat/__pending__', userRoot, validateToolCall);
229
+ printReport(user);
230
+ const totalCalls = repo.calls + user.calls;
231
+ const totalOk = repo.ok + user.ok;
232
+ console.log(`\n[scan] TOTAL exec_command_calls=${totalCalls} ok=${totalOk} fail=${totalCalls - totalOk}`);
233
+ process.exitCode = totalCalls - totalOk > 0 ? 1 : 0;
234
+ return;
235
+ }
236
+
237
+ const roots = [];
238
+ if (await fileExists(USER_CODEX_ROOT)) roots.push({ label: 'user ~/.routecodex/codex-samples (all)', dir: USER_CODEX_ROOT });
239
+ if (await fileExists(USER_CODEX_ROOT_ALT)) roots.push({ label: 'user ~/.routecodex/codex samples (all)', dir: USER_CODEX_ROOT_ALT });
240
+ if (!roots.length) {
241
+ const user = await scanRoot('user ~/.routecodex/codex-samples (all)', USER_CODEX_ROOT, validateToolCall);
242
+ printReport(user);
243
+ const totalCalls = repo.calls + user.calls;
244
+ const totalOk = repo.ok + user.ok;
245
+ console.log(`\n[scan] TOTAL exec_command_calls=${totalCalls} ok=${totalOk} fail=${totalCalls - totalOk}`);
246
+ process.exitCode = totalCalls - totalOk > 0 ? 1 : 0;
247
+ return;
248
+ }
249
+
250
+ let merged = null;
251
+ for (const root of roots) {
252
+ const report = await scanRoot(root.label, root.dir, validateToolCall);
253
+ printReport(report);
254
+ merged = merged ? mergeReports(merged, report) : report;
255
+ }
256
+
257
+ const user = merged;
258
+ const totalCalls = repo.calls + user.calls;
259
+ const totalOk = repo.ok + user.ok;
260
+ console.log(`\n[scan] TOTAL exec_command_calls=${totalCalls} ok=${totalOk} fail=${totalCalls - totalOk}`);
261
+
262
+ process.exitCode = totalCalls - totalOk > 0 ? 1 : 0;
263
+ }
264
+
265
+ main().catch((error) => {
266
+ console.error('[scan-exec-command-samples] failed:', error?.stack || error?.message || String(error ?? 'unknown'));
267
+ process.exit(2);
268
+ });
269
+
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Scan tool-call argument shapes in:
5
+ * 1) repo samples: samples/ci-goldens/**
6
+ * 2) user samples: ~/.routecodex/codex-samples/** (or ~/.routecodex/codex samples/**)
7
+ *
8
+ * Tools covered:
9
+ * - apply_patch
10
+ * - exec_command
11
+ *
12
+ * Notes:
13
+ * - This script validates "shape/type/format" only. Context mismatch is not handled here.
14
+ * - For apply_patch, validateToolCall() may capture regressions automatically into:
15
+ * ~/.routecodex/golden_samples/ci-regression/apply_patch
16
+ * Use --to-repo to write into repo CI goldens (_regressions/apply_patch).
17
+ *
18
+ * Usage:
19
+ * node scripts/scan-tool-shape-samples.mjs
20
+ * node scripts/scan-tool-shape-samples.mjs --user-all
21
+ * node scripts/scan-tool-shape-samples.mjs --to-repo --user-all
22
+ */
23
+
24
+ import fs from 'node:fs/promises';
25
+ import path from 'node:path';
26
+ import os from 'node:os';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+
29
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ const repoRoot = path.resolve(__dirname, '..');
31
+
32
+ const coreLoaderPath = path.join(repoRoot, 'dist', 'modules', 'llmswitch', 'core-loader.js');
33
+ const coreLoaderUrl = pathToFileURL(coreLoaderPath).href;
34
+
35
+ const HOME = os.homedir();
36
+ const USER_CODEX_ROOT = path.join(HOME, '.routecodex', 'codex-samples');
37
+ const USER_CODEX_ROOT_SPACE = path.join(HOME, '.routecodex', 'codex samples');
38
+ const USER_CODEX_PENDING = path.join(USER_CODEX_ROOT, 'openai-chat', '__pending__');
39
+ const USER_CODEX_PENDING_SPACE = path.join(USER_CODEX_ROOT_SPACE, 'openai-chat', '__pending__');
40
+ const REPO_GOLDENS_ROOT = path.join(repoRoot, 'samples', 'ci-goldens');
41
+
42
+ const TOOL_NAMES = ['apply_patch', 'exec_command'];
43
+
44
+ async function fileExists(p) {
45
+ try {
46
+ await fs.access(p);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ async function* walkJsonFiles(root) {
54
+ const stack = [root];
55
+ while (stack.length) {
56
+ const current = stack.pop();
57
+ let entries;
58
+ try {
59
+ entries = await fs.readdir(current, { withFileTypes: true });
60
+ } catch {
61
+ continue;
62
+ }
63
+ for (const entry of entries) {
64
+ const full = path.join(current, entry.name);
65
+ if (entry.isDirectory()) {
66
+ stack.push(full);
67
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
68
+ yield full;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ function extractToolArgs(doc, toolNames, { allowRegressionOriginalArgs } = { allowRegressionOriginalArgs: false }) {
75
+ const out = [];
76
+ const wanted = new Set(toolNames.map((n) => String(n).trim().toLowerCase()).filter(Boolean));
77
+
78
+ function visit(node) {
79
+ if (!node) return;
80
+ if (Array.isArray(node)) {
81
+ for (const item of node) visit(item);
82
+ return;
83
+ }
84
+ if (typeof node !== 'object') return;
85
+
86
+ const any = node;
87
+
88
+ // OpenAI chat tool_calls: { function: { name, arguments } }
89
+ if (any.function && typeof any.function === 'object') {
90
+ const fn = any.function;
91
+ const name = typeof fn.name === 'string' ? fn.name.trim().toLowerCase() : '';
92
+ if (wanted.has(name)) {
93
+ const args = fn.arguments;
94
+ if (typeof args === 'string' && args.trim()) {
95
+ out.push({ name, args });
96
+ }
97
+ }
98
+ }
99
+
100
+ // Responses output: { type:"function_call", name:"...", arguments:"..." }
101
+ if (any.type === 'function_call') {
102
+ const name = typeof any.name === 'string' ? any.name.trim().toLowerCase() : '';
103
+ if (wanted.has(name)) {
104
+ const args = any.arguments;
105
+ if (typeof args === 'string' && args.trim()) {
106
+ out.push({ name, args });
107
+ }
108
+ }
109
+ }
110
+
111
+ // Regression sample shape: { originalArgs: "...", errorType: "..." }
112
+ if (allowRegressionOriginalArgs) {
113
+ const name = typeof any.tool === 'string' ? any.tool.trim().toLowerCase() : '';
114
+ if (wanted.has(name) && typeof any.originalArgs === 'string' && any.originalArgs.trim()) {
115
+ out.push({ name, args: any.originalArgs });
116
+ }
117
+ // Historical apply_patch regression capture doesn't include tool name; default to apply_patch.
118
+ if (
119
+ typeof any.originalArgs === 'string' &&
120
+ any.originalArgs.trim() &&
121
+ typeof any.errorType === 'string' &&
122
+ any.errorType.trim() &&
123
+ !name
124
+ ) {
125
+ out.push({ name: 'apply_patch', args: any.originalArgs });
126
+ }
127
+ }
128
+
129
+ for (const value of Object.values(any)) visit(value);
130
+ }
131
+
132
+ visit(doc);
133
+ return out;
134
+ }
135
+
136
+ async function loadValidator() {
137
+ if (!(await fileExists(coreLoaderPath))) {
138
+ throw new Error(`core-loader missing at ${coreLoaderPath} (run npm run build:dev first)`);
139
+ }
140
+ const { importCoreModule } = await import(coreLoaderUrl);
141
+ const { validateToolCall } = await importCoreModule('tools/tool-registry');
142
+ if (typeof validateToolCall !== 'function') {
143
+ throw new Error('validateToolCall not found in llmswitch-core tools/tool-registry');
144
+ }
145
+ return { validateToolCall };
146
+ }
147
+
148
+ async function scanRoot(label, rootDir, validateToolCall) {
149
+ if (!(await fileExists(rootDir))) {
150
+ return { label, rootDir, files: 0, calls: 0, ok: 0, byReason: new Map(), byTool: new Map(), examples: [] };
151
+ }
152
+
153
+ const byReason = new Map();
154
+ const byTool = new Map();
155
+ const examples = [];
156
+ let files = 0;
157
+ let calls = 0;
158
+ let ok = 0;
159
+
160
+ for await (const filePath of walkJsonFiles(rootDir)) {
161
+ files += 1;
162
+ let raw;
163
+ try {
164
+ raw = await fs.readFile(filePath, 'utf-8');
165
+ } catch {
166
+ continue;
167
+ }
168
+ // Fast prefilter: skip JSON that doesn't even mention our tool names.
169
+ if (!TOOL_NAMES.some((n) => raw.includes(n))) continue;
170
+
171
+ let doc;
172
+ try {
173
+ doc = JSON.parse(raw);
174
+ } catch {
175
+ continue;
176
+ }
177
+
178
+ const isRegressionFile = filePath.includes(`${path.sep}_regressions${path.sep}`);
179
+ const argsList = extractToolArgs(doc, TOOL_NAMES, { allowRegressionOriginalArgs: isRegressionFile });
180
+ if (!argsList.length) continue;
181
+
182
+ const dedupKey = (x) => `${x.name}:${x.args}`;
183
+ const dedup = Array.from(new Map(argsList.map((x) => [dedupKey(x), x])).values());
184
+
185
+ for (const entry of dedup) {
186
+ calls += 1;
187
+ const toolName = entry.name;
188
+ const toolCount = byTool.get(toolName) || { calls: 0, ok: 0 };
189
+ toolCount.calls += 1;
190
+ byTool.set(toolName, toolCount);
191
+
192
+ const res = validateToolCall(toolName, entry.args);
193
+ if (res && res.ok) {
194
+ ok += 1;
195
+ toolCount.ok += 1;
196
+ continue;
197
+ }
198
+ const reason = (res && res.reason) || 'unknown';
199
+ const cur = byReason.get(reason) || { count: 0 };
200
+ cur.count += 1;
201
+ byReason.set(reason, cur);
202
+ if (examples.length < 12) {
203
+ examples.push({ file: filePath, reason, tool: toolName });
204
+ }
205
+ }
206
+ }
207
+
208
+ return { label, rootDir, files, calls, ok, byReason, byTool, examples };
209
+ }
210
+
211
+ function printReport(report) {
212
+ const failures = report.calls - report.ok;
213
+ console.log(`\n[scan] ${report.label}`);
214
+ console.log(`root: ${report.rootDir}`);
215
+ console.log(`files: ${report.files} tool_calls: ${report.calls} ok: ${report.ok} fail: ${failures}`);
216
+
217
+ const toolEntries = Array.from(report.byTool.entries()).sort((a, b) => b[1].calls - a[1].calls);
218
+ if (toolEntries.length) {
219
+ console.log('by_tool:');
220
+ for (const [tool, v] of toolEntries) {
221
+ console.log(` - ${tool}: calls=${v.calls} ok=${v.ok} fail=${v.calls - v.ok}`);
222
+ }
223
+ }
224
+
225
+ const entries = Array.from(report.byReason.entries()).sort((a, b) => b[1].count - a[1].count);
226
+ if (entries.length) {
227
+ console.log('failures_by_reason:');
228
+ for (const [reason, v] of entries.slice(0, 30)) {
229
+ console.log(` - ${reason}: ${v.count}`);
230
+ }
231
+ }
232
+ if (report.examples.length) {
233
+ console.log('examples:');
234
+ for (const ex of report.examples) {
235
+ console.log(` - ${path.relative(repoRoot, ex.file)} tool=${ex.tool} reason=${ex.reason}`);
236
+ }
237
+ }
238
+ }
239
+
240
+ function resolveUserRoot({ userAll }) {
241
+ const candidates = [];
242
+ if (userAll) {
243
+ candidates.push(USER_CODEX_ROOT);
244
+ candidates.push(USER_CODEX_ROOT_SPACE);
245
+ } else {
246
+ candidates.push(USER_CODEX_PENDING);
247
+ candidates.push(USER_CODEX_PENDING_SPACE);
248
+ candidates.push(USER_CODEX_ROOT);
249
+ candidates.push(USER_CODEX_ROOT_SPACE);
250
+ }
251
+ return candidates;
252
+ }
253
+
254
+ async function main() {
255
+ const args = process.argv.slice(2);
256
+ const userAll = args.includes('--user-all');
257
+ const toRepo = args.includes('--to-repo');
258
+ if (toRepo) {
259
+ process.env.ROUTECODEX_APPLY_PATCH_REGRESSION_TO_REPO = '1';
260
+ }
261
+
262
+ const { validateToolCall } = await loadValidator();
263
+
264
+ const repo = await scanRoot('repo samples/ci-goldens', REPO_GOLDENS_ROOT, validateToolCall);
265
+ printReport(repo);
266
+
267
+ let userRoot = null;
268
+ for (const p of resolveUserRoot({ userAll })) {
269
+ if (await fileExists(p)) {
270
+ userRoot = p;
271
+ break;
272
+ }
273
+ }
274
+ const userLabel = userAll
275
+ ? '~/.routecodex/(codex-samples|codex samples) (all)'
276
+ : '~/.routecodex/(codex-samples|codex samples)/openai-chat/__pending__';
277
+ const user = await scanRoot(`user ${userLabel}`, userRoot || USER_CODEX_ROOT, validateToolCall);
278
+ printReport(user);
279
+
280
+ const totalCalls = repo.calls + user.calls;
281
+ const totalOk = repo.ok + user.ok;
282
+ console.log(`\n[scan] TOTAL tool_calls=${totalCalls} ok=${totalOk} fail=${totalCalls - totalOk}`);
283
+
284
+ process.exitCode = totalCalls - totalOk > 0 ? 1 : 0;
285
+ }
286
+
287
+ main().catch((error) => {
288
+ console.error('[scan-tool-shape-samples] failed:', error?.stack || error?.message || String(error ?? 'unknown'));
289
+ process.exit(2);
290
+ });
291
+
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const PROJECT_ROOT = path.resolve(__dirname, '../..');
11
+
12
+ const SOURCE_ROOT = path.join(os.homedir(), '.routecodex', 'golden_samples', 'ci-regression', 'apply_patch');
13
+ const TARGET_ROOT = path.join(PROJECT_ROOT, 'samples', 'ci-goldens', '_regressions', 'apply_patch');
14
+
15
+ function ensureDir(dir) {
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ function listTypeDirs(root) {
20
+ if (!fs.existsSync(root)) return [];
21
+ return fs
22
+ .readdirSync(root, { withFileTypes: true })
23
+ .filter((d) => d.isDirectory())
24
+ .map((d) => d.name);
25
+ }
26
+
27
+ function listJsonFiles(dir) {
28
+ try {
29
+ return fs
30
+ .readdirSync(dir, { withFileTypes: true })
31
+ .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.json'))
32
+ .map((e) => e.name);
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ function usage() {
39
+ console.log('Usage: node scripts/tools/sync-apply-patch-regressions.mjs [--force]');
40
+ process.exit(0);
41
+ }
42
+
43
+ function main() {
44
+ const args = process.argv.slice(2);
45
+ const force = args.includes('--force');
46
+ if (args.includes('--help') || args.includes('-h')) usage();
47
+
48
+ if (!fs.existsSync(SOURCE_ROOT)) {
49
+ console.log(`[sync-apply-patch-regressions] skip (source missing: ${SOURCE_ROOT})`);
50
+ return;
51
+ }
52
+
53
+ ensureDir(TARGET_ROOT);
54
+
55
+ let copied = 0;
56
+ let skipped = 0;
57
+ const types = listTypeDirs(SOURCE_ROOT);
58
+ for (const type of types) {
59
+ const srcDir = path.join(SOURCE_ROOT, type);
60
+ const dstDir = path.join(TARGET_ROOT, type);
61
+ ensureDir(dstDir);
62
+ const files = listJsonFiles(srcDir);
63
+ for (const f of files) {
64
+ const src = path.join(srcDir, f);
65
+ const dst = path.join(dstDir, f);
66
+ if (!force && fs.existsSync(dst)) {
67
+ skipped += 1;
68
+ continue;
69
+ }
70
+ fs.copyFileSync(src, dst);
71
+ copied += 1;
72
+ }
73
+ }
74
+
75
+ console.log(
76
+ `[sync-apply-patch-regressions] synced ${copied} file(s) into ${TARGET_ROOT} (${skipped} skipped)`
77
+ );
78
+ }
79
+
80
+ try {
81
+ main();
82
+ } catch (error) {
83
+ console.error('[sync-apply-patch-regressions] failed:', error?.message || error);
84
+ process.exit(1);
85
+ }
86
+