aiopt 0.2.9 → 0.3.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/dist/cli.js CHANGED
@@ -230,6 +230,169 @@ var init_solutions = __esm({
230
230
  }
231
231
  });
232
232
 
233
+ // src/code-scan.ts
234
+ function isTextLike(p) {
235
+ const ext = import_path3.default.extname(p).toLowerCase();
236
+ return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".kt", ".rb", ".php"].includes(ext);
237
+ }
238
+ function walk(root, out) {
239
+ const items = import_fs3.default.readdirSync(root, { withFileTypes: true });
240
+ for (const it of items) {
241
+ if (DEFAULT_EXCLUDES.has(it.name)) continue;
242
+ const full = import_path3.default.join(root, it.name);
243
+ if (it.isDirectory()) walk(full, out);
244
+ else out.push(full);
245
+ }
246
+ }
247
+ function findAllLines(content, re) {
248
+ const lines = [];
249
+ const parts = content.split(/\r?\n/);
250
+ for (let i = 0; i < parts.length; i++) {
251
+ const line = parts[i];
252
+ if (re.test(line)) lines.push(i + 1);
253
+ }
254
+ return lines;
255
+ }
256
+ function runCodeScan(rootDir) {
257
+ const files = [];
258
+ walk(rootDir, files);
259
+ const findings = [];
260
+ for (const file of files) {
261
+ if (!isTextLike(file)) continue;
262
+ let st;
263
+ try {
264
+ st = import_fs3.default.statSync(file);
265
+ } catch {
266
+ continue;
267
+ }
268
+ if (!st.isFile()) continue;
269
+ if (st.size > 1024 * 1024) continue;
270
+ let content = "";
271
+ try {
272
+ content = import_fs3.default.readFileSync(file, "utf8");
273
+ } catch {
274
+ continue;
275
+ }
276
+ for (const ln of findAllLines(content, /\b(maxRetries|maximumRetries|retries|max_attempts|attempts)\s*[:=]\s*(\d{2,}|[6-9])\b/i)) {
277
+ findings.push({
278
+ ruleId: "AIOPT.RETRY.EXPLOSION_RISK",
279
+ level: "warning",
280
+ message: "High retry/attempt count detected. Consider capping retries to prevent cost explosions.",
281
+ file,
282
+ line: ln,
283
+ help: "Cap retries (e.g. 2-3), add backoff, and fail fast on non-retriable errors."
284
+ });
285
+ break;
286
+ }
287
+ for (const ln of findAllLines(content, /(gpt-5\.?2|gpt-4\.?|o1-|o3-|claude-3|sonnet|opus)/i)) {
288
+ findings.push({
289
+ ruleId: "AIOPT.MODEL.ROUTING.EXPENSIVE_DEFAULT",
290
+ level: "note",
291
+ message: "Possible expensive model hard-coded. Consider cheap default + explicit override for critical paths.",
292
+ file,
293
+ line: ln,
294
+ help: "Route cheap by default; allow overrides via env/config for high-impact tasks."
295
+ });
296
+ break;
297
+ }
298
+ const hasOpenAI = /\bopenai\b|\bOpenAI\b|responses\.create|chat\.completions\.create/i.test(content);
299
+ const hasTimeout = /\btimeout\b|\brequestTimeout\b|\bsignal\b/i.test(content);
300
+ if (hasOpenAI && !hasTimeout) {
301
+ findings.push({
302
+ ruleId: "AIOPT.TIMEOUT.MISSING",
303
+ level: "note",
304
+ message: "OpenAI/LLM call detected without obvious timeout. Add a timeout to reduce hanging retries and cost waste.",
305
+ file,
306
+ line: 1,
307
+ help: "Add request timeout / AbortSignal and handle retryable errors explicitly."
308
+ });
309
+ }
310
+ }
311
+ return findings.slice(0, 200);
312
+ }
313
+ var import_fs3, import_path3, DEFAULT_EXCLUDES;
314
+ var init_code_scan = __esm({
315
+ "src/code-scan.ts"() {
316
+ "use strict";
317
+ import_fs3 = __toESM(require("fs"));
318
+ import_path3 = __toESM(require("path"));
319
+ DEFAULT_EXCLUDES = /* @__PURE__ */ new Set([
320
+ ".git",
321
+ "node_modules",
322
+ "dist",
323
+ "aiopt-output",
324
+ ".next",
325
+ "build",
326
+ "coverage"
327
+ ]);
328
+ }
329
+ });
330
+
331
+ // src/sarif.ts
332
+ function toUri(p) {
333
+ const abs = import_path4.default.resolve(p);
334
+ const u = abs.replace(/\\/g, "/");
335
+ return u.match(/^[A-Za-z]:\//) ? `file:///${u}` : `file://${u}`;
336
+ }
337
+ function buildSarif(toolName, toolVersion, findings) {
338
+ const rulesMap = /* @__PURE__ */ new Map();
339
+ for (const f of findings) {
340
+ if (!rulesMap.has(f.ruleId)) {
341
+ rulesMap.set(f.ruleId, {
342
+ id: f.ruleId,
343
+ shortDescription: f.ruleId,
344
+ help: f.help
345
+ });
346
+ }
347
+ }
348
+ const rules = [...rulesMap.values()].map((r) => ({
349
+ id: r.id,
350
+ shortDescription: { text: r.shortDescription },
351
+ help: r.help ? { text: r.help } : void 0
352
+ })).map((x) => {
353
+ const y = { ...x };
354
+ if (!y.help) delete y.help;
355
+ return y;
356
+ });
357
+ const results = findings.map((f) => ({
358
+ ruleId: f.ruleId,
359
+ level: f.level,
360
+ message: { text: f.message },
361
+ locations: [
362
+ {
363
+ physicalLocation: {
364
+ artifactLocation: { uri: toUri(f.file) },
365
+ region: { startLine: Math.max(1, Math.floor(f.line || 1)) }
366
+ }
367
+ }
368
+ ]
369
+ }));
370
+ return {
371
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
372
+ version: "2.1.0",
373
+ runs: [
374
+ {
375
+ tool: {
376
+ driver: {
377
+ name: toolName,
378
+ version: toolVersion,
379
+ informationUri: "https://www.npmjs.com/package/aiopt",
380
+ rules
381
+ }
382
+ },
383
+ results
384
+ }
385
+ ]
386
+ };
387
+ }
388
+ var import_path4;
389
+ var init_sarif = __esm({
390
+ "src/sarif.ts"() {
391
+ "use strict";
392
+ import_path4 = __toESM(require("path"));
393
+ }
394
+ });
395
+
233
396
  // src/scan.ts
234
397
  function topN(map, n) {
235
398
  return [...map.entries()].map(([key, v]) => ({ key, cost: v.cost, events: v.events })).sort((a, b) => b.cost - a.cost).slice(0, n);
@@ -387,8 +550,8 @@ function round22(n) {
387
550
  }
388
551
  function writeOutputs(outDir, analysis, savings, policy, meta) {
389
552
  const mode = meta?.mode || "legacy";
390
- import_fs3.default.mkdirSync(outDir, { recursive: true });
391
- import_fs3.default.writeFileSync(import_path3.default.join(outDir, "analysis.json"), JSON.stringify(analysis, null, 2));
553
+ import_fs4.default.mkdirSync(outDir, { recursive: true });
554
+ import_fs4.default.writeFileSync(import_path5.default.join(outDir, "analysis.json"), JSON.stringify(analysis, null, 2));
392
555
  const unknownCount = analysis.unknown_models?.length || 0;
393
556
  const confidence = unknownCount === 0 ? "HIGH" : unknownCount <= 3 ? "MED" : "LOW";
394
557
  const ratio = analysis.total_cost > 0 ? savings.estimated_savings_total / analysis.total_cost : 0;
@@ -420,7 +583,7 @@ function writeOutputs(outDir, analysis, savings, policy, meta) {
420
583
  unknown_models: analysis.unknown_models,
421
584
  notes: savings.notes
422
585
  };
423
- import_fs3.default.writeFileSync(import_path3.default.join(outDir, "report.json"), JSON.stringify(reportJson, null, 2));
586
+ import_fs4.default.writeFileSync(import_path5.default.join(outDir, "report.json"), JSON.stringify(reportJson, null, 2));
424
587
  const ratioMd = analysis.total_cost > 0 ? savings.estimated_savings_total / analysis.total_cost : 0;
425
588
  const warningsMd = [];
426
589
  if (ratioMd >= 0.9) warningsMd.push("WARNING: estimated savings >= 90% \u2014 check overlap/missing rate table");
@@ -449,7 +612,7 @@ function writeOutputs(outDir, analysis, savings, policy, meta) {
449
612
  "- `aiopt-output/patches/*`",
450
613
  ""
451
614
  ].join("\n");
452
- import_fs3.default.writeFileSync(import_path3.default.join(outDir, "report.md"), reportMd);
615
+ import_fs4.default.writeFileSync(import_path5.default.join(outDir, "report.md"), reportMd);
453
616
  const reportTxt = [
454
617
  `\uCD1D\uBE44\uC6A9: $${analysis.total_cost}`,
455
618
  `\uC808\uAC10 \uAC00\uB2A5 \uAE08\uC561(Estimated): $${savings.estimated_savings_total}`,
@@ -459,19 +622,31 @@ function writeOutputs(outDir, analysis, savings, policy, meta) {
459
622
  savings.notes[2],
460
623
  ""
461
624
  ].join("\n");
462
- import_fs3.default.writeFileSync(import_path3.default.join(outDir, "report.txt"), reportTxt);
463
- import_fs3.default.writeFileSync(import_path3.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
625
+ import_fs4.default.writeFileSync(import_path5.default.join(outDir, "report.txt"), reportTxt);
626
+ import_fs4.default.writeFileSync(import_path5.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
464
627
  const fixes = buildTopFixes(analysis, savings);
465
628
  writePatches(outDir, fixes);
629
+ try {
630
+ const cwd = meta?.cwd;
631
+ const cliVersion = meta?.cliVersion || "unknown";
632
+ if (cwd && import_fs4.default.existsSync(cwd)) {
633
+ const findings = runCodeScan(cwd);
634
+ const sarif = buildSarif("aiopt", cliVersion, findings);
635
+ import_fs4.default.writeFileSync(import_path5.default.join(outDir, "aiopt.sarif"), JSON.stringify(sarif, null, 2));
636
+ }
637
+ } catch {
638
+ }
466
639
  }
467
- var import_fs3, import_path3, ROUTE_TO_CHEAP_FEATURES;
640
+ var import_fs4, import_path5, ROUTE_TO_CHEAP_FEATURES;
468
641
  var init_scan = __esm({
469
642
  "src/scan.ts"() {
470
643
  "use strict";
471
- import_fs3 = __toESM(require("fs"));
472
- import_path3 = __toESM(require("path"));
644
+ import_fs4 = __toESM(require("fs"));
645
+ import_path5 = __toESM(require("path"));
473
646
  init_cost();
474
647
  init_solutions();
648
+ init_code_scan();
649
+ init_sarif();
475
650
  ROUTE_TO_CHEAP_FEATURES = /* @__PURE__ */ new Set(["summarize", "classify", "translate"]);
476
651
  }
477
652
  });
@@ -481,7 +656,7 @@ var require_package = __commonJS({
481
656
  "package.json"(exports2, module2) {
482
657
  module2.exports = {
483
658
  name: "aiopt",
484
- version: "0.2.9",
659
+ version: "0.3.1",
485
660
  description: "Pre-deploy LLM cost accident guardrail (serverless local CLI)",
486
661
  bin: {
487
662
  aiopt: "dist/cli.js"
@@ -522,19 +697,19 @@ __export(install_exports, {
522
697
  runInstall: () => runInstall
523
698
  });
524
699
  function ensureDir2(p) {
525
- import_fs4.default.mkdirSync(p, { recursive: true });
700
+ import_fs5.default.mkdirSync(p, { recursive: true });
526
701
  }
527
702
  function writeFile(filePath, content, force) {
528
- if (!force && import_fs4.default.existsSync(filePath)) return { wrote: false, reason: "exists" };
529
- ensureDir2(import_path4.default.dirname(filePath));
530
- import_fs4.default.writeFileSync(filePath, content);
703
+ if (!force && import_fs5.default.existsSync(filePath)) return { wrote: false, reason: "exists" };
704
+ ensureDir2(import_path6.default.dirname(filePath));
705
+ import_fs5.default.writeFileSync(filePath, content);
531
706
  return { wrote: true };
532
707
  }
533
708
  function runInstall(cwd, opts) {
534
709
  const force = Boolean(opts.force);
535
- const aioptDir = import_path4.default.join(cwd, "aiopt");
536
- const policiesDir = import_path4.default.join(aioptDir, "policies");
537
- const outDir = import_path4.default.join(cwd, "aiopt-output");
710
+ const aioptDir = import_path6.default.join(cwd, "aiopt");
711
+ const policiesDir = import_path6.default.join(aioptDir, "policies");
712
+ const outDir = import_path6.default.join(cwd, "aiopt-output");
538
713
  ensureDir2(aioptDir);
539
714
  ensureDir2(policiesDir);
540
715
  ensureDir2(outDir);
@@ -590,7 +765,7 @@ return {
590
765
  };
591
766
  \`\`\`
592
767
  `;
593
- const r1 = writeFile(import_path4.default.join(aioptDir, "README.md"), readme, force);
768
+ const r1 = writeFile(import_path6.default.join(aioptDir, "README.md"), readme, force);
594
769
  created.push({ path: "aiopt/README.md", status: r1.wrote ? "created" : "skipped" });
595
770
  const config = {
596
771
  version: 1,
@@ -600,7 +775,7 @@ return {
600
775
  policies_dir: "./aiopt/policies",
601
776
  rate_table: { path: "./rates/rate_table.json" }
602
777
  };
603
- const r2 = writeFile(import_path4.default.join(aioptDir, "aiopt.config.json"), JSON.stringify(config, null, 2) + "\n", force);
778
+ const r2 = writeFile(import_path6.default.join(aioptDir, "aiopt.config.json"), JSON.stringify(config, null, 2) + "\n", force);
604
779
  created.push({ path: "aiopt/aiopt.config.json", status: r2.wrote ? "created" : "skipped" });
605
780
  const routing = {
606
781
  version: 1,
@@ -631,15 +806,15 @@ return {
631
806
  reduce_top_percentile: 0.2,
632
807
  assumed_reduction_ratio: 0.25
633
808
  };
634
- const p1 = writeFile(import_path4.default.join(policiesDir, "routing.json"), JSON.stringify(routing, null, 2) + "\n", force);
635
- const p2 = writeFile(import_path4.default.join(policiesDir, "retry.json"), JSON.stringify(retry, null, 2) + "\n", force);
636
- const p3 = writeFile(import_path4.default.join(policiesDir, "output.json"), JSON.stringify(output, null, 2) + "\n", force);
637
- const p4 = writeFile(import_path4.default.join(policiesDir, "context.json"), JSON.stringify(context, null, 2) + "\n", force);
809
+ const p1 = writeFile(import_path6.default.join(policiesDir, "routing.json"), JSON.stringify(routing, null, 2) + "\n", force);
810
+ const p2 = writeFile(import_path6.default.join(policiesDir, "retry.json"), JSON.stringify(retry, null, 2) + "\n", force);
811
+ const p3 = writeFile(import_path6.default.join(policiesDir, "output.json"), JSON.stringify(output, null, 2) + "\n", force);
812
+ const p4 = writeFile(import_path6.default.join(policiesDir, "context.json"), JSON.stringify(context, null, 2) + "\n", force);
638
813
  created.push({ path: "aiopt/policies/routing.json", status: p1.wrote ? "created" : "skipped" });
639
814
  created.push({ path: "aiopt/policies/retry.json", status: p2.wrote ? "created" : "skipped" });
640
815
  created.push({ path: "aiopt/policies/output.json", status: p3.wrote ? "created" : "skipped" });
641
816
  created.push({ path: "aiopt/policies/context.json", status: p4.wrote ? "created" : "skipped" });
642
- const wrapperPath = import_path4.default.join(aioptDir, "aiopt-wrapper.js");
817
+ const wrapperPath = import_path6.default.join(aioptDir, "aiopt-wrapper.js");
643
818
  const wrapper = `// AIOpt Wrapper (guardrails) \u2014 local-only (CommonJS)
644
819
 
645
820
  const fs = require('fs');
@@ -793,9 +968,9 @@ module.exports = { guardedCall };
793
968
  ;
794
969
  const w = writeFile(wrapperPath, wrapper, force);
795
970
  created.push({ path: "aiopt/aiopt-wrapper.js", status: w.wrote ? "created" : "skipped" });
796
- const usagePath = import_path4.default.join(outDir, "usage.jsonl");
797
- if (force || !import_fs4.default.existsSync(usagePath)) {
798
- import_fs4.default.writeFileSync(usagePath, "");
971
+ const usagePath = import_path6.default.join(outDir, "usage.jsonl");
972
+ if (force || !import_fs5.default.existsSync(usagePath)) {
973
+ import_fs5.default.writeFileSync(usagePath, "");
799
974
  created.push({ path: "aiopt-output/usage.jsonl", status: "created" });
800
975
  if (opts.seedSample) {
801
976
  const sample = {
@@ -815,19 +990,19 @@ module.exports = { guardedCall };
815
990
  latency_ms: 1,
816
991
  meta: { feature_tag: "demo", routed_from: null, policy_hits: ["install-sample"] }
817
992
  };
818
- import_fs4.default.appendFileSync(usagePath, JSON.stringify(sample) + "\n");
993
+ import_fs5.default.appendFileSync(usagePath, JSON.stringify(sample) + "\n");
819
994
  }
820
995
  } else {
821
996
  created.push({ path: "aiopt-output/usage.jsonl", status: "skipped" });
822
997
  }
823
998
  return { created };
824
999
  }
825
- var import_fs4, import_path4;
1000
+ var import_fs5, import_path6;
826
1001
  var init_install = __esm({
827
1002
  "src/install.ts"() {
828
1003
  "use strict";
829
- import_fs4 = __toESM(require("fs"));
830
- import_path4 = __toESM(require("path"));
1004
+ import_fs5 = __toESM(require("fs"));
1005
+ import_path6 = __toESM(require("path"));
831
1006
  }
832
1007
  });
833
1008
 
@@ -838,10 +1013,10 @@ __export(doctor_exports, {
838
1013
  });
839
1014
  function canWrite(dir) {
840
1015
  try {
841
- import_fs5.default.mkdirSync(dir, { recursive: true });
842
- const p = import_path5.default.join(dir, `.aiopt-write-test-${Date.now()}`);
843
- import_fs5.default.writeFileSync(p, "ok");
844
- import_fs5.default.unlinkSync(p);
1016
+ import_fs6.default.mkdirSync(dir, { recursive: true });
1017
+ const p = import_path7.default.join(dir, `.aiopt-write-test-${Date.now()}`);
1018
+ import_fs6.default.writeFileSync(p, "ok");
1019
+ import_fs6.default.unlinkSync(p);
845
1020
  return true;
846
1021
  } catch {
847
1022
  return false;
@@ -849,7 +1024,7 @@ function canWrite(dir) {
849
1024
  }
850
1025
  function tailLines(filePath, n) {
851
1026
  try {
852
- const raw = import_fs5.default.readFileSync(filePath, "utf8");
1027
+ const raw = import_fs6.default.readFileSync(filePath, "utf8");
853
1028
  const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
854
1029
  return lines.slice(Math.max(0, lines.length - n));
855
1030
  } catch {
@@ -857,15 +1032,15 @@ function tailLines(filePath, n) {
857
1032
  }
858
1033
  }
859
1034
  function runDoctor(cwd) {
860
- const aioptDir = import_path5.default.join(cwd, "aiopt");
861
- const policiesDir = import_path5.default.join(aioptDir, "policies");
862
- const outDir = import_path5.default.join(cwd, "aiopt-output");
863
- const usagePath = import_path5.default.join(outDir, "usage.jsonl");
1035
+ const aioptDir = import_path7.default.join(cwd, "aiopt");
1036
+ const policiesDir = import_path7.default.join(aioptDir, "policies");
1037
+ const outDir = import_path7.default.join(cwd, "aiopt-output");
1038
+ const usagePath = import_path7.default.join(outDir, "usage.jsonl");
864
1039
  const checks = [];
865
- checks.push({ name: "aiopt/ exists", ok: import_fs5.default.existsSync(aioptDir) });
866
- checks.push({ name: "aiopt/policies exists", ok: import_fs5.default.existsSync(policiesDir) });
1040
+ checks.push({ name: "aiopt/ exists", ok: import_fs6.default.existsSync(aioptDir) });
1041
+ checks.push({ name: "aiopt/policies exists", ok: import_fs6.default.existsSync(policiesDir) });
867
1042
  checks.push({ name: "aiopt-output/ writable", ok: canWrite(outDir) });
868
- checks.push({ name: "usage.jsonl exists", ok: import_fs5.default.existsSync(usagePath), detail: usagePath });
1043
+ checks.push({ name: "usage.jsonl exists", ok: import_fs6.default.existsSync(usagePath), detail: usagePath });
869
1044
  const last5raw = tailLines(usagePath, 5);
870
1045
  const last5 = last5raw.length === 0 ? [{ status: "(empty usage.jsonl)" }] : last5raw.map((l) => {
871
1046
  try {
@@ -903,12 +1078,12 @@ function runDoctor(cwd) {
903
1078
  const ok = checks.every((c) => c.ok);
904
1079
  return { ok, checks, last5 };
905
1080
  }
906
- var import_fs5, import_path5;
1081
+ var import_fs6, import_path7;
907
1082
  var init_doctor = __esm({
908
1083
  "src/doctor.ts"() {
909
1084
  "use strict";
910
- import_fs5 = __toESM(require("fs"));
911
- import_path5 = __toESM(require("path"));
1085
+ import_fs6 = __toESM(require("fs"));
1086
+ import_path7 = __toESM(require("path"));
912
1087
  }
913
1088
  });
914
1089
 
@@ -963,7 +1138,7 @@ function verifyLicenseKey(key, publicKeyPem) {
963
1138
  }
964
1139
  }
965
1140
  function defaultLicensePath(cwd) {
966
- return import_path6.default.join(cwd, "aiopt", "license.json");
1141
+ return import_path8.default.join(cwd, "aiopt", "license.json");
967
1142
  }
968
1143
  function writeLicenseFile(p, key, payload, verified) {
969
1144
  const out = {
@@ -972,19 +1147,19 @@ function writeLicenseFile(p, key, payload, verified) {
972
1147
  verified,
973
1148
  verified_at: (/* @__PURE__ */ new Date()).toISOString()
974
1149
  };
975
- import_fs6.default.mkdirSync(import_path6.default.dirname(p), { recursive: true });
976
- import_fs6.default.writeFileSync(p, JSON.stringify(out, null, 2));
1150
+ import_fs7.default.mkdirSync(import_path8.default.dirname(p), { recursive: true });
1151
+ import_fs7.default.writeFileSync(p, JSON.stringify(out, null, 2));
977
1152
  }
978
1153
  function readLicenseFile(p) {
979
- return JSON.parse(import_fs6.default.readFileSync(p, "utf8"));
1154
+ return JSON.parse(import_fs7.default.readFileSync(p, "utf8"));
980
1155
  }
981
- var import_crypto, import_fs6, import_path6, DEFAULT_PUBLIC_KEY_PEM;
1156
+ var import_crypto, import_fs7, import_path8, DEFAULT_PUBLIC_KEY_PEM;
982
1157
  var init_license = __esm({
983
1158
  "src/license.ts"() {
984
1159
  "use strict";
985
1160
  import_crypto = __toESM(require("crypto"));
986
- import_fs6 = __toESM(require("fs"));
987
- import_path6 = __toESM(require("path"));
1161
+ import_fs7 = __toESM(require("fs"));
1162
+ import_path8 = __toESM(require("path"));
988
1163
  DEFAULT_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
989
1164
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz1LLE/pXIx5TloDa0LAf
990
1165
  jg9NSIW6STWhsAFP2ZzXgpWoQ3cCmW6xcB/4QNEmPpGlfMWhyRfkxsdKuhnjUMTg
@@ -997,6 +1172,284 @@ DwIDAQAB
997
1172
  }
998
1173
  });
999
1174
 
1175
+ // src/gate.ts
1176
+ var gate_exports = {};
1177
+ __export(gate_exports, {
1178
+ formatGateStdout: () => formatGateStdout,
1179
+ runGate: () => runGate
1180
+ });
1181
+ function parseFileUri(uri) {
1182
+ try {
1183
+ if (!uri) return "";
1184
+ const u = uri.replace(/^file:\/\//, "");
1185
+ const norm = u.replace(/^\/+/, "").replace(/%20/g, " ");
1186
+ return norm;
1187
+ } catch {
1188
+ return uri;
1189
+ }
1190
+ }
1191
+ function runGate(outDir, cwd) {
1192
+ const sarifPath = import_path9.default.join(outDir, "aiopt.sarif");
1193
+ if (!import_fs8.default.existsSync(sarifPath)) {
1194
+ return { violations: 0, top3: [] };
1195
+ }
1196
+ let sarif;
1197
+ try {
1198
+ sarif = JSON.parse(import_fs8.default.readFileSync(sarifPath, "utf8"));
1199
+ } catch {
1200
+ return { violations: 0, top3: [] };
1201
+ }
1202
+ const results = sarif.runs?.[0]?.results || [];
1203
+ const viol = results.filter((r) => {
1204
+ const lvl = String(r.level || "").toLowerCase();
1205
+ return lvl === "warning" || lvl === "error";
1206
+ });
1207
+ const locs = [];
1208
+ for (const r of viol) {
1209
+ const loc = r.locations?.[0]?.physicalLocation;
1210
+ const uri = loc?.artifactLocation?.uri || "";
1211
+ const line = Number(loc?.region?.startLine || 1);
1212
+ let file = parseFileUri(uri);
1213
+ try {
1214
+ const abs = import_path9.default.isAbsolute(file) ? file : import_path9.default.resolve(file);
1215
+ file = import_path9.default.relative(cwd, abs) || file;
1216
+ } catch {
1217
+ }
1218
+ locs.push({ file, line: Number.isFinite(line) && line > 0 ? line : 1 });
1219
+ }
1220
+ const top3 = locs.slice(0, 3);
1221
+ return { violations: viol.length, top3 };
1222
+ }
1223
+ function formatGateStdout(r, outDir) {
1224
+ const lines = [];
1225
+ if (r.violations <= 0) {
1226
+ lines.push("OK: no policy violations");
1227
+ lines.push(`Artifacts: ${import_path9.default.join(outDir, "report.md")} | ${import_path9.default.join(outDir, "aiopt.sarif")}`);
1228
+ return { text: lines.join("\n"), exitCode: 0 };
1229
+ }
1230
+ lines.push(`FAIL: policy violations=${r.violations}`);
1231
+ lines.push("Top3:");
1232
+ for (const x of r.top3) lines.push(`- ${x.file}:${x.line}`);
1233
+ lines.push("See artifacts: aiopt-output/report.md | aiopt-output/aiopt.sarif | aiopt-output/patches/");
1234
+ const text = lines.slice(0, 10).join("\n");
1235
+ return { text, exitCode: 1 };
1236
+ }
1237
+ var import_fs8, import_path9;
1238
+ var init_gate = __esm({
1239
+ "src/gate.ts"() {
1240
+ "use strict";
1241
+ import_fs8 = __toESM(require("fs"));
1242
+ import_path9 = __toESM(require("path"));
1243
+ }
1244
+ });
1245
+
1246
+ // src/fix.ts
1247
+ var fix_exports = {};
1248
+ __export(fix_exports, {
1249
+ runFix: () => runFix
1250
+ });
1251
+ function isTextLike2(p) {
1252
+ const ext = import_path10.default.extname(p).toLowerCase();
1253
+ return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext);
1254
+ }
1255
+ function walk2(root, out) {
1256
+ const items = import_fs9.default.readdirSync(root, { withFileTypes: true });
1257
+ for (const it of items) {
1258
+ if (it.name.startsWith(".")) continue;
1259
+ if (DEFAULT_EXCLUDES2.has(it.name)) continue;
1260
+ const full = import_path10.default.join(root, it.name);
1261
+ if (it.isDirectory()) walk2(full, out);
1262
+ else out.push(full);
1263
+ }
1264
+ }
1265
+ function insertModelConst(content) {
1266
+ if (/\bAIOPT_MODEL\b/.test(content)) return { changed: false, next: content };
1267
+ if (!/\bprocess\.env\.AIOPT_MODEL\b/.test(content)) {
1268
+ }
1269
+ const line = "const AIOPT_MODEL = process.env.AIOPT_MODEL || 'gpt-5.2-mini';";
1270
+ const lines = content.split(/\r?\n/);
1271
+ let i = 0;
1272
+ if (lines[0] && lines[0].startsWith("#!")) i++;
1273
+ while (i < lines.length) {
1274
+ const t = lines[i].trim();
1275
+ if (t.startsWith("import ")) {
1276
+ i++;
1277
+ continue;
1278
+ }
1279
+ if (t.startsWith("const ") && t.includes("require(")) {
1280
+ i++;
1281
+ continue;
1282
+ }
1283
+ if (t === "" || t.startsWith("//") || t.startsWith("/*")) {
1284
+ i++;
1285
+ continue;
1286
+ }
1287
+ break;
1288
+ }
1289
+ lines.splice(i, 0, line, "");
1290
+ return { changed: true, next: lines.join("\n") };
1291
+ }
1292
+ function applyRetryCap(content) {
1293
+ let changed = false;
1294
+ let next = content;
1295
+ next = next.replace(/\b(maxRetries|maximumRetries|max_attempts|attempts)\s*:\s*(\d+)\b/gi, (m, key, num) => {
1296
+ const n = Number(num);
1297
+ if (!Number.isFinite(n) || n < 4) return m;
1298
+ changed = true;
1299
+ return `${key}: 3`;
1300
+ });
1301
+ next = next.replace(/\b(retries)\s*:\s*(\d+)\b/gi, (m, key, num) => {
1302
+ const n = Number(num);
1303
+ if (!Number.isFinite(n) || n < 4) return m;
1304
+ changed = true;
1305
+ return `${key}: 3`;
1306
+ });
1307
+ return { changed, next };
1308
+ }
1309
+ function applyModelRouting(content) {
1310
+ let changed = false;
1311
+ let next = content;
1312
+ const re = /(\bmodel\s*[:=]\s*)(['"])gpt-5\.2\2/g;
1313
+ if (re.test(next)) {
1314
+ const ins = insertModelConst(next);
1315
+ next = ins.next;
1316
+ next = next.replace(re, (_m, prefix) => {
1317
+ changed = true;
1318
+ return `${prefix}AIOPT_MODEL`;
1319
+ });
1320
+ const re2 = /(\bmodel\s*[:=]\s*)(['"])openai-codex\/gpt-5\.2\2/g;
1321
+ next = next.replace(re2, (_m, prefix) => {
1322
+ changed = true;
1323
+ return `${prefix}(process.env.AIOPT_MODEL_FULL || 'openai-codex/gpt-5.2-mini')`;
1324
+ });
1325
+ }
1326
+ return { changed, next };
1327
+ }
1328
+ function tmpFilePath(original) {
1329
+ const base = import_path10.default.basename(original);
1330
+ const rand = Math.random().toString(16).slice(2);
1331
+ return import_path10.default.join(import_os.default.tmpdir(), `aiopt-fix-${base}-${rand}`);
1332
+ }
1333
+ function diffNoIndex(oldPath, newPath) {
1334
+ try {
1335
+ return (0, import_child_process.execSync)(`git diff --no-index -- ${JSON.stringify(oldPath)} ${JSON.stringify(newPath)}`, {
1336
+ encoding: "utf8",
1337
+ stdio: ["ignore", "pipe", "pipe"]
1338
+ });
1339
+ } catch (e) {
1340
+ const out = String(e?.stdout || "");
1341
+ return out;
1342
+ }
1343
+ }
1344
+ function normalizePatchPaths(diffText, rel) {
1345
+ const raw = diffText.split(/\r?\n/);
1346
+ const lines = [];
1347
+ for (let i = 0; i < raw.length; i++) {
1348
+ const line = raw[i];
1349
+ if (line.startsWith("old mode ") || line.startsWith("new mode ")) continue;
1350
+ if (line.startsWith("diff --git ")) {
1351
+ lines.push(`diff --git a/${rel} b/${rel}`);
1352
+ continue;
1353
+ }
1354
+ if (line.startsWith("--- ")) {
1355
+ lines.push(`--- a/${rel}`);
1356
+ continue;
1357
+ }
1358
+ if (line.startsWith("+++ ")) {
1359
+ lines.push(`+++ b/${rel}`);
1360
+ continue;
1361
+ }
1362
+ lines.push(line);
1363
+ }
1364
+ return lines.join("\n");
1365
+ }
1366
+ function runFix(cwd, opts) {
1367
+ const files = [];
1368
+ walk2(cwd, files);
1369
+ const patches = [];
1370
+ const changedFiles = [];
1371
+ for (const file of files) {
1372
+ if (!isTextLike2(file)) continue;
1373
+ let st;
1374
+ try {
1375
+ st = import_fs9.default.statSync(file);
1376
+ } catch {
1377
+ continue;
1378
+ }
1379
+ if (!st.isFile()) continue;
1380
+ if (st.size > 1024 * 1024) continue;
1381
+ let content = "";
1382
+ try {
1383
+ content = import_fs9.default.readFileSync(file, "utf8");
1384
+ } catch {
1385
+ continue;
1386
+ }
1387
+ const r1 = applyRetryCap(content);
1388
+ const r2 = applyModelRouting(r1.next);
1389
+ const next = r2.next;
1390
+ if (next === content) continue;
1391
+ const tmp = tmpFilePath(file);
1392
+ import_fs9.default.writeFileSync(tmp, next);
1393
+ const rel = import_path10.default.relative(cwd, file).replace(/\\/g, "/");
1394
+ const d0 = diffNoIndex(file, tmp);
1395
+ const d = normalizePatchPaths(d0, rel);
1396
+ if (d && d.trim().length > 0) {
1397
+ patches.push(d);
1398
+ changedFiles.push(rel);
1399
+ }
1400
+ try {
1401
+ import_fs9.default.unlinkSync(tmp);
1402
+ } catch {
1403
+ }
1404
+ if (patches.join("\n").length > 5e5) break;
1405
+ }
1406
+ import_fs9.default.mkdirSync(opts.outDir, { recursive: true });
1407
+ const patchPath = import_path10.default.join(opts.outDir, "aiopt.patch");
1408
+ const header = [
1409
+ "# AIOpt patch (generated)",
1410
+ "# - retry cap: reduce high retry/attempt counts to 3",
1411
+ "# - model routing: cheap default via AIOPT_MODEL env override",
1412
+ ""
1413
+ ].join("\n");
1414
+ import_fs9.default.writeFileSync(patchPath, header + patches.join("\n"));
1415
+ if (opts.apply) {
1416
+ try {
1417
+ const inside = (0, import_child_process.execSync)("git rev-parse --is-inside-work-tree", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1418
+ if (inside !== "true") {
1419
+ return { ok: false, applied: false, patchPath, changedFiles, hint: "not inside a git work tree (run from your repo root, or omit --apply and apply manually)" };
1420
+ }
1421
+ } catch {
1422
+ return { ok: false, applied: false, patchPath, changedFiles, hint: "git not available or not a git repo (omit --apply and apply manually)" };
1423
+ }
1424
+ try {
1425
+ (0, import_child_process.execSync)(`git apply ${JSON.stringify(patchPath)}`, { stdio: "inherit" });
1426
+ return { ok: true, applied: true, patchPath, changedFiles };
1427
+ } catch {
1428
+ return { ok: false, applied: false, patchPath, changedFiles, hint: "git apply failed. Ensure a clean working tree, then re-run; or open aiopt.patch and apply manually." };
1429
+ }
1430
+ }
1431
+ return { ok: true, applied: false, patchPath, changedFiles };
1432
+ }
1433
+ var import_fs9, import_path10, import_os, import_child_process, DEFAULT_EXCLUDES2;
1434
+ var init_fix = __esm({
1435
+ "src/fix.ts"() {
1436
+ "use strict";
1437
+ import_fs9 = __toESM(require("fs"));
1438
+ import_path10 = __toESM(require("path"));
1439
+ import_os = __toESM(require("os"));
1440
+ import_child_process = require("child_process");
1441
+ DEFAULT_EXCLUDES2 = /* @__PURE__ */ new Set([
1442
+ ".git",
1443
+ "node_modules",
1444
+ "dist",
1445
+ "aiopt-output",
1446
+ ".next",
1447
+ "build",
1448
+ "coverage"
1449
+ ]);
1450
+ }
1451
+ });
1452
+
1000
1453
  // src/guard.ts
1001
1454
  var guard_exports = {};
1002
1455
  __export(guard_exports, {
@@ -1221,12 +1674,12 @@ __export(dashboard_exports, {
1221
1674
  async function startDashboard(cwd, opts) {
1222
1675
  const host = "127.0.0.1";
1223
1676
  const port = opts.port || 3010;
1224
- const outDir = import_path7.default.join(cwd, "aiopt-output");
1225
- const file = (name) => import_path7.default.join(outDir, name);
1677
+ const outDir = import_path11.default.join(cwd, "aiopt-output");
1678
+ const file = (name) => import_path11.default.join(outDir, name);
1226
1679
  function readOrNull(p) {
1227
1680
  try {
1228
- if (!import_fs7.default.existsSync(p)) return null;
1229
- return import_fs7.default.readFileSync(p, "utf8");
1681
+ if (!import_fs10.default.existsSync(p)) return null;
1682
+ return import_fs10.default.readFileSync(p, "utf8");
1230
1683
  } catch {
1231
1684
  return null;
1232
1685
  }
@@ -1295,7 +1748,10 @@ async function startDashboard(cwd, opts) {
1295
1748
  <body>
1296
1749
  <div class="wrap">
1297
1750
  <div class="top">
1298
- <div class="h1">AIOpt Local Dashboard</div>
1751
+ <div>
1752
+ <div class="h1">AIOpt Local Dashboard</div>
1753
+ <div class="mini" id="baseDir">base: \u2014</div>
1754
+ </div>
1299
1755
  <div class="pill"><span class="dot"></span> local-only \xB7 reads <span class="k">./aiopt-output</span></div>
1300
1756
  </div>
1301
1757
 
@@ -1389,6 +1845,16 @@ function renderBars(el, items){
1389
1845
  }
1390
1846
 
1391
1847
  async function load(){
1848
+ const meta = await fetch('/api/_meta').then(r=>r.ok?r.json():null);
1849
+ if(meta && meta.baseDir){
1850
+ document.getElementById('baseDir').textContent = 'base: ' + meta.baseDir;
1851
+ }
1852
+ if(meta && meta.missing && meta.missing.length){
1853
+ // show missing files hint in the guard panel if nothing else yet
1854
+ // (prevents users from thinking it is stuck on loading)
1855
+ document.getElementById('guard').textContent = '(missing: ' + meta.missing.join(', ') + ')';
1856
+ }
1857
+
1392
1858
  const guardTxt = await fetch('/api/guard-last.txt').then(r=>r.ok?r.text():null);
1393
1859
  const guardMeta = await fetch('/api/guard-last.json').then(r=>r.ok?r.json():null);
1394
1860
 
@@ -1508,6 +1974,13 @@ load();
1508
1974
  }
1509
1975
  if (url.startsWith("/api/")) {
1510
1976
  const name = url.replace("/api/", "");
1977
+ if (name === "_meta") {
1978
+ const expected = ["guard-last.txt", "guard-last.json", "report.json", "report.md", "usage.jsonl", "guard-history.jsonl"];
1979
+ const missing = expected.filter((f) => !import_fs10.default.existsSync(file(f)));
1980
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1981
+ res.end(JSON.stringify({ baseDir: cwd, outDir, missing }, null, 2));
1982
+ return;
1983
+ }
1511
1984
  const allow = /* @__PURE__ */ new Set(["guard-last.txt", "guard-last.json", "guard-history.jsonl", "report.md", "report.json", "usage.jsonl"]);
1512
1985
  if (!allow.has(name)) {
1513
1986
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
@@ -1538,27 +2011,71 @@ load();
1538
2011
  await new Promise(() => {
1539
2012
  });
1540
2013
  }
1541
- var import_http, import_fs7, import_path7;
2014
+ var import_http, import_fs10, import_path11;
1542
2015
  var init_dashboard = __esm({
1543
2016
  "src/dashboard.ts"() {
1544
2017
  "use strict";
1545
2018
  import_http = __toESM(require("http"));
1546
- import_fs7 = __toESM(require("fs"));
1547
- import_path7 = __toESM(require("path"));
2019
+ import_fs10 = __toESM(require("fs"));
2020
+ import_path11 = __toESM(require("path"));
2021
+ }
2022
+ });
2023
+
2024
+ // src/find-output.ts
2025
+ var find_output_exports = {};
2026
+ __export(find_output_exports, {
2027
+ findAioptOutputDir: () => findAioptOutputDir
2028
+ });
2029
+ function findAioptOutputDir(startCwd) {
2030
+ let cur = import_path12.default.resolve(startCwd);
2031
+ while (true) {
2032
+ const outDir = import_path12.default.join(cur, "aiopt-output");
2033
+ if (import_fs11.default.existsSync(outDir)) {
2034
+ try {
2035
+ if (import_fs11.default.statSync(outDir).isDirectory()) return { cwd: cur, outDir };
2036
+ } catch {
2037
+ }
2038
+ }
2039
+ const parent = import_path12.default.dirname(cur);
2040
+ if (parent === cur) break;
2041
+ cur = parent;
2042
+ }
2043
+ try {
2044
+ const base = import_path12.default.resolve(startCwd);
2045
+ const children = import_fs11.default.readdirSync(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => import_path12.default.join(base, d.name));
2046
+ for (const child of children) {
2047
+ const outDir = import_path12.default.join(child, "aiopt-output");
2048
+ if (import_fs11.default.existsSync(outDir)) {
2049
+ try {
2050
+ if (import_fs11.default.statSync(outDir).isDirectory()) return { cwd: child, outDir };
2051
+ } catch {
2052
+ }
2053
+ }
2054
+ }
2055
+ } catch {
2056
+ }
2057
+ return { cwd: import_path12.default.resolve(startCwd), outDir: import_path12.default.join(import_path12.default.resolve(startCwd), "aiopt-output") };
2058
+ }
2059
+ var import_fs11, import_path12;
2060
+ var init_find_output = __esm({
2061
+ "src/find-output.ts"() {
2062
+ "use strict";
2063
+ import_fs11 = __toESM(require("fs"));
2064
+ import_path12 = __toESM(require("path"));
1548
2065
  }
1549
2066
  });
1550
2067
 
1551
2068
  // src/rates-util.ts
1552
2069
  function loadRateTableFromDistPath() {
1553
- const p = import_path8.default.join(__dirname, "..", "rates", "rate_table.json");
1554
- return JSON.parse(import_fs8.default.readFileSync(p, "utf8"));
2070
+ const p = import_path13.default.join(__dirname, "..", "rates", "rate_table.json");
2071
+ return JSON.parse(import_fs12.default.readFileSync(p, "utf8"));
1555
2072
  }
1556
- var import_fs8, import_path8;
2073
+ var import_fs12, import_path13;
1557
2074
  var init_rates_util = __esm({
1558
2075
  "src/rates-util.ts"() {
1559
2076
  "use strict";
1560
- import_fs8 = __toESM(require("fs"));
1561
- import_path8 = __toESM(require("path"));
2077
+ import_fs12 = __toESM(require("fs"));
2078
+ import_path13 = __toESM(require("path"));
1562
2079
  }
1563
2080
  });
1564
2081
 
@@ -1569,8 +2086,8 @@ __export(quickstart_exports, {
1569
2086
  seedDemoUsage: () => seedDemoUsage
1570
2087
  });
1571
2088
  function seedDemoUsage(outDir) {
1572
- import_fs9.default.mkdirSync(outDir, { recursive: true });
1573
- const p = import_path9.default.join(outDir, "usage.jsonl");
2089
+ import_fs13.default.mkdirSync(outDir, { recursive: true });
2090
+ const p = import_path14.default.join(outDir, "usage.jsonl");
1574
2091
  const now = Date.now();
1575
2092
  const lines = [];
1576
2093
  for (let i = 0; i < 60; i++) {
@@ -1587,18 +2104,18 @@ function seedDemoUsage(outDir) {
1587
2104
  meta: { feature_tag: i % 2 ? "summarize" : "coding" }
1588
2105
  });
1589
2106
  }
1590
- import_fs9.default.writeFileSync(p, lines.map((x) => JSON.stringify(x)).join("\n") + "\n");
2107
+ import_fs13.default.writeFileSync(p, lines.map((x) => JSON.stringify(x)).join("\n") + "\n");
1591
2108
  return p;
1592
2109
  }
1593
2110
  function runQuickstart(cwd, opts) {
1594
- const outDir = import_path9.default.join(cwd, "aiopt-output");
2111
+ const outDir = import_path14.default.join(cwd, "aiopt-output");
1595
2112
  const usagePath = seedDemoUsage(outDir);
1596
2113
  const rt = loadRateTableFromDistPath();
1597
2114
  const { readJsonl: readJsonl2 } = (init_io(), __toCommonJS(io_exports));
1598
2115
  const events = readJsonl2(usagePath);
1599
2116
  const { analysis, savings, policy, meta } = analyze(rt, events);
1600
- import_fs9.default.writeFileSync(import_path9.default.join(outDir, "analysis.json"), JSON.stringify(analysis, null, 2));
1601
- import_fs9.default.writeFileSync(import_path9.default.join(outDir, "report.json"), JSON.stringify({
2117
+ import_fs13.default.writeFileSync(import_path14.default.join(outDir, "analysis.json"), JSON.stringify(analysis, null, 2));
2118
+ import_fs13.default.writeFileSync(import_path14.default.join(outDir, "report.json"), JSON.stringify({
1602
2119
  version: 3,
1603
2120
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1604
2121
  confidence: analysis.unknown_models?.length ? "MEDIUM" : "HIGH",
@@ -1618,8 +2135,8 @@ function runQuickstart(cwd, opts) {
1618
2135
  unknown_models: analysis.unknown_models || [],
1619
2136
  notes: []
1620
2137
  }, null, 2));
1621
- import_fs9.default.writeFileSync(import_path9.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
1622
- import_fs9.default.writeFileSync(import_path9.default.join(outDir, "report.md"), "# AIOpt quickstart demo\n\nThis is a demo report generated by `aiopt quickstart --demo`.\n");
2138
+ import_fs13.default.writeFileSync(import_path14.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
2139
+ import_fs13.default.writeFileSync(import_path14.default.join(outDir, "report.md"), "# AIOpt quickstart demo\n\nThis is a demo report generated by `aiopt quickstart --demo`.\n");
1623
2140
  const r = runGuard(rt, {
1624
2141
  baselineEvents: events,
1625
2142
  candidate: {
@@ -1630,12 +2147,12 @@ function runQuickstart(cwd, opts) {
1630
2147
  });
1631
2148
  return { usagePath, outDir, guard: r, port: opts.port };
1632
2149
  }
1633
- var import_fs9, import_path9;
2150
+ var import_fs13, import_path14;
1634
2151
  var init_quickstart = __esm({
1635
2152
  "src/quickstart.ts"() {
1636
2153
  "use strict";
1637
- import_fs9 = __toESM(require("fs"));
1638
- import_path9 = __toESM(require("path"));
2154
+ import_fs13 = __toESM(require("fs"));
2155
+ import_path14 = __toESM(require("path"));
1639
2156
  init_scan();
1640
2157
  init_rates_util();
1641
2158
  init_guard();
@@ -1643,8 +2160,8 @@ var init_quickstart = __esm({
1643
2160
  });
1644
2161
 
1645
2162
  // src/cli.ts
1646
- var import_fs10 = __toESM(require("fs"));
1647
- var import_path10 = __toESM(require("path"));
2163
+ var import_fs14 = __toESM(require("fs"));
2164
+ var import_path15 = __toESM(require("path"));
1648
2165
  var import_commander = require("commander");
1649
2166
  init_io();
1650
2167
  init_scan();
@@ -1652,17 +2169,17 @@ var program = new import_commander.Command();
1652
2169
  var DEFAULT_INPUT = "./aiopt-output/usage.jsonl";
1653
2170
  var DEFAULT_OUTPUT_DIR = "./aiopt-output";
1654
2171
  function loadRateTable() {
1655
- const p = import_path10.default.join(__dirname, "..", "rates", "rate_table.json");
1656
- return JSON.parse(import_fs10.default.readFileSync(p, "utf8"));
2172
+ const p = import_path15.default.join(__dirname, "..", "rates", "rate_table.json");
2173
+ return JSON.parse(import_fs14.default.readFileSync(p, "utf8"));
1657
2174
  }
1658
2175
  program.name("aiopt").description("AI \uBE44\uC6A9 \uC790\uB3D9 \uC808\uAC10 \uC778\uD504\uB77C \u2014 \uC11C\uBC84 \uC5C6\uB294 \uB85C\uCEEC CLI MVP").version(require_package().version);
1659
2176
  program.command("init").description("aiopt-input/ \uBC0F \uC0D8\uD50C usage.jsonl, aiopt-output/ \uC0DD\uC131").action(() => {
1660
2177
  ensureDir("./aiopt-input");
1661
2178
  ensureDir("./aiopt-output");
1662
- const sampleSrc = import_path10.default.join(__dirname, "..", "samples", "sample_usage.jsonl");
1663
- const dst = import_path10.default.join("./aiopt-input", "usage.jsonl");
1664
- if (!import_fs10.default.existsSync(dst)) {
1665
- import_fs10.default.copyFileSync(sampleSrc, dst);
2179
+ const sampleSrc = import_path15.default.join(__dirname, "..", "samples", "sample_usage.jsonl");
2180
+ const dst = import_path15.default.join("./aiopt-input", "usage.jsonl");
2181
+ if (!import_fs14.default.existsSync(dst)) {
2182
+ import_fs14.default.copyFileSync(sampleSrc, dst);
1666
2183
  console.log("Created ./aiopt-input/usage.jsonl (sample)");
1667
2184
  } else {
1668
2185
  console.log("Exists ./aiopt-input/usage.jsonl (skip)");
@@ -1672,7 +2189,7 @@ program.command("init").description("aiopt-input/ \uBC0F \uC0D8\uD50C usage.json
1672
2189
  program.command("scan").description("\uC785\uB825 \uB85C\uADF8(JSONL/CSV)\uB97C \uBD84\uC11D\uD558\uACE0 report.md/report.json + patches\uAE4C\uC9C0 \uC0DD\uC131").option("--input <path>", "input file path (default: ./aiopt-output/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action(async (opts) => {
1673
2190
  const inputPath = String(opts.input);
1674
2191
  const outDir = String(opts.out);
1675
- if (!import_fs10.default.existsSync(inputPath)) {
2192
+ if (!import_fs14.default.existsSync(inputPath)) {
1676
2193
  console.error(`Input not found: ${inputPath}`);
1677
2194
  process.exit(1);
1678
2195
  }
@@ -1680,7 +2197,7 @@ program.command("scan").description("\uC785\uB825 \uB85C\uADF8(JSONL/CSV)\uB97C
1680
2197
  const events = isCsvPath(inputPath) ? readCsv(inputPath) : readJsonl(inputPath);
1681
2198
  const { analysis, savings, policy, meta } = analyze(rt, events);
1682
2199
  policy.generated_from.input = inputPath;
1683
- writeOutputs(outDir, analysis, savings, policy, meta);
2200
+ writeOutputs(outDir, analysis, savings, policy, { ...meta, cwd: process.cwd(), cliVersion: program.version() });
1684
2201
  const { buildTopFixes: buildTopFixes2 } = await Promise.resolve().then(() => (init_solutions(), solutions_exports));
1685
2202
  const fixes = buildTopFixes2(analysis, savings).slice(0, 3);
1686
2203
  console.log("Top Fix 3:");
@@ -1688,7 +2205,7 @@ program.command("scan").description("\uC785\uB825 \uB85C\uADF8(JSONL/CSV)\uB97C
1688
2205
  const tag = f.status === "no-issue" ? "(no issue detected)" : `($${Math.round(f.impact_usd * 100) / 100})`;
1689
2206
  console.log(`${i + 1}) ${f.title} ${tag}`);
1690
2207
  });
1691
- console.log(`Report: ${import_path10.default.join(outDir, "report.md")}`);
2208
+ console.log(`Report: ${import_path15.default.join(outDir, "report.md")}`);
1692
2209
  });
1693
2210
  program.command("policy").description("\uB9C8\uC9C0\uB9C9 scan \uACB0\uACFC \uAE30\uBC18\uC73C\uB85C cost-policy.json\uB9CC \uC7AC\uC0DD\uC131 (MVP: scan\uACFC \uB3D9\uC77C \uB85C\uC9C1)").option("--input <path>", "input file path (default: ./aiopt-input/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action((opts) => {
1694
2211
  const inputPath = String(opts.input);
@@ -1698,7 +2215,7 @@ program.command("policy").description("\uB9C8\uC9C0\uB9C9 scan \uACB0\uACFC \uAE
1698
2215
  const { policy } = analyze(rt, events);
1699
2216
  policy.generated_from.input = inputPath;
1700
2217
  ensureDir(outDir);
1701
- import_fs10.default.writeFileSync(import_path10.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
2218
+ import_fs14.default.writeFileSync(import_path15.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
1702
2219
  console.log(`OK: ${outDir}/cost-policy.json`);
1703
2220
  });
1704
2221
  program.command("install").description("Install AIOpt guardrails: create aiopt/ + policies + usage.jsonl").option("--force", "overwrite existing files").option("--seed-sample", "seed 1 sample line into aiopt-output/usage.jsonl").action(async (opts) => {
@@ -1738,7 +2255,7 @@ licenseCmd.command("verify").option("--path <path>", "license.json path (default
1738
2255
  const { DEFAULT_PUBLIC_KEY_PEM: DEFAULT_PUBLIC_KEY_PEM2, defaultLicensePath: defaultLicensePath2, readLicenseFile: readLicenseFile2, verifyLicenseKey: verifyLicenseKey2 } = await Promise.resolve().then(() => (init_license(), license_exports));
1739
2256
  const p = opts.path ? String(opts.path) : defaultLicensePath2(process.cwd());
1740
2257
  const pub = process.env.AIOPT_LICENSE_PUBKEY || DEFAULT_PUBLIC_KEY_PEM2;
1741
- if (!import_fs10.default.existsSync(p)) {
2258
+ if (!import_fs14.default.existsSync(p)) {
1742
2259
  console.error(`FAIL: license file not found: ${p}`);
1743
2260
  process.exit(3);
1744
2261
  }
@@ -1755,7 +2272,7 @@ licenseCmd.command("status").option("--path <path>", "license.json path (default
1755
2272
  const { DEFAULT_PUBLIC_KEY_PEM: DEFAULT_PUBLIC_KEY_PEM2, defaultLicensePath: defaultLicensePath2, readLicenseFile: readLicenseFile2, verifyLicenseKey: verifyLicenseKey2 } = await Promise.resolve().then(() => (init_license(), license_exports));
1756
2273
  const p = opts.path ? String(opts.path) : defaultLicensePath2(process.cwd());
1757
2274
  const pub = process.env.AIOPT_LICENSE_PUBKEY || DEFAULT_PUBLIC_KEY_PEM2;
1758
- if (!import_fs10.default.existsSync(p)) {
2275
+ if (!import_fs14.default.existsSync(p)) {
1759
2276
  console.log("NO_LICENSE");
1760
2277
  process.exit(2);
1761
2278
  }
@@ -1768,6 +2285,44 @@ licenseCmd.command("status").option("--path <path>", "license.json path (default
1768
2285
  console.log(`INVALID: ${v.reason || "unknown"}`);
1769
2286
  process.exit(3);
1770
2287
  });
2288
+ program.command("gate").description("Merge gate (CI-friendly): fail (exit 1) when policy violations are detected; prints <=10 lines").option("--input <path>", "input usage jsonl/csv (default: ./aiopt-output/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action(async (opts) => {
2289
+ const inputPath = String(opts.input);
2290
+ const outDir = String(opts.out);
2291
+ if (!import_fs14.default.existsSync(inputPath)) {
2292
+ console.error(`FAIL: input not found: ${inputPath}`);
2293
+ process.exit(1);
2294
+ }
2295
+ const rt = loadRateTable();
2296
+ const events = isCsvPath(inputPath) ? readCsv(inputPath) : readJsonl(inputPath);
2297
+ const { analysis, savings, policy, meta } = analyze(rt, events);
2298
+ policy.generated_from.input = inputPath;
2299
+ writeOutputs(outDir, analysis, savings, policy, { ...meta, cwd: process.cwd(), cliVersion: program.version() });
2300
+ const { runGate: runGate2, formatGateStdout: formatGateStdout2 } = await Promise.resolve().then(() => (init_gate(), gate_exports));
2301
+ const r = runGate2(outDir, process.cwd());
2302
+ const out = formatGateStdout2(r, outDir);
2303
+ console.log(out.text);
2304
+ process.exit(out.exitCode);
2305
+ });
2306
+ program.command("fix").description("Auto-fix suggestions: generate aiopt.patch (and optionally apply it via git apply)").option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).option("--apply", "apply the generated patch via git apply").action(async (opts) => {
2307
+ const outDir = String(opts.out);
2308
+ const { runFix: runFix2 } = await Promise.resolve().then(() => (init_fix(), fix_exports));
2309
+ const r = runFix2(process.cwd(), { outDir, apply: Boolean(opts.apply) });
2310
+ console.log(`Patch: ${r.patchPath}`);
2311
+ if (r.changedFiles.length) {
2312
+ console.log(`Files: ${r.changedFiles.slice(0, 10).join(", ")}${r.changedFiles.length > 10 ? " ..." : ""}`);
2313
+ } else {
2314
+ console.log("No changes suggested.");
2315
+ }
2316
+ if (r.applied) {
2317
+ console.log("OK: patch applied");
2318
+ process.exit(0);
2319
+ }
2320
+ if (!r.ok) {
2321
+ console.error(`FAIL: could not apply patch${r.hint ? ` (${r.hint})` : ""}`);
2322
+ process.exit(1);
2323
+ }
2324
+ process.exit(0);
2325
+ });
1771
2326
  program.command("guard").description("Pre-deploy guardrail: compare baseline usage vs candidate change (or diff two log sets) and print warnings (exit codes 0/2/3)").option("--input <path>", "baseline usage jsonl/csv (legacy alias for --baseline; default: ./aiopt-output/usage.jsonl)", DEFAULT_INPUT).option("--baseline <path>", "baseline usage jsonl/csv (diff mode when used with --candidate)").option("--candidate <path>", "candidate usage jsonl/csv (diff mode: compare two real log sets)").option("--provider <provider>", "candidate provider override (transform mode only)").option("--model <model>", "candidate model override (transform mode only)").option("--context-mult <n>", "multiply input_tokens by n (transform mode only)", (v) => Number(v)).option("--output-mult <n>", "multiply output_tokens by n (transform mode only)", (v) => Number(v)).option("--retries-delta <n>", "add n to retries (transform mode only)", (v) => Number(v)).option("--call-mult <n>", "multiply call volume by n (traffic spike)", (v) => Number(v)).option("--budget-monthly <usd>", "fail if estimated candidate monthly cost exceeds this budget", (v) => Number(v)).action(async (opts) => {
1772
2327
  const rt = loadRateTable();
1773
2328
  const baselinePath = String(opts.baseline || opts.input);
@@ -1777,11 +2332,11 @@ program.command("guard").description("Pre-deploy guardrail: compare baseline usa
1777
2332
  console.error("FAIL: diff mode requires both --baseline and --candidate");
1778
2333
  process.exit(3);
1779
2334
  }
1780
- if (!import_fs10.default.existsSync(baselinePath)) {
2335
+ if (!import_fs14.default.existsSync(baselinePath)) {
1781
2336
  console.error(`FAIL: baseline not found: ${baselinePath}`);
1782
2337
  process.exit(3);
1783
2338
  }
1784
- if (candidatePath && !import_fs10.default.existsSync(candidatePath)) {
2339
+ if (candidatePath && !import_fs14.default.existsSync(candidatePath)) {
1785
2340
  console.error(`FAIL: candidate not found: ${candidatePath}`);
1786
2341
  process.exit(3);
1787
2342
  }
@@ -1803,20 +2358,27 @@ program.command("guard").description("Pre-deploy guardrail: compare baseline usa
1803
2358
  });
1804
2359
  console.log(r.message);
1805
2360
  try {
1806
- const outDir = import_path10.default.resolve(DEFAULT_OUTPUT_DIR);
1807
- import_fs10.default.mkdirSync(outDir, { recursive: true });
2361
+ const outDir = import_path15.default.resolve(DEFAULT_OUTPUT_DIR);
2362
+ import_fs14.default.mkdirSync(outDir, { recursive: true });
1808
2363
  const ts = (/* @__PURE__ */ new Date()).toISOString();
1809
- import_fs10.default.writeFileSync(import_path10.default.join(outDir, "guard-last.txt"), r.message);
1810
- import_fs10.default.writeFileSync(import_path10.default.join(outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.exitCode }, null, 2));
2364
+ import_fs14.default.writeFileSync(import_path15.default.join(outDir, "guard-last.txt"), r.message);
2365
+ import_fs14.default.writeFileSync(import_path15.default.join(outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.exitCode }, null, 2));
1811
2366
  const histLine = JSON.stringify({ ts, exitCode: r.exitCode, mode: candidateEvents ? "diff" : "transform", baseline: baselinePath, candidate: candidatePath }) + "\n";
1812
- import_fs10.default.appendFileSync(import_path10.default.join(outDir, "guard-history.jsonl"), histLine);
2367
+ import_fs14.default.appendFileSync(import_path15.default.join(outDir, "guard-history.jsonl"), histLine);
1813
2368
  } catch {
1814
2369
  }
1815
2370
  process.exit(r.exitCode);
1816
2371
  });
1817
- program.command("dashboard").description("Local dashboard (localhost only): view last guard + last scan outputs").option("--port <n>", "port (default: 3010)", (v) => Number(v), 3010).action(async (opts) => {
2372
+ program.command("dashboard").description("Local dashboard (localhost only): view last guard + last scan outputs").option("--port <n>", "port (default: 3010)", (v) => Number(v), 3010).option("--dir <path>", "base directory containing ./aiopt-output (default: cwd)").option("--auto", "auto-detect by searching parents (and one-level children) for aiopt-output").action(async (opts) => {
1818
2373
  const { startDashboard: startDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
1819
- await startDashboard2(process.cwd(), { port: Number(opts.port || 3010) });
2374
+ const base = opts.dir ? String(opts.dir) : process.cwd();
2375
+ if (opts.auto) {
2376
+ const { findAioptOutputDir: findAioptOutputDir2 } = await Promise.resolve().then(() => (init_find_output(), find_output_exports));
2377
+ const found = findAioptOutputDir2(base);
2378
+ await startDashboard2(found.cwd, { port: Number(opts.port || 3010) });
2379
+ return;
2380
+ }
2381
+ await startDashboard2(base, { port: Number(opts.port || 3010) });
1820
2382
  });
1821
2383
  program.command("quickstart").description("1-minute demo: generate sample usage, run scan+guard, and print dashboard URL").option("--demo", "run demo workflow (writes to ./aiopt-output)").option("--port <n>", "dashboard port (default: 3010)", (v) => Number(v), 3010).option("--budget-monthly <usd>", "optional budget gate for the demo guard", (v) => Number(v)).option("--serve", "start the local dashboard after generating demo outputs").option("--open", "best-effort open browser to the dashboard URL").action(async (opts) => {
1822
2384
  if (!opts.demo) {
@@ -1830,14 +2392,14 @@ program.command("quickstart").description("1-minute demo: generate sample usage,
1830
2392
  console.log("--- guard ---");
1831
2393
  console.log(r.guard.message);
1832
2394
  try {
1833
- const fs11 = await import("fs");
1834
- const path11 = await import("path");
1835
- fs11.mkdirSync(r.outDir, { recursive: true });
2395
+ const fs15 = await import("fs");
2396
+ const path16 = await import("path");
2397
+ fs15.mkdirSync(r.outDir, { recursive: true });
1836
2398
  const ts = (/* @__PURE__ */ new Date()).toISOString();
1837
- fs11.writeFileSync(path11.join(r.outDir, "guard-last.txt"), r.guard.message);
1838
- fs11.writeFileSync(path11.join(r.outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.guard.exitCode }, null, 2));
2399
+ fs15.writeFileSync(path16.join(r.outDir, "guard-last.txt"), r.guard.message);
2400
+ fs15.writeFileSync(path16.join(r.outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.guard.exitCode }, null, 2));
1839
2401
  const histLine = JSON.stringify({ ts, exitCode: r.guard.exitCode, mode: "quickstart", baseline: r.usagePath, candidate: null }) + "\n";
1840
- fs11.appendFileSync(path11.join(r.outDir, "guard-history.jsonl"), histLine);
2402
+ fs15.appendFileSync(path16.join(r.outDir, "guard-history.jsonl"), histLine);
1841
2403
  } catch {
1842
2404
  }
1843
2405
  console.log("--- next ---");
@@ -1846,11 +2408,11 @@ program.command("quickstart").description("1-minute demo: generate sample usage,
1846
2408
  const { startDashboard: startDashboard2 } = await Promise.resolve().then(() => (init_dashboard(), dashboard_exports));
1847
2409
  if (opts.open) {
1848
2410
  try {
1849
- const { execSync } = await import("child_process");
2411
+ const { execSync: execSync2 } = await import("child_process");
1850
2412
  const url = `http://127.0.0.1:${port}/`;
1851
- if (process.platform === "darwin") execSync(`open "${url}"`);
1852
- else if (process.platform === "win32") execSync(`cmd.exe /c start "" "${url}"`);
1853
- else execSync(`xdg-open "${url}"`);
2413
+ if (process.platform === "darwin") execSync2(`open "${url}"`);
2414
+ else if (process.platform === "win32") execSync2(`cmd.exe /c start "" "${url}"`);
2415
+ else execSync2(`xdg-open "${url}"`);
1854
2416
  } catch {
1855
2417
  }
1856
2418
  }