glassbox 0.15.0-beta.3 → 0.15.0-beta.6

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
@@ -16838,7 +16838,12 @@ import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync, writeFile
16838
16838
  import { homedir } from "os";
16839
16839
  import { join as join2 } from "path";
16840
16840
  var GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
16841
- var GLOBAL_CONFIG_DIR = join2(homedir(), ".glassbox");
16841
+ function resolveGlobalConfigDir() {
16842
+ const override = process.env.GLASSBOX_CONFIG_DIR;
16843
+ if (override !== void 0 && override.trim() !== "") return override;
16844
+ return join2(homedir(), ".glassbox");
16845
+ }
16846
+ var GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
16842
16847
  var GLOBAL_CONFIG_PATH = join2(GLOBAL_CONFIG_DIR, "config.json");
16843
16848
  function readGlobalConfig() {
16844
16849
  try {
@@ -16981,6 +16986,7 @@ var PLATFORMS = {
16981
16986
  apple: "Apple"
16982
16987
  };
16983
16988
  var APPLE_ON_DEVICE_MODEL_ID = "apple-on-device";
16989
+ var APPLE_FM_ANALYSIS_ENABLED = true;
16984
16990
  var MODELS = {
16985
16991
  anthropic: [
16986
16992
  { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 1e6, isDefault: false },
@@ -17138,7 +17144,10 @@ var ConfigFileSchema = external_exports.object({
17138
17144
  platform: external_exports.string().optional(),
17139
17145
  model: external_exports.string().optional(),
17140
17146
  keys: external_exports.record(external_exports.string(), external_exports.string()).optional(),
17141
- localEndpoint: external_exports.string().optional()
17147
+ localEndpoint: external_exports.string().optional(),
17148
+ // Secondary model used when the primary (Apple FM) fails a batch.
17149
+ fallbackPlatform: external_exports.string().optional(),
17150
+ fallbackModel: external_exports.string().optional()
17142
17151
  }).optional(),
17143
17152
  guidedReview: external_exports.object({
17144
17153
  enabled: external_exports.boolean().optional(),
@@ -17150,16 +17159,39 @@ function readConfigFile() {
17150
17159
  const parsed = ConfigFileSchema.safeParse(raw2);
17151
17160
  return parsed.success ? parsed.data : {};
17152
17161
  }
17153
- function loadAIConfig() {
17154
- const config2 = readConfigFile();
17155
- const platformRaw = config2.ai?.platform ?? "anthropic";
17156
- const platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
17157
- const rawModel = config2.ai?.model ?? getDefaultModel(platform);
17162
+ function resolvePlatformConfig(platform, rawModelOrUndefined) {
17163
+ const rawModel = rawModelOrUndefined ?? getDefaultModel(platform);
17158
17164
  const model = KEYLESS_PLATFORMS.has(platform) ? rawModel : resolveModelId(platform, rawModel);
17159
17165
  const { key, source } = resolveAPIKey(platform);
17160
17166
  const baseUrl = platform === "local" ? resolveLocalEndpoint() : void 0;
17161
17167
  return { platform, model, apiKey: key, keySource: source, baseUrl };
17162
17168
  }
17169
+ function loadAIConfig() {
17170
+ const config2 = readConfigFile();
17171
+ const platformRaw = config2.ai?.platform ?? "anthropic";
17172
+ let platform = AIPlatformSchema.safeParse(platformRaw).success ? AIPlatformSchema.parse(platformRaw) : "anthropic";
17173
+ if (platform === "apple" && !APPLE_FM_ANALYSIS_ENABLED) platform = "anthropic";
17174
+ const primary = resolvePlatformConfig(platform, config2.ai?.model);
17175
+ if (platform === "apple") {
17176
+ const fallback = resolveFallbackConfig(config2);
17177
+ if (fallback !== null) primary.fallback = fallback;
17178
+ }
17179
+ return primary;
17180
+ }
17181
+ function fallbackSelectionFrom(config2) {
17182
+ const raw2 = config2.ai?.fallbackPlatform;
17183
+ if (raw2 === void 0 || raw2 === "") return null;
17184
+ const parsed = AIPlatformSchema.safeParse(raw2);
17185
+ if (!parsed.success || parsed.data === "apple") return null;
17186
+ return { platform: parsed.data, model: config2.ai?.fallbackModel ?? getDefaultModel(parsed.data) };
17187
+ }
17188
+ function loadFallbackSelection() {
17189
+ return fallbackSelectionFrom(readConfigFile());
17190
+ }
17191
+ function resolveFallbackConfig(config2) {
17192
+ const sel = fallbackSelectionFrom(config2);
17193
+ return sel === null ? null : resolvePlatformConfig(sel.platform, sel.model);
17194
+ }
17163
17195
  function saveAIConfigPreferences(platform, model, opts = {}) {
17164
17196
  updateGlobalConfig((config2) => {
17165
17197
  const parsed = ConfigFileSchema.safeParse(config2);
@@ -17171,6 +17203,12 @@ function saveAIConfigPreferences(platform, model, opts = {}) {
17171
17203
  const trimmed = opts.localEndpoint.trim();
17172
17204
  cfg.ai.localEndpoint = trimmed === "" ? void 0 : trimmed;
17173
17205
  }
17206
+ if (opts.fallbackPlatform !== void 0) {
17207
+ const fp = opts.fallbackPlatform.trim();
17208
+ const fm = opts.fallbackModel?.trim();
17209
+ cfg.ai.fallbackPlatform = fp === "" ? void 0 : fp;
17210
+ cfg.ai.fallbackModel = fp === "" || fm === void 0 || fm === "" ? void 0 : fm;
17211
+ }
17174
17212
  return cfg;
17175
17213
  });
17176
17214
  }
@@ -19006,6 +19044,16 @@ function extractJSON(text) {
19006
19044
  throw new Error(`Could not extract JSON from AI response: ${text.slice(0, 300)}`);
19007
19045
  }
19008
19046
  async function runAnalysisBatch(files, config2, repoRoot, options) {
19047
+ try {
19048
+ return await runAnalysisBatchOnce(files, config2, repoRoot, options);
19049
+ } catch (err) {
19050
+ if (config2.fallback === void 0) throw err;
19051
+ const msg = err instanceof Error ? err.message : String(err);
19052
+ debugLog(`[${options.analysisName}] primary platform '${config2.platform}' failed for batch (${msg.slice(0, 160)}); retrying with fallback '${config2.fallback.platform}'`);
19053
+ return runAnalysisBatchOnce(files, config2.fallback, repoRoot, options);
19054
+ }
19055
+ }
19056
+ async function runAnalysisBatchOnce(files, config2, repoRoot, options) {
19009
19057
  const contextWindow = getModelContextWindow(config2.platform, config2.model);
19010
19058
  const charBudget = Math.floor(contextWindow * 0.7 * 3);
19011
19059
  const contexts = buildFileContexts(files, charBudget);
@@ -19696,13 +19744,20 @@ var AIConfigRespSchema = external_exports.object({
19696
19744
  keySource: KeySourceSchema,
19697
19745
  /** Base URL for the `local` (OpenAI-compatible) platform. */
19698
19746
  localEndpoint: external_exports.string(),
19699
- guidedReview: GuidedReviewConfigShapeSchema
19747
+ guidedReview: GuidedReviewConfigShapeSchema,
19748
+ /** Secondary model used when the primary (Apple FM) fails a batch. `null`
19749
+ * when none is configured. */
19750
+ fallbackPlatform: AIPlatformSchema.nullable(),
19751
+ fallbackModel: external_exports.string().nullable()
19700
19752
  });
19701
19753
  var SaveAIConfigReqSchema = external_exports.object({
19702
19754
  platform: AIPlatformSchema,
19703
19755
  model: external_exports.string().min(1),
19704
19756
  localEndpoint: external_exports.string().optional(),
19705
- guidedReview: GuidedReviewConfigShapeSchema.optional()
19757
+ guidedReview: GuidedReviewConfigShapeSchema.optional(),
19758
+ /** Apple-FM fallback selection. An empty `fallbackPlatform` clears it. */
19759
+ fallbackPlatform: external_exports.string().optional(),
19760
+ fallbackModel: external_exports.string().optional()
19706
19761
  });
19707
19762
  var SaveAIConfigRespSchema = OkResponseSchema;
19708
19763
  var ListAIModelsRespSchema = external_exports.object({
@@ -21060,7 +21115,7 @@ aiAnalysisRoutes.post("/analyze", async (c) => {
21060
21115
  debugLog(`POST /analyze: type=${analysisType}, reviewId=${reviewId}`);
21061
21116
  const testMode = isAIServiceTest();
21062
21117
  const config2 = loadAIConfig();
21063
- if (config2.apiKey === null && !testMode) {
21118
+ if (config2.apiKey === null && !testMode && !KEYLESS_PLATFORMS.has(config2.platform)) {
21064
21119
  debugLog("POST /analyze: no API key configured");
21065
21120
  return c.json({ error: "No API key configured" }, 400);
21066
21121
  }
@@ -21540,21 +21595,30 @@ async function fetchAvailableModels(platform, apiKey, opts = {}) {
21540
21595
  var aiConfigRoutes = new Hono2();
21541
21596
  aiConfigRoutes.get("/config", async (c) => {
21542
21597
  const config2 = loadAIConfig();
21543
- const appleReady = config2.platform === "apple" && await isAppleFoundationAvailable();
21598
+ const appleReady = APPLE_FM_ANALYSIS_ENABLED && config2.platform === "apple" && await isAppleFoundationAvailable();
21599
+ const fallbackSelection = loadFallbackSelection();
21544
21600
  return c.json({
21545
21601
  platform: config2.platform,
21546
21602
  model: config2.model,
21547
21603
  keyConfigured: config2.apiKey !== null || config2.platform === "local" || appleReady || isAIServiceTest() || getDemoMode() !== null,
21548
21604
  keySource: config2.keySource,
21549
21605
  localEndpoint: resolveLocalEndpoint(),
21550
- guidedReview: loadGuidedReviewConfig()
21606
+ guidedReview: loadGuidedReviewConfig(),
21607
+ // Apple-FM fallback selection as stored, regardless of the current primary
21608
+ // platform, so the settings dialog can show/preserve it; `null` when unset.
21609
+ fallbackPlatform: fallbackSelection?.platform ?? null,
21610
+ fallbackModel: fallbackSelection?.model ?? null
21551
21611
  });
21552
21612
  });
21553
21613
  aiConfigRoutes.post("/config", async (c) => {
21554
21614
  const parsed = await parseBody(c, SaveAIConfigReqSchema);
21555
21615
  if (!parsed.ok) return parsed.response;
21556
21616
  const body = parsed.data;
21557
- saveAIConfigPreferences(body.platform, body.model, { localEndpoint: body.localEndpoint });
21617
+ saveAIConfigPreferences(body.platform, body.model, {
21618
+ localEndpoint: body.localEndpoint,
21619
+ fallbackPlatform: body.fallbackPlatform,
21620
+ fallbackModel: body.fallbackModel
21621
+ });
21558
21622
  if (body.guidedReview !== void 0) {
21559
21623
  saveGuidedReviewConfig(body.guidedReview);
21560
21624
  }
@@ -21580,7 +21644,7 @@ aiConfigRoutes.get("/models", async (c) => {
21580
21644
  const localLive = await fetchAvailableModels("local", localKey ?? "", { baseUrl: resolveLocalEndpoint() });
21581
21645
  if (localLive !== null && localLive.length > 0) models.local = localLive;
21582
21646
  }
21583
- const appleAvailable = await isAppleFoundationAvailable();
21647
+ const appleAvailable = APPLE_FM_ANALYSIS_ENABLED && (isAIServiceTest() || await isAppleFoundationAvailable());
21584
21648
  return c.json({ platforms: PLATFORMS, models, appleAvailable });
21585
21649
  });
21586
21650
  aiConfigRoutes.get("/key-status", (c) => {
@@ -25221,7 +25285,7 @@ async function main() {
25221
25285
  console.log("AI service test mode enabled \u2014 using mock AI responses");
25222
25286
  }
25223
25287
  if (debug) {
25224
- console.log(`[debug] Build timestamp: ${"2026-06-19T00:30:57.390Z"}`);
25288
+ console.log(`[debug] Build timestamp: ${"2026-06-19T07:12:20.944Z"}`);
25225
25289
  }
25226
25290
  if (projectDir !== null) {
25227
25291
  process.chdir(projectDir);