glotfile 0.7.2 → 0.7.3

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.
@@ -2,7 +2,7 @@
2
2
  import { Hono as Hono2 } from "hono";
3
3
  import { serve } from "@hono/node-server";
4
4
  import { fileURLToPath } from "url";
5
- import { dirname as dirname4, join as join16, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
5
+ import { dirname as dirname4, join as join17, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
6
6
  import { readFile, stat } from "fs/promises";
7
7
  import { createServer } from "net";
8
8
  import open from "open";
@@ -2841,7 +2841,7 @@ function checkOutputs(state, root) {
2841
2841
  }
2842
2842
 
2843
2843
  // src/server/api.ts
2844
- import { readFileSync as readFileSync22, existsSync as existsSync12, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync5 } from "fs";
2844
+ import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync6 } from "fs";
2845
2845
  import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
2846
2846
 
2847
2847
  // src/server/ai/anthropic.ts
@@ -2851,6 +2851,9 @@ import Anthropic from "@anthropic-ai/sdk";
2851
2851
  function supportsBatchTranslate(p) {
2852
2852
  return typeof p.submitTranslationBatch === "function";
2853
2853
  }
2854
+ function supportsBatchComplete(p) {
2855
+ return typeof p.submitCompletionBatch === "function";
2856
+ }
2854
2857
  function buildSystemPrompt(hasPluralItems) {
2855
2858
  const lines = [
2856
2859
  "You are a professional software localization engine for a UI string catalog.",
@@ -3198,10 +3201,13 @@ var AnthropicProvider = class {
3198
3201
  content.push({ type: "text", text: buildBatchPrompt(batch) });
3199
3202
  return content;
3200
3203
  }
3201
- async complete(req) {
3202
- const content = req.content.map(
3204
+ completionContent(req) {
3205
+ return req.content.map(
3203
3206
  (b) => b.type === "image" ? { type: "image", source: { type: "base64", media_type: b.mediaType, data: b.base64 } } : { type: "text", text: b.text ?? "" }
3204
3207
  );
3208
+ }
3209
+ async complete(req) {
3210
+ const content = this.completionContent(req);
3205
3211
  const res = await this.client.messages.create({
3206
3212
  model: this.config.model,
3207
3213
  max_tokens: req.maxTokens ?? 8192,
@@ -3265,6 +3271,40 @@ var AnthropicProvider = class {
3265
3271
  async cancelTranslationBatch(batchId) {
3266
3272
  await this.batchesClient().cancel(batchId);
3267
3273
  }
3274
+ // Mirrors complete() exactly — same prompts and schema — so batch and sync
3275
+ // completion replies are interchangeable downstream.
3276
+ async submitCompletionBatch(jobs) {
3277
+ const requests = jobs.map((job) => ({
3278
+ custom_id: job.customId,
3279
+ params: {
3280
+ model: this.config.model,
3281
+ max_tokens: job.request.maxTokens ?? 8192,
3282
+ // Batch entries don't share a live cache window, so cache_control is omitted here.
3283
+ system: [{ type: "text", text: job.request.system }],
3284
+ output_config: { format: { type: "json_schema", schema: job.request.schema } },
3285
+ messages: [{ role: "user", content: this.completionContent(job.request) }]
3286
+ }
3287
+ }));
3288
+ const res = await this.batchesClient().create({ requests });
3289
+ return res.id;
3290
+ }
3291
+ async completionBatchResults(batchId) {
3292
+ const out = /* @__PURE__ */ new Map();
3293
+ for await (const entry of await this.batchesClient().results(batchId)) {
3294
+ if (entry.result.type !== "succeeded") {
3295
+ out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
3296
+ continue;
3297
+ }
3298
+ this.recordUsage(entry.result.message?.usage);
3299
+ const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
3300
+ try {
3301
+ out.set(entry.custom_id, { type: "json", value: JSON.parse(text) });
3302
+ } catch {
3303
+ out.set(entry.custom_id, { type: "malformed", raw: text });
3304
+ }
3305
+ }
3306
+ return out;
3307
+ }
3268
3308
  async callBatch(batch, signal) {
3269
3309
  const content = this.buildUserContent(batch);
3270
3310
  const res = await this.client.messages.create({
@@ -3998,6 +4038,133 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
3998
4038
  return { written, errors, staleSkipped: stale.length, retried: retryReqs.length, screenshotsSkipped };
3999
4039
  }
4000
4040
 
4041
+ // src/server/ai/pending-context-batch.ts
4042
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync4, rmSync as rmSync5 } from "fs";
4043
+ import { join as join5 } from "path";
4044
+ function pendingContextBatchPath(projectRoot) {
4045
+ return join5(projectRoot, ".glotfile", "context-batch.json");
4046
+ }
4047
+ function loadPendingContextBatch(projectRoot) {
4048
+ const path = pendingContextBatchPath(projectRoot);
4049
+ if (!existsSync10(path)) return void 0;
4050
+ try {
4051
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
4052
+ if (parsed?.version !== 1) return void 0;
4053
+ return parsed;
4054
+ } catch {
4055
+ return void 0;
4056
+ }
4057
+ }
4058
+ function savePendingContextBatch(projectRoot, pending) {
4059
+ const dir = join5(projectRoot, ".glotfile");
4060
+ mkdirSync5(dir, { recursive: true });
4061
+ const gitignore = join5(dir, ".gitignore");
4062
+ if (!existsSync10(gitignore)) writeFileSync4(gitignore, "*\n");
4063
+ writeFileSync4(pendingContextBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
4064
+ }
4065
+ function clearPendingContextBatch(projectRoot) {
4066
+ rmSync5(pendingContextBatchPath(projectRoot), { force: true });
4067
+ }
4068
+
4069
+ // src/server/ai/context-batch-run.ts
4070
+ function completionRequestFor(chunk2) {
4071
+ return {
4072
+ system: buildContextSystemPrompt(),
4073
+ content: [{ type: "text", text: buildContextBatchPrompt(chunk2) }],
4074
+ schema: CONTEXT_BATCH_SCHEMA
4075
+ };
4076
+ }
4077
+ async function submitContextBatch(provider, targets, batchSize, model, projectRoot, force) {
4078
+ if (loadPendingContextBatch(projectRoot)) {
4079
+ throw new Error("A context batch is already pending. Apply or cancel it first.");
4080
+ }
4081
+ const chunks = [];
4082
+ const size = Math.max(1, batchSize);
4083
+ for (let i = 0; i < targets.length; i += size) chunks.push(targets.slice(i, i + size));
4084
+ const jobs = chunks.map((chunk2, i) => ({ customId: `ctx_${i}`, chunk: chunk2 }));
4085
+ const batchId = await provider.submitCompletionBatch(
4086
+ jobs.map((j) => ({ customId: j.customId, request: completionRequestFor(j.chunk) }))
4087
+ );
4088
+ const pending = {
4089
+ version: 1,
4090
+ // Only Anthropic implements completion batches today.
4091
+ provider: "anthropic",
4092
+ model,
4093
+ batchId,
4094
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4095
+ total: targets.length,
4096
+ force,
4097
+ jobs: jobs.map((j) => ({
4098
+ customId: j.customId,
4099
+ requests: j.chunk.map(({ image: _image, ...rest }) => rest)
4100
+ }))
4101
+ };
4102
+ savePendingContextBatch(projectRoot, pending);
4103
+ return pending;
4104
+ }
4105
+ async function applyContextBatchResults(load, persist, provider, pending, projectRoot, ai) {
4106
+ provider.takeUsage?.();
4107
+ const outcomes = await provider.completionBatchResults(pending.batchId);
4108
+ const batchUsage = provider.takeUsage?.();
4109
+ const applied = [];
4110
+ const items = [];
4111
+ const errors = [];
4112
+ const jobFailures = [];
4113
+ const retryChunks = [];
4114
+ for (const job of pending.jobs) {
4115
+ const outcome = outcomes.get(job.customId);
4116
+ if (outcome?.type === "json") {
4117
+ const batch = outcome.value;
4118
+ applied.push(...job.requests);
4119
+ items.push(...batch.items ?? []);
4120
+ continue;
4121
+ }
4122
+ if (!outcome) jobFailures.push({ customId: job.customId, locale: "", type: "missing" });
4123
+ else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: "", type: "malformed", raw: outcome.raw });
4124
+ else jobFailures.push({ customId: job.customId, locale: "", type: "failed", error: outcome.error });
4125
+ retryChunks.push(job.requests);
4126
+ }
4127
+ for (const chunk2 of retryChunks) {
4128
+ try {
4129
+ const raw = await provider.complete(completionRequestFor(chunk2));
4130
+ const batch = raw;
4131
+ applied.push(...chunk2);
4132
+ items.push(...batch.items ?? []);
4133
+ } catch (e) {
4134
+ errors.push(...chunk2.map((t) => ({ key: t.key, error: e.message })));
4135
+ }
4136
+ }
4137
+ const retryUsage = provider.takeUsage?.();
4138
+ const pricing = resolvePricing({ ...ai, model: pending.model });
4139
+ let estimatedCostUsd;
4140
+ if (pricing && (batchUsage || retryUsage)) {
4141
+ estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
4142
+ }
4143
+ let usage;
4144
+ if (batchUsage || retryUsage) {
4145
+ usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
4146
+ if (retryUsage) addUsage(usage, retryUsage);
4147
+ }
4148
+ const fresh = load();
4149
+ const { written, errors: applyErrors } = applyContext(fresh, applied, items, void 0, pending.force);
4150
+ errors.push(...applyErrors);
4151
+ persist(fresh);
4152
+ clearPendingContextBatch(projectRoot);
4153
+ const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
4154
+ appendLog(projectRoot, {
4155
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4156
+ kind: "context",
4157
+ summary: `Applied context batch ${pending.batchId}: wrote ${written}, ${errors.length} error(s), ${retryChunks.length} job(s) retried${costSuffix}`,
4158
+ model: pending.model,
4159
+ items: applied.map((r) => ({ id: r.id, key: r.key, source: r.source })),
4160
+ results: items.map((r) => ({ id: r.id, value: r.context, error: r.error })),
4161
+ jobFailures: jobFailures.length ? jobFailures : void 0,
4162
+ usage,
4163
+ estimatedCostUsd
4164
+ });
4165
+ return { written, errors, retried: retryChunks.length };
4166
+ }
4167
+
4001
4168
  // src/server/ai/estimate.ts
4002
4169
  var CJK_RE = /[ -鿿가-힯豈-﫿]/g;
4003
4170
  function estimateTokens(text) {
@@ -4055,8 +4222,8 @@ function estimateTranslation(state, ai, opts) {
4055
4222
  import { relative as relative3 } from "path";
4056
4223
 
4057
4224
  // src/server/import/detect.ts
4058
- import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
4059
- import { join as join5 } from "path";
4225
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync2 } from "fs";
4226
+ import { join as join6 } from "path";
4060
4227
  var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4061
4228
  var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
4062
4229
  function safeIsDir(p) {
@@ -4067,7 +4234,7 @@ function safeIsDir(p) {
4067
4234
  }
4068
4235
  }
4069
4236
  function listDirs(dir) {
4070
- return readdirSync3(dir).filter((e) => safeIsDir(join5(dir, e)));
4237
+ return readdirSync3(dir).filter((e) => safeIsDir(join6(dir, e)));
4071
4238
  }
4072
4239
  function fileCount(dir) {
4073
4240
  try {
@@ -4081,23 +4248,23 @@ function pickSource(locales, sizeOf) {
4081
4248
  return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
4082
4249
  }
4083
4250
  function detectLaravel(root) {
4084
- const localeRoot = [join5(root, "resources", "lang"), join5(root, "lang")].find(safeIsDir);
4251
+ const localeRoot = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
4085
4252
  if (!localeRoot) return null;
4086
4253
  const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
4087
4254
  if (locales.length === 0) return null;
4088
- const sourceLocale = pickSource(locales, (loc) => fileCount(join5(localeRoot, loc)));
4255
+ const sourceLocale = pickSource(locales, (loc) => fileCount(join6(localeRoot, loc)));
4089
4256
  return { format: "laravel-php", localeRoot, locales, sourceLocale };
4090
4257
  }
4091
4258
  function detectVue(root, forced = false) {
4092
4259
  for (const rel of VUE_DIR_CANDIDATES) {
4093
- const localeRoot = join5(root, rel);
4260
+ const localeRoot = join6(root, rel);
4094
4261
  if (!safeIsDir(localeRoot)) continue;
4095
4262
  const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
4096
4263
  const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
4097
4264
  if (enough) {
4098
4265
  const sourceLocale = pickSource(locales, (loc) => {
4099
4266
  try {
4100
- return statSync2(join5(localeRoot, `${loc}.json`)).size;
4267
+ return statSync2(join6(localeRoot, `${loc}.json`)).size;
4101
4268
  } catch {
4102
4269
  return 0;
4103
4270
  }
@@ -4109,7 +4276,7 @@ function detectVue(root, forced = false) {
4109
4276
  }
4110
4277
  function detectArb(root) {
4111
4278
  for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
4112
- const localeRoot = join5(root, rel);
4279
+ const localeRoot = join6(root, rel);
4113
4280
  if (!safeIsDir(localeRoot)) continue;
4114
4281
  const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
4115
4282
  if (locales.length >= 1) {
@@ -4119,10 +4286,10 @@ function detectArb(root) {
4119
4286
  return null;
4120
4287
  }
4121
4288
  function lprojLocales(dir) {
4122
- return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync10(join5(dir, `${l}.lproj`, "Localizable.strings")));
4289
+ return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join6(dir, `${l}.lproj`, "Localizable.strings")));
4123
4290
  }
4124
4291
  function detectApple(root) {
4125
- const candidates = [root, ...listDirs(root).map((d) => join5(root, d))];
4292
+ const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
4126
4293
  let best = null;
4127
4294
  for (const dir of candidates) {
4128
4295
  const locales = lprojLocales(dir);
@@ -4134,7 +4301,7 @@ function detectApple(root) {
4134
4301
  locales,
4135
4302
  sourceLocale: pickSource(locales, (loc) => {
4136
4303
  try {
4137
- return statSync2(join5(dir, `${loc}.lproj`, "Localizable.strings")).size;
4304
+ return statSync2(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
4138
4305
  } catch {
4139
4306
  return 0;
4140
4307
  }
@@ -4147,7 +4314,7 @@ function detectApple(root) {
4147
4314
  var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
4148
4315
  function detectAngularXliff(root) {
4149
4316
  for (const rel of ANGULAR_DIR_CANDIDATES) {
4150
- const localeRoot = rel === "." ? root : join5(root, rel);
4317
+ const localeRoot = rel === "." ? root : join6(root, rel);
4151
4318
  if (!safeIsDir(localeRoot)) continue;
4152
4319
  const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
4153
4320
  if (files.length === 0) continue;
@@ -4155,7 +4322,7 @@ function detectAngularXliff(root) {
4155
4322
  const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
4156
4323
  let sourceLocale;
4157
4324
  try {
4158
- sourceLocale = readFileSync10(join5(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
4325
+ sourceLocale = readFileSync11(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
4159
4326
  } catch {
4160
4327
  }
4161
4328
  if (!sourceLocale && locales.length === 0) continue;
@@ -4166,14 +4333,14 @@ function detectAngularXliff(root) {
4166
4333
  return null;
4167
4334
  }
4168
4335
  function detectRails(root) {
4169
- const localeRoot = join5(root, "config", "locales");
4336
+ const localeRoot = join6(root, "config", "locales");
4170
4337
  if (!safeIsDir(localeRoot)) return null;
4171
4338
  const locales = [];
4172
4339
  for (const file of readdirSync3(localeRoot).sort()) {
4173
4340
  if (!/\.ya?ml$/.test(file)) continue;
4174
4341
  let text;
4175
4342
  try {
4176
- text = readFileSync10(join5(localeRoot, file), "utf8");
4343
+ text = readFileSync11(join6(localeRoot, file), "utf8");
4177
4344
  } catch {
4178
4345
  continue;
4179
4346
  }
@@ -4188,15 +4355,15 @@ function detectRails(root) {
4188
4355
  var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
4189
4356
  function detectI18next(root) {
4190
4357
  for (const rel of I18NEXT_DIR_CANDIDATES) {
4191
- const localeRoot = join5(root, rel);
4358
+ const localeRoot = join6(root, rel);
4192
4359
  if (!safeIsDir(localeRoot)) continue;
4193
4360
  const locales = listDirs(localeRoot).filter(
4194
- (d) => LOCALE_RE.test(d) && readdirSync3(join5(localeRoot, d)).some((f) => f.endsWith(".json"))
4361
+ (d) => LOCALE_RE.test(d) && readdirSync3(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
4195
4362
  );
4196
4363
  if (locales.length === 0) continue;
4197
4364
  const sourceLocale = pickSource(locales, (loc) => {
4198
4365
  try {
4199
- return readdirSync3(join5(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join5(localeRoot, loc, f)).size, 0);
4366
+ return readdirSync3(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join6(localeRoot, loc, f)).size, 0);
4200
4367
  } catch {
4201
4368
  return 0;
4202
4369
  }
@@ -4213,8 +4380,8 @@ function gettextLocales(dir) {
4213
4380
  if (!locales.includes(flat)) locales.push(flat);
4214
4381
  continue;
4215
4382
  }
4216
- if (!LOCALE_RE.test(entry) || !safeIsDir(join5(dir, entry))) continue;
4217
- const sub = join5(dir, entry);
4383
+ if (!LOCALE_RE.test(entry) || !safeIsDir(join6(dir, entry))) continue;
4384
+ const sub = join6(dir, entry);
4218
4385
  const hasPo = (d) => {
4219
4386
  try {
4220
4387
  return readdirSync3(d).some((f) => f.endsWith(".po"));
@@ -4222,7 +4389,7 @@ function gettextLocales(dir) {
4222
4389
  return false;
4223
4390
  }
4224
4391
  };
4225
- if (hasPo(join5(sub, "LC_MESSAGES")) || hasPo(sub)) {
4392
+ if (hasPo(join6(sub, "LC_MESSAGES")) || hasPo(sub)) {
4226
4393
  if (!locales.includes(entry)) locales.push(entry);
4227
4394
  }
4228
4395
  }
@@ -4231,7 +4398,7 @@ function gettextLocales(dir) {
4231
4398
  var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
4232
4399
  function detectGettext(root) {
4233
4400
  for (const rel of GETTEXT_DIR_CANDIDATES) {
4234
- const localeRoot = join5(root, rel);
4401
+ const localeRoot = join6(root, rel);
4235
4402
  if (!safeIsDir(localeRoot)) continue;
4236
4403
  const locales = gettextLocales(localeRoot);
4237
4404
  if (locales.length === 0) continue;
@@ -4240,10 +4407,10 @@ function detectGettext(root) {
4240
4407
  return null;
4241
4408
  }
4242
4409
  function detectAppleStringsdict(root) {
4243
- const candidates = [root, ...listDirs(root).map((d) => join5(root, d))];
4410
+ const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
4244
4411
  let best = null;
4245
4412
  for (const dir of candidates) {
4246
- const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync10(join5(dir, `${l}.lproj`, "Localizable.stringsdict")));
4413
+ const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join6(dir, `${l}.lproj`, "Localizable.stringsdict")));
4247
4414
  if (locales.length === 0) continue;
4248
4415
  if (!best || locales.length > best.locales.length) {
4249
4416
  best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
@@ -4274,7 +4441,7 @@ var BY_FORMAT = {
4274
4441
  "apple-stringsdict": detectAppleStringsdict
4275
4442
  };
4276
4443
  function detect(root, formatOverride) {
4277
- if (!existsSync10(root)) return null;
4444
+ if (!existsSync11(root)) return null;
4278
4445
  if (formatOverride) {
4279
4446
  const fn = BY_FORMAT[formatOverride];
4280
4447
  if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
@@ -4288,8 +4455,8 @@ function detect(root, formatOverride) {
4288
4455
  }
4289
4456
 
4290
4457
  // src/server/import/parsers/vue-i18n-json.ts
4291
- import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
4292
- import { join as join6 } from "path";
4458
+ import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
4459
+ import { join as join7 } from "path";
4293
4460
 
4294
4461
  // src/server/import/flatten.ts
4295
4462
  function flattenObject(value, prefix, warnings) {
@@ -4328,7 +4495,7 @@ var vueI18nJson2 = {
4328
4495
  if (opts?.locales && !opts.locales.includes(locale)) continue;
4329
4496
  let data;
4330
4497
  try {
4331
- data = JSON.parse(readFileSync11(join6(localeRoot, file), "utf8"));
4498
+ data = JSON.parse(readFileSync12(join7(localeRoot, file), "utf8"));
4332
4499
  } catch (e) {
4333
4500
  warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
4334
4501
  continue;
@@ -4344,7 +4511,7 @@ var vueI18nJson2 = {
4344
4511
 
4345
4512
  // src/server/import/parsers/laravel-php.ts
4346
4513
  import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
4347
- import { join as join7, relative as relative2 } from "path";
4514
+ import { join as join8, relative as relative2 } from "path";
4348
4515
  import { execFileSync } from "child_process";
4349
4516
 
4350
4517
  // src/server/import/placeholders.ts
@@ -4354,13 +4521,13 @@ function laravelToCanonical(value) {
4354
4521
 
4355
4522
  // src/server/import/parsers/laravel-php.ts
4356
4523
  function listDirs2(dir) {
4357
- return readdirSync5(dir).filter((e) => statSync3(join7(dir, e)).isDirectory());
4524
+ return readdirSync5(dir).filter((e) => statSync3(join8(dir, e)).isDirectory());
4358
4525
  }
4359
4526
  function listPhpFiles(dir) {
4360
4527
  const out = [];
4361
4528
  const walk = (d) => {
4362
4529
  for (const e of readdirSync5(d)) {
4363
- const full = join7(d, e);
4530
+ const full = join8(d, e);
4364
4531
  if (statSync3(full).isDirectory()) walk(full);
4365
4532
  else if (e.endsWith(".php")) out.push(full);
4366
4533
  }
@@ -4397,7 +4564,7 @@ var laravelPhp2 = {
4397
4564
  for (const locale of listDirs2(localeRoot).sort()) {
4398
4565
  if (locale === "vendor") continue;
4399
4566
  if (opts?.locales && !opts.locales.includes(locale)) continue;
4400
- const localeDir = join7(localeRoot, locale);
4567
+ const localeDir = join8(localeRoot, locale);
4401
4568
  locales.push(locale);
4402
4569
  for (const file of listPhpFiles(localeDir)) {
4403
4570
  const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
@@ -4420,8 +4587,8 @@ var laravelPhp2 = {
4420
4587
  };
4421
4588
 
4422
4589
  // src/server/import/parsers/flutter-arb.ts
4423
- import { readdirSync as readdirSync6, readFileSync as readFileSync12 } from "fs";
4424
- import { join as join8 } from "path";
4590
+ import { readdirSync as readdirSync6, readFileSync as readFileSync13 } from "fs";
4591
+ import { join as join9 } from "path";
4425
4592
  var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4426
4593
  function localeFromArbName(file) {
4427
4594
  const m = file.match(/^(.+)\.arb$/);
@@ -4457,7 +4624,7 @@ var flutterArb2 = {
4457
4624
  if (opts?.locales && !opts.locales.includes(locale)) continue;
4458
4625
  let data;
4459
4626
  try {
4460
- data = JSON.parse(readFileSync12(join8(localeRoot, file), "utf8"));
4627
+ data = JSON.parse(readFileSync13(join9(localeRoot, file), "utf8"));
4461
4628
  } catch (e) {
4462
4629
  warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
4463
4630
  continue;
@@ -4482,8 +4649,8 @@ var flutterArb2 = {
4482
4649
  };
4483
4650
 
4484
4651
  // src/server/import/parsers/apple-strings.ts
4485
- import { readdirSync as readdirSync7, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
4486
- import { join as join9 } from "path";
4652
+ import { readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync4 } from "fs";
4653
+ import { join as join10 } from "path";
4487
4654
  var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4488
4655
  var TABLE = "Localizable.strings";
4489
4656
  function localeFromLproj(dir) {
@@ -4587,16 +4754,16 @@ var appleStrings2 = {
4587
4754
  const locale = localeFromLproj(dir);
4588
4755
  if (!locale) continue;
4589
4756
  if (opts?.locales && !opts.locales.includes(locale)) continue;
4590
- const file = join9(localeRoot, dir, TABLE);
4757
+ const file = join10(localeRoot, dir, TABLE);
4591
4758
  let text;
4592
4759
  try {
4593
4760
  if (!statSync4(file).isFile()) continue;
4594
- text = readFileSync13(file, "utf8");
4761
+ text = readFileSync14(file, "utf8");
4595
4762
  } catch {
4596
4763
  continue;
4597
4764
  }
4598
4765
  locales.push(locale);
4599
- const others = readdirSync7(join9(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
4766
+ const others = readdirSync7(join10(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
4600
4767
  if (others.length) {
4601
4768
  warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
4602
4769
  }
@@ -4609,8 +4776,8 @@ var appleStrings2 = {
4609
4776
  };
4610
4777
 
4611
4778
  // src/server/import/parsers/angular-xliff.ts
4612
- import { readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
4613
- import { join as join10 } from "path";
4779
+ import { readdirSync as readdirSync8, readFileSync as readFileSync15 } from "fs";
4780
+ import { join as join11 } from "path";
4614
4781
  var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4615
4782
  var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
4616
4783
  function decodeEntities(s) {
@@ -4658,7 +4825,7 @@ var angularXliff2 = {
4658
4825
  if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
4659
4826
  let xml;
4660
4827
  try {
4661
- xml = readFileSync14(join10(localeRoot, file), "utf8");
4828
+ xml = readFileSync15(join11(localeRoot, file), "utf8");
4662
4829
  } catch (e) {
4663
4830
  warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
4664
4831
  continue;
@@ -4699,8 +4866,8 @@ var angularXliff2 = {
4699
4866
  };
4700
4867
 
4701
4868
  // src/server/import/parsers/gettext-po.ts
4702
- import { readdirSync as readdirSync9, readFileSync as readFileSync15 } from "fs";
4703
- import { join as join11 } from "path";
4869
+ import { readdirSync as readdirSync9, readFileSync as readFileSync16 } from "fs";
4870
+ import { join as join12 } from "path";
4704
4871
  var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4705
4872
  var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
4706
4873
  var CONT_RE = /^[ \t]*"(.*)"\s*$/;
@@ -4774,17 +4941,17 @@ function discoverPoFiles(root) {
4774
4941
  for (const e of entries) {
4775
4942
  if (e.isFile() && e.name.endsWith(".po")) {
4776
4943
  const base = e.name.slice(0, -3);
4777
- found.push({ path: join11(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
4944
+ found.push({ path: join12(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
4778
4945
  } else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
4779
- for (const sub of [join11(e.name, "LC_MESSAGES"), e.name]) {
4946
+ for (const sub of [join12(e.name, "LC_MESSAGES"), e.name]) {
4780
4947
  let names;
4781
4948
  try {
4782
- names = readdirSync9(join11(root, sub)).sort();
4949
+ names = readdirSync9(join12(root, sub)).sort();
4783
4950
  } catch {
4784
4951
  continue;
4785
4952
  }
4786
4953
  for (const f of names) {
4787
- if (f.endsWith(".po")) found.push({ path: join11(root, sub, f), rel: join11(sub, f), locale: e.name });
4954
+ if (f.endsWith(".po")) found.push({ path: join12(root, sub, f), rel: join12(sub, f), locale: e.name });
4788
4955
  }
4789
4956
  }
4790
4957
  }
@@ -4800,7 +4967,7 @@ var gettextPo2 = {
4800
4967
  for (const file of discoverPoFiles(localeRoot)) {
4801
4968
  let entries;
4802
4969
  try {
4803
- entries = parseEntries(readFileSync15(file.path, "utf8"));
4970
+ entries = parseEntries(readFileSync16(file.path, "utf8"));
4804
4971
  } catch (e) {
4805
4972
  warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
4806
4973
  continue;
@@ -4845,8 +5012,8 @@ var gettextPo2 = {
4845
5012
  };
4846
5013
 
4847
5014
  // src/server/import/parsers/i18next-json.ts
4848
- import { readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
4849
- import { join as join12 } from "path";
5015
+ import { readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync5 } from "fs";
5016
+ import { join as join13 } from "path";
4850
5017
  var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4851
5018
  var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
4852
5019
  var PLURAL_ARG = "count";
@@ -4865,7 +5032,7 @@ function fromI18next(value) {
4865
5032
  function ingestFile(path, label, prefix, locale, keys, warnings) {
4866
5033
  let data;
4867
5034
  try {
4868
- data = JSON.parse(readFileSync16(path, "utf8"));
5035
+ data = JSON.parse(readFileSync17(path, "utf8"));
4869
5036
  } catch (e) {
4870
5037
  warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
4871
5038
  return false;
@@ -4907,7 +5074,7 @@ var i18nextJson2 = {
4907
5074
  const keys = {};
4908
5075
  const locales = [];
4909
5076
  for (const entry of readdirSync10(localeRoot).sort()) {
4910
- const full = join12(localeRoot, entry);
5077
+ const full = join13(localeRoot, entry);
4911
5078
  if (safeIsDir2(full)) {
4912
5079
  if (!LOCALE_RE7.test(entry)) continue;
4913
5080
  if (opts?.locales && !opts.locales.includes(entry)) continue;
@@ -4916,7 +5083,7 @@ var i18nextJson2 = {
4916
5083
  if (!file.endsWith(".json")) continue;
4917
5084
  const ns = file.slice(0, -".json".length);
4918
5085
  const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
4919
- if (ingestFile(join12(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
5086
+ if (ingestFile(join13(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
4920
5087
  }
4921
5088
  if (any && !locales.includes(entry)) locales.push(entry);
4922
5089
  } else if (entry.endsWith(".json")) {
@@ -4933,8 +5100,8 @@ var i18nextJson2 = {
4933
5100
  };
4934
5101
 
4935
5102
  // src/server/import/parsers/rails-yaml.ts
4936
- import { readdirSync as readdirSync11, readFileSync as readFileSync17 } from "fs";
4937
- import { join as join13 } from "path";
5103
+ import { readdirSync as readdirSync11, readFileSync as readFileSync18 } from "fs";
5104
+ import { join as join14 } from "path";
4938
5105
  var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
4939
5106
  var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
4940
5107
  function fromRuby(value) {
@@ -5146,7 +5313,7 @@ var railsYaml2 = {
5146
5313
  if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
5147
5314
  let text;
5148
5315
  try {
5149
- text = readFileSync17(join13(localeRoot, file), "utf8");
5316
+ text = readFileSync18(join14(localeRoot, file), "utf8");
5150
5317
  } catch (e) {
5151
5318
  warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
5152
5319
  continue;
@@ -5167,8 +5334,8 @@ var railsYaml2 = {
5167
5334
  };
5168
5335
 
5169
5336
  // src/server/import/parsers/apple-stringsdict.ts
5170
- import { readdirSync as readdirSync12, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
5171
- import { join as join14 } from "path";
5337
+ import { readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
5338
+ import { join as join15 } from "path";
5172
5339
  var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5173
5340
  var TABLE2 = "Localizable.stringsdict";
5174
5341
  function localeFromLproj2(dir) {
@@ -5304,16 +5471,16 @@ var appleStringsdict2 = {
5304
5471
  const locale = localeFromLproj2(dir);
5305
5472
  if (!locale) continue;
5306
5473
  if (opts?.locales && !opts.locales.includes(locale)) continue;
5307
- const file = join14(localeRoot, dir, TABLE2);
5474
+ const file = join15(localeRoot, dir, TABLE2);
5308
5475
  let text;
5309
5476
  try {
5310
5477
  if (!statSync6(file).isFile()) continue;
5311
- text = readFileSync18(file, "utf8");
5478
+ text = readFileSync19(file, "utf8");
5312
5479
  } catch {
5313
5480
  continue;
5314
5481
  }
5315
5482
  locales.push(locale);
5316
- const others = readdirSync12(join14(localeRoot, dir)).filter(
5483
+ const others = readdirSync12(join15(localeRoot, dir)).filter(
5317
5484
  (f) => f.endsWith(".stringsdict") && f !== TABLE2
5318
5485
  );
5319
5486
  if (others.length) {
@@ -5486,7 +5653,7 @@ function runImport(opts) {
5486
5653
  }
5487
5654
 
5488
5655
  // src/server/export-run.ts
5489
- import { existsSync as existsSync11, readFileSync as readFileSync19, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
5656
+ import { existsSync as existsSync12, readFileSync as readFileSync20, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
5490
5657
  import { dirname as dirname2, resolve as resolve7, sep } from "path";
5491
5658
  function effectiveLocales(config) {
5492
5659
  const limit = config.exportLocales;
@@ -5529,7 +5696,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
5529
5696
  if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
5530
5697
  const next = resolve7(dir, segment);
5531
5698
  if (isLast) {
5532
- if (stale(locale) && existsSync11(next) && statSync7(next).isFile()) {
5699
+ if (stale(locale) && existsSync12(next) && statSync7(next).isFile()) {
5533
5700
  unlinkSync(next);
5534
5701
  deleted++;
5535
5702
  removeEmptyDirs(dir, root);
@@ -5585,7 +5752,7 @@ function exportToDisk(state, projectRoot, opts) {
5585
5752
  writtenPaths.add(abs);
5586
5753
  let current = null;
5587
5754
  try {
5588
- current = readFileSync19(abs, "utf8");
5755
+ current = readFileSync20(abs, "utf8");
5589
5756
  } catch {
5590
5757
  }
5591
5758
  if (current === f.contents) {
@@ -5602,17 +5769,17 @@ function exportToDisk(state, projectRoot, opts) {
5602
5769
  }
5603
5770
 
5604
5771
  // src/server/ui-prefs.ts
5605
- import { readFileSync as readFileSync20 } from "fs";
5772
+ import { readFileSync as readFileSync21 } from "fs";
5606
5773
  import { homedir } from "os";
5607
- import { join as join15 } from "path";
5774
+ import { join as join16 } from "path";
5608
5775
  var THEMES = ["system", "light", "dark"];
5609
5776
  var isThemeMode = (v) => THEMES.includes(v);
5610
5777
  var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
5611
- var defaultUiPrefsPath = () => join15(homedir(), ".glotfile", "ui.json");
5778
+ var defaultUiPrefsPath = () => join16(homedir(), ".glotfile", "ui.json");
5612
5779
  var DEFAULTS = { theme: "system" };
5613
5780
  function readJson(path) {
5614
5781
  try {
5615
- const parsed = JSON.parse(readFileSync20(path, "utf8"));
5782
+ const parsed = JSON.parse(readFileSync21(path, "utf8"));
5616
5783
  return parsed && typeof parsed === "object" ? parsed : {};
5617
5784
  } catch {
5618
5785
  return {};
@@ -5631,7 +5798,7 @@ function saveUiPrefs(path, prefs) {
5631
5798
  }
5632
5799
 
5633
5800
  // src/server/local-settings.ts
5634
- import { readFileSync as readFileSync21 } from "fs";
5801
+ import { readFileSync as readFileSync22 } from "fs";
5635
5802
  import { resolve as resolve8 } from "path";
5636
5803
  var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
5637
5804
  var isEditorId = (v) => EDITOR_IDS.includes(v);
@@ -5646,7 +5813,7 @@ var DEFAULT_EDITOR = "vscode";
5646
5813
  var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
5647
5814
  function readJson2(path) {
5648
5815
  try {
5649
- const parsed = JSON.parse(readFileSync21(path, "utf8"));
5816
+ const parsed = JSON.parse(readFileSync22(path, "utf8"));
5650
5817
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5651
5818
  } catch {
5652
5819
  return {};
@@ -5717,15 +5884,30 @@ var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
5717
5884
  var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
5718
5885
  function projectName(root) {
5719
5886
  const nameFile = resolve9(root, ".idea", ".name");
5720
- if (existsSync12(nameFile)) {
5887
+ if (existsSync13(nameFile)) {
5721
5888
  try {
5722
- const name = readFileSync22(nameFile, "utf8").trim();
5889
+ const name = readFileSync23(nameFile, "utf8").trim();
5723
5890
  if (name) return name;
5724
5891
  } catch {
5725
5892
  }
5726
5893
  }
5727
5894
  return basename(root);
5728
5895
  }
5896
+ function attachUsageSnippets(targets, cache2, projectRoot) {
5897
+ const fileCache = /* @__PURE__ */ new Map();
5898
+ for (const target of targets) {
5899
+ const allRefs = Object.entries(cache2.files).flatMap(
5900
+ ([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
5901
+ key: r.key,
5902
+ file,
5903
+ line: r.line,
5904
+ col: r.col,
5905
+ scanner: r.scanner
5906
+ }))
5907
+ );
5908
+ target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
5909
+ }
5910
+ }
5729
5911
  function createApi(deps) {
5730
5912
  const app = new Hono();
5731
5913
  const load = () => loadState(deps.statePath);
@@ -5852,7 +6034,7 @@ function createApi(deps) {
5852
6034
  if (name.startsWith(".") || name === "node_modules") continue;
5853
6035
  const abs = resolve9(dir, name);
5854
6036
  let filePath = null;
5855
- if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync12(resolve9(abs, "config.json"))) {
6037
+ if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync13(resolve9(abs, "config.json"))) {
5856
6038
  filePath = resolve9(dir, `${name}.json`);
5857
6039
  } else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
5858
6040
  filePath = abs;
@@ -5886,7 +6068,7 @@ function createApi(deps) {
5886
6068
  const resolved = resolve9(projectRoot, path);
5887
6069
  const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
5888
6070
  if (!inside) return c.json({ error: "file is outside the project" }, 400);
5889
- if (!existsSync12(resolved)) return c.json({ error: "file not found" }, 400);
6071
+ if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
5890
6072
  loadState(resolved);
5891
6073
  deps.statePath = resolved;
5892
6074
  return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
@@ -5947,9 +6129,9 @@ function createApi(deps) {
5947
6129
  const abs = resolve9(root, screenshot);
5948
6130
  const rel = relative4(root, abs);
5949
6131
  const seg0 = rel.split(sep2)[0] ?? "";
5950
- if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync12(abs)) {
6132
+ if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync13(abs)) {
5951
6133
  try {
5952
- rmSync5(abs);
6134
+ rmSync6(abs);
5953
6135
  } catch {
5954
6136
  }
5955
6137
  }
@@ -6677,19 +6859,7 @@ function createApi(deps) {
6677
6859
  return;
6678
6860
  }
6679
6861
  await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
6680
- const fileCache = /* @__PURE__ */ new Map();
6681
- for (const target of targets) {
6682
- const allRefs = Object.entries(cache2.files).flatMap(
6683
- ([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
6684
- key: r.key,
6685
- file,
6686
- line: r.line,
6687
- col: r.col,
6688
- scanner: r.scanner
6689
- }))
6690
- );
6691
- target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
6692
- }
6862
+ attachUsageSnippets(targets, cache2, projectRoot);
6693
6863
  const system = buildContextSystemPrompt();
6694
6864
  const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
6695
6865
  const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
@@ -6741,6 +6911,100 @@ function createApi(deps) {
6741
6911
  await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
6742
6912
  });
6743
6913
  });
6914
+ app.get("/context/batch/status", async (c) => {
6915
+ const aiCfg = loadLocalSettings(projectRoot).ai;
6916
+ let supported = false;
6917
+ let provider;
6918
+ try {
6919
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
6920
+ supported = supportsBatchComplete(provider);
6921
+ } catch {
6922
+ }
6923
+ const pending = loadPendingContextBatch(projectRoot);
6924
+ if (!pending) return c.json({ supported, pending: null });
6925
+ const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
6926
+ if (!provider || !supportsBatchComplete(provider)) {
6927
+ return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
6928
+ }
6929
+ try {
6930
+ const status = await provider.translationBatchStatus(pending.batchId);
6931
+ return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
6932
+ } catch (e) {
6933
+ return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
6934
+ }
6935
+ });
6936
+ app.post("/context/batch", (c) => withTranslateLock(async () => {
6937
+ const body = await c.req.json().catch(() => ({}));
6938
+ const s = load();
6939
+ const cache2 = loadUsageCache(projectRoot);
6940
+ if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
6941
+ const targets = selectContextTargets(s, {
6942
+ all: body.all,
6943
+ keyGlob: body.keyGlob,
6944
+ limit: body.limit,
6945
+ since: body.since,
6946
+ keys: body.keys,
6947
+ force: body.force
6948
+ }, cache2, body.lastRunAt);
6949
+ if (!targets.length) return c.json({ error: "Nothing to build." }, 400);
6950
+ const aiCfg = loadLocalSettings(projectRoot).ai;
6951
+ let provider;
6952
+ try {
6953
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
6954
+ } catch (e) {
6955
+ return c.json({ error: e.message }, 400);
6956
+ }
6957
+ if (!supportsBatchComplete(provider)) {
6958
+ return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
6959
+ }
6960
+ attachUsageSnippets(targets, cache2, projectRoot);
6961
+ const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
6962
+ let pending;
6963
+ try {
6964
+ pending = await submitContextBatch(provider, targets, batchSize, aiCfg.model, projectRoot, body.force === true);
6965
+ } catch (e) {
6966
+ return c.json({ error: e.message }, 409);
6967
+ }
6968
+ appendLog(projectRoot, {
6969
+ at: (/* @__PURE__ */ new Date()).toISOString(),
6970
+ kind: "context",
6971
+ summary: `Submitted context batch ${pending.batchId} (${pending.total} keys)`,
6972
+ model: aiCfg.model,
6973
+ system: buildContextSystemPrompt(),
6974
+ items: targets.map((t) => ({ id: t.id, key: t.key, source: t.source }))
6975
+ });
6976
+ console.log(`[context-batch] submitted ${pending.batchId} \u2014 ${pending.total} key(s)`);
6977
+ return c.json({ batchId: pending.batchId, total: pending.total });
6978
+ }));
6979
+ app.post("/context/batch/apply", (c) => withTranslateLock(async () => {
6980
+ const pending = loadPendingContextBatch(projectRoot);
6981
+ if (!pending) return c.json({ error: "No pending context batch." }, 404);
6982
+ const aiCfg = loadLocalSettings(projectRoot).ai;
6983
+ let provider;
6984
+ try {
6985
+ provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
6986
+ } catch (e) {
6987
+ return c.json({ error: e.message }, 400);
6988
+ }
6989
+ if (!supportsBatchComplete(provider)) {
6990
+ return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
6991
+ }
6992
+ const outcome = await applyContextBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
6993
+ console.log(`[context-batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
6994
+ return c.json(outcome);
6995
+ }));
6996
+ app.post("/context/batch/cancel", async (c) => {
6997
+ const pending = loadPendingContextBatch(projectRoot);
6998
+ if (!pending) return c.json({ error: "No pending context batch." }, 404);
6999
+ const aiCfg = loadLocalSettings(projectRoot).ai;
7000
+ try {
7001
+ const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
7002
+ if (supportsBatchComplete(provider)) await provider.cancelTranslationBatch(pending.batchId);
7003
+ } catch {
7004
+ }
7005
+ clearPendingContextBatch(projectRoot);
7006
+ return c.json({ canceled: pending.batchId });
7007
+ });
6744
7008
  app.onError(
6745
7009
  (err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
6746
7010
  );
@@ -6749,7 +7013,7 @@ function createApi(deps) {
6749
7013
 
6750
7014
  // src/server/server.ts
6751
7015
  var here = dirname4(fileURLToPath(import.meta.url));
6752
- var DEFAULT_UI_DIR = join16(here, "..", "ui");
7016
+ var DEFAULT_UI_DIR = join17(here, "..", "ui");
6753
7017
  var MIME = {
6754
7018
  ".html": "text/html; charset=utf-8",
6755
7019
  ".js": "text/javascript; charset=utf-8",
@@ -6803,7 +7067,7 @@ function buildApp(opts) {
6803
7067
  const file = await readFileResponse(target);
6804
7068
  if (file) return file;
6805
7069
  }
6806
- const index = await readFileResponse(join16(root, "index.html"));
7070
+ const index = await readFileResponse(join17(root, "index.html"));
6807
7071
  if (index) return index;
6808
7072
  return c.notFound();
6809
7073
  });