@mariokreitz/langsync 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,10 +18,16 @@ chaos of hand-edited JSON or fragile Excel hand-offs.
18
18
 
19
19
  ## Install
20
20
 
21
+ <!-- embedme ../../docs/shared/install.sh -->
22
+
21
23
  ```bash
22
24
  pnpm add -D @mariokreitz/langsync
23
- # or: npm install -D @mariokreitz/langsync
24
- # or: yarn add -D @mariokreitz/langsync
25
+ # or
26
+ npm install -D @mariokreitz/langsync
27
+ # or
28
+ yarn add -D @mariokreitz/langsync
29
+
30
+
25
31
  ```
26
32
 
27
33
  > Requires **Node.js 22+** and an ESM-compatible project.
@@ -38,10 +44,13 @@ npx langsync validate
38
44
  # 3. Add missing keys (empty placeholders) to non-reference locales
39
45
  npx langsync sync
40
46
 
41
- # 4. Hand off to translators via Excel
47
+ # 4. Optionally fill the gaps with AI
48
+ npx langsync translate
49
+
50
+ # 5. Hand off to translators via Excel
42
51
  npx langsync export excel
43
52
 
44
- # 5. Import their work back
53
+ # 6. Import their work back
45
54
  npx langsync import excel
46
55
  ```
47
56
 
@@ -53,6 +62,8 @@ npx langsync import excel
53
62
  | `langsync validate` | Report missing, extra, and empty keys; exits non-zero on errors. |
54
63
  | `langsync find-missing` | Report missing keys per locale; exits non-zero on errors. |
55
64
  | `langsync sync` | Synchronize keys from the reference locale into every other locale. |
65
+ | `langsync translate` | Fill empty values in non-reference locales using an AI provider. |
66
+ | `langsync watch` | Watch locale files and run incremental sync + validation on change. |
56
67
  | `langsync export excel` | Export all locales into a single `.xlsx` workbook. |
57
68
  | `langsync import excel` | Import translations from a workbook back into JSON files. |
58
69
 
@@ -61,8 +72,9 @@ All read commands support `--reporter json`. All write commands support
61
72
 
62
73
  ## Configuration
63
74
 
75
+ <!-- embedme ../../docs/shared/config.ts -->
76
+
64
77
  ```ts
65
- // langsync.config.ts
66
78
  import { defineConfig } from '@mariokreitz/langsync';
67
79
 
68
80
  export default defineConfig({
@@ -71,6 +83,14 @@ export default defineConfig({
71
83
  locales: ['en', 'de', 'fr'],
72
84
  defaultLocale: 'en',
73
85
  framework: 'i18next',
86
+ excel: {
87
+ file: 'translations.xlsx',
88
+ sheetName: 'Translations',
89
+ },
90
+ ai: {
91
+ provider: 'openai',
92
+ model: 'gpt-5-mini',
93
+ },
74
94
  });
75
95
  ```
76
96
 
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { cosmiconfig } from 'cosmiconfig';
9
9
  import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
10
10
  import { z } from 'zod';
11
11
  import ExcelJS from 'exceljs';
12
+ import chokidar from 'chokidar';
12
13
 
13
14
  var PREFIX = chalk.bold.cyan("langsync");
14
15
  var logger = {
@@ -44,7 +45,7 @@ function assertNodeVersion(minMajor) {
44
45
  }
45
46
  }
46
47
  var FRAMEWORK_SIGNATURES = [
47
- { framework: "i18next", packages: ["i18next", "react-i18next", "vue-i18next"] },
48
+ { framework: "i18next", packages: ["i18next", "react-i18next", "i18next-vue", "vue-i18next"] },
48
49
  { framework: "ngx-translate", packages: ["@ngx-translate/core", "@ngx-translate/http-loader"] },
49
50
  { framework: "react-intl", packages: ["react-intl", "@formatjs/intl"] }
50
51
  ];
@@ -253,7 +254,12 @@ var LangSyncConfigSchema = z.object({
253
254
  excel: z.object({
254
255
  file: z.string().default("translations.xlsx"),
255
256
  sheetName: z.string().default("Translations")
256
- }).optional()
257
+ }).optional(),
258
+ ai: z.object({
259
+ provider: z.enum(["openai", "deepl", "anthropic", "gemini"]).default("openai").describe("AI translation provider."),
260
+ apiKey: z.string().optional().describe("API key. Falls back to the provider-specific env var."),
261
+ model: z.string().optional().describe("Provider model id (e.g. gpt-5-mini).")
262
+ }).optional().describe("AI translation settings.")
257
263
  });
258
264
  async function loadConfig(cwd = process.cwd()) {
259
265
  const explorer = cosmiconfig("langsync", {
@@ -669,8 +675,411 @@ function registerImportCommand(program) {
669
675
  });
670
676
  }
671
677
 
678
+ // ../ai-engine/dist/index.js
679
+ var DEFAULT_MODEL = "gpt-5-mini";
680
+ var ENDPOINT = "https://api.openai.com/v1/chat/completions";
681
+ var OpenAIAdapter = class {
682
+ provider = "openai";
683
+ apiKey;
684
+ model;
685
+ fetchImpl;
686
+ constructor(options = {}) {
687
+ const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
688
+ if (!apiKey) {
689
+ throw new Error(
690
+ "OpenAI API key missing. Set `ai.apiKey` in your config or the OPENAI_API_KEY env var."
691
+ );
692
+ }
693
+ this.apiKey = apiKey;
694
+ this.model = options.model ?? DEFAULT_MODEL;
695
+ this.fetchImpl = options.fetchImpl ?? fetch;
696
+ }
697
+ async translate(request) {
698
+ const response = await this.fetchImpl(ENDPOINT, {
699
+ method: "POST",
700
+ headers: {
701
+ "content-type": "application/json",
702
+ authorization: `Bearer ${this.apiKey}`
703
+ },
704
+ body: JSON.stringify({
705
+ model: this.model,
706
+ temperature: 0,
707
+ messages: [
708
+ {
709
+ role: "system",
710
+ content: `You are a professional software localization engine. Translate the user message from ${request.sourceLocale} to ${request.targetLocale}. Preserve placeholders, ICU syntax, and surrounding punctuation. Respond with the translation only, no quotes or commentary.`
711
+ },
712
+ { role: "user", content: request.text }
713
+ ]
714
+ })
715
+ });
716
+ if (!response.ok) {
717
+ throw new Error(`OpenAI request failed: ${response.status} ${response.statusText}`);
718
+ }
719
+ const data = await response.json();
720
+ const content = data.choices?.[0]?.message?.content?.trim();
721
+ if (!content) {
722
+ throw new Error("OpenAI returned an empty translation.");
723
+ }
724
+ return content;
725
+ }
726
+ };
727
+ var FREE_ENDPOINT = "https://api-free.deepl.com/v2/translate";
728
+ var PRO_ENDPOINT = "https://api.deepl.com/v2/translate";
729
+ var FREE_KEY_SUFFIX = ":fx";
730
+ function toDeepLLang(locale) {
731
+ return locale.split("-")[0].toUpperCase();
732
+ }
733
+ var DeepLAdapter = class {
734
+ provider = "deepl";
735
+ apiKey;
736
+ endpoint;
737
+ fetchImpl;
738
+ constructor(options = {}) {
739
+ const apiKey = options.apiKey ?? process.env.DEEPL_API_KEY;
740
+ if (!apiKey) {
741
+ throw new Error(
742
+ "DeepL API key missing. Set `ai.apiKey` in your config or the DEEPL_API_KEY env var."
743
+ );
744
+ }
745
+ this.apiKey = apiKey;
746
+ const useFreeTier = options.useFreeTier ?? apiKey.endsWith(FREE_KEY_SUFFIX);
747
+ this.endpoint = useFreeTier ? FREE_ENDPOINT : PRO_ENDPOINT;
748
+ this.fetchImpl = options.fetchImpl ?? fetch;
749
+ }
750
+ async translate(request) {
751
+ const response = await this.fetchImpl(this.endpoint, {
752
+ method: "POST",
753
+ headers: {
754
+ "content-type": "application/json",
755
+ authorization: `DeepL-Auth-Key ${this.apiKey}`
756
+ },
757
+ body: JSON.stringify({
758
+ text: [request.text],
759
+ source_lang: toDeepLLang(request.sourceLocale),
760
+ target_lang: toDeepLLang(request.targetLocale)
761
+ })
762
+ });
763
+ if (!response.ok) {
764
+ throw new Error(`DeepL request failed: ${response.status} ${response.statusText}`);
765
+ }
766
+ const data = await response.json();
767
+ const content = data.translations?.[0]?.text?.trim();
768
+ if (!content) {
769
+ throw new Error("DeepL returned an empty translation.");
770
+ }
771
+ return content;
772
+ }
773
+ };
774
+ var DEFAULT_MODEL2 = "claude-haiku-4-5";
775
+ var ENDPOINT2 = "https://api.anthropic.com/v1/messages";
776
+ var ANTHROPIC_VERSION = "2023-06-01";
777
+ var MAX_TOKENS = 1024;
778
+ var AnthropicAdapter = class {
779
+ provider = "anthropic";
780
+ apiKey;
781
+ model;
782
+ fetchImpl;
783
+ constructor(options = {}) {
784
+ const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
785
+ if (!apiKey) {
786
+ throw new Error(
787
+ "Anthropic API key missing. Set `ai.apiKey` in your config or the ANTHROPIC_API_KEY env var."
788
+ );
789
+ }
790
+ this.apiKey = apiKey;
791
+ this.model = options.model ?? DEFAULT_MODEL2;
792
+ this.fetchImpl = options.fetchImpl ?? fetch;
793
+ }
794
+ async translate(request) {
795
+ const response = await this.fetchImpl(ENDPOINT2, {
796
+ method: "POST",
797
+ headers: {
798
+ "content-type": "application/json",
799
+ "x-api-key": this.apiKey,
800
+ "anthropic-version": ANTHROPIC_VERSION
801
+ },
802
+ body: JSON.stringify({
803
+ model: this.model,
804
+ max_tokens: MAX_TOKENS,
805
+ system: `You are a professional software localization engine. Translate the user message from ${request.sourceLocale} to ${request.targetLocale}. Preserve placeholders, ICU syntax, and surrounding punctuation. Respond with the translation only, no quotes or commentary.`,
806
+ messages: [{ role: "user", content: request.text }]
807
+ })
808
+ });
809
+ if (!response.ok) {
810
+ throw new Error(`Anthropic request failed: ${response.status} ${response.statusText}`);
811
+ }
812
+ const data = await response.json();
813
+ const content = data.content?.find((block) => block.type === "text" || block.text)?.text?.trim();
814
+ if (!content) {
815
+ throw new Error("Anthropic returned an empty translation.");
816
+ }
817
+ return content;
818
+ }
819
+ };
820
+ var DEFAULT_MODEL3 = "gemini-3-flash";
821
+ var BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
822
+ var GeminiAdapter = class {
823
+ provider = "gemini";
824
+ apiKey;
825
+ model;
826
+ fetchImpl;
827
+ constructor(options = {}) {
828
+ const apiKey = options.apiKey ?? process.env.GEMINI_API_KEY;
829
+ if (!apiKey) {
830
+ throw new Error(
831
+ "Gemini API key missing. Set `ai.apiKey` in your config or the GEMINI_API_KEY env var."
832
+ );
833
+ }
834
+ this.apiKey = apiKey;
835
+ this.model = options.model ?? DEFAULT_MODEL3;
836
+ this.fetchImpl = options.fetchImpl ?? fetch;
837
+ }
838
+ async translate(request) {
839
+ const url = `${BASE_URL}/${this.model}:generateContent?key=${this.apiKey}`;
840
+ const response = await this.fetchImpl(url, {
841
+ method: "POST",
842
+ headers: { "content-type": "application/json" },
843
+ body: JSON.stringify({
844
+ systemInstruction: {
845
+ parts: [
846
+ {
847
+ text: `You are a professional software localization engine. Translate the user message from ${request.sourceLocale} to ${request.targetLocale}. Preserve placeholders, ICU syntax, and surrounding punctuation. Respond with the translation only, no quotes or commentary.`
848
+ }
849
+ ]
850
+ },
851
+ contents: [{ parts: [{ text: request.text }] }],
852
+ generationConfig: { temperature: 0 }
853
+ })
854
+ });
855
+ if (!response.ok) {
856
+ throw new Error(`Gemini request failed: ${response.status} ${response.statusText}`);
857
+ }
858
+ const data = await response.json();
859
+ const content = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
860
+ if (!content) {
861
+ throw new Error("Gemini returned an empty translation.");
862
+ }
863
+ return content;
864
+ }
865
+ };
866
+ var RELEASED_PROVIDERS = ["openai"];
867
+ var ALL_PROVIDERS = ["openai", "deepl", "anthropic", "gemini"];
868
+ function experimentalEnabled() {
869
+ return process.env.LANGSYNC_AI_EXPERIMENTAL === "1";
870
+ }
871
+ function availableProviders() {
872
+ return experimentalEnabled() ? [...ALL_PROVIDERS] : [...RELEASED_PROVIDERS];
873
+ }
874
+ function createAdapter(options) {
875
+ if (!availableProviders().includes(options.provider)) {
876
+ if (ALL_PROVIDERS.includes(options.provider)) {
877
+ throw new Error(
878
+ `AI provider "${options.provider}" is not yet available. Currently supported: ${availableProviders().join(", ")}.`
879
+ );
880
+ }
881
+ throw new Error(`Unknown AI provider "${options.provider}".`);
882
+ }
883
+ const { provider, ...rest } = options;
884
+ switch (provider) {
885
+ case "openai":
886
+ return new OpenAIAdapter(rest);
887
+ case "deepl":
888
+ return new DeepLAdapter(rest);
889
+ case "anthropic":
890
+ return new AnthropicAdapter(rest);
891
+ case "gemini":
892
+ return new GeminiAdapter(rest);
893
+ default: {
894
+ const exhaustive = provider;
895
+ throw new Error(`AI provider "${String(exhaustive)}" has no adapter implementation yet.`);
896
+ }
897
+ }
898
+ }
899
+ function isEmpty(value) {
900
+ return value === void 0 || value.trim() === "";
901
+ }
902
+ async function fillEmptyTranslations(options) {
903
+ const referenceFlat = flatten(options.reference);
904
+ const targetFlat = flatten(options.target);
905
+ const translatedKeys = [];
906
+ for (const [key, referenceValue] of Object.entries(referenceFlat)) {
907
+ if (isEmpty(referenceValue)) continue;
908
+ if (!isEmpty(targetFlat[key])) continue;
909
+ targetFlat[key] = await options.adapter.translate({
910
+ text: referenceValue,
911
+ sourceLocale: options.sourceLocale,
912
+ targetLocale: options.targetLocale
913
+ });
914
+ translatedKeys.push(key);
915
+ }
916
+ return { tree: unflatten(targetFlat), translatedKeys };
917
+ }
918
+
919
+ // src/commands/translate/run.ts
920
+ async function runTranslate(options) {
921
+ const loaded = await loadConfig(options.cwd);
922
+ if (!loaded) {
923
+ throw new Error("No LangSync config found. Run `langsync init` first.");
924
+ }
925
+ const { config } = loaded;
926
+ const referenceLocale = config.defaultLocale ?? config.locales[0];
927
+ const provider = options.provider ?? config.ai?.provider ?? "openai";
928
+ const adapter = createAdapter({
929
+ provider,
930
+ apiKey: config.ai?.apiKey,
931
+ model: options.model ?? config.ai?.model
932
+ });
933
+ const files = await loadLocaleFiles({
934
+ cwd: options.cwd,
935
+ inputDir: config.input,
936
+ locales: config.locales
937
+ });
938
+ const reference = files.find((f) => f.locale === referenceLocale);
939
+ if (!reference) {
940
+ throw new Error(`Could not find reference locale file for "${referenceLocale}".`);
941
+ }
942
+ const targets = files.filter((f) => f.locale !== referenceLocale);
943
+ const written = [];
944
+ const planned = [];
945
+ const translatedByLocale = {};
946
+ for (const target of targets) {
947
+ const { tree, translatedKeys } = await fillEmptyTranslations({
948
+ reference: reference.translations,
949
+ target: target.translations,
950
+ sourceLocale: referenceLocale,
951
+ targetLocale: target.locale,
952
+ adapter
953
+ });
954
+ translatedByLocale[target.locale] = translatedKeys;
955
+ if (translatedKeys.length === 0) continue;
956
+ planned.push(target.path);
957
+ if (!options.dryRun) {
958
+ await writeJson(target.path, tree);
959
+ written.push(target.path);
960
+ }
961
+ }
962
+ return { provider, referenceLocale, written, planned, translatedByLocale };
963
+ }
964
+
965
+ // src/commands/translate.ts
966
+ function registerTranslateCommand(program) {
967
+ program.command("translate").description("Fill empty values in non-reference locales using an AI provider.").option("--provider <provider>", "Override the configured AI provider.").option("--model <model>", "Override the configured provider model.").option("--dry-run", "Report what would be translated without writing files.", false).action(async (flags) => {
968
+ try {
969
+ const cwd = process.cwd();
970
+ const result = await runTranslate({
971
+ cwd,
972
+ dryRun: flags.dryRun,
973
+ provider: flags.provider,
974
+ model: flags.model
975
+ });
976
+ const totals = Object.values(result.translatedByLocale).reduce(
977
+ (sum, keys) => sum + keys.length,
978
+ 0
979
+ );
980
+ if (totals === 0) {
981
+ logger.info(`Nothing to translate with ${chalk.cyan(result.provider)}.`);
982
+ return;
983
+ }
984
+ const verb = flags.dryRun ? "Would translate" : "Translated";
985
+ for (const [locale, keys] of Object.entries(result.translatedByLocale)) {
986
+ if (keys.length === 0) continue;
987
+ logger.success(`${verb} ${chalk.bold(String(keys.length))} key(s) for ${locale}`);
988
+ }
989
+ if (!flags.dryRun) {
990
+ for (const path of result.written) {
991
+ logger.info(`Wrote ${chalk.bold(relative(cwd, path))}`);
992
+ }
993
+ }
994
+ } catch (error) {
995
+ const message = error instanceof Error ? error.message : String(error);
996
+ logger.error(message);
997
+ process.exitCode = 1;
998
+ }
999
+ });
1000
+ }
1001
+ async function resolveWatchDir(cwd) {
1002
+ const loaded = await loadConfig(cwd);
1003
+ if (!loaded) {
1004
+ throw new Error("No LangSync config found. Run `langsync init` first.");
1005
+ }
1006
+ return resolve(cwd, loaded.config.input);
1007
+ }
1008
+ async function runWatchPass(options) {
1009
+ const { referenceLocale, written } = await runSync({
1010
+ cwd: options.cwd,
1011
+ dryRun: options.dryRun
1012
+ });
1013
+ const loaded = await loadConfig(options.cwd);
1014
+ if (!loaded) {
1015
+ throw new Error("No LangSync config found. Run `langsync init` first.");
1016
+ }
1017
+ const files = await loadLocaleFiles({
1018
+ cwd: options.cwd,
1019
+ inputDir: loaded.config.input,
1020
+ locales: loaded.config.locales
1021
+ });
1022
+ const issues = validateLocales(files, referenceLocale);
1023
+ return { referenceLocale, written, issues };
1024
+ }
1025
+
1026
+ // src/commands/watch.ts
1027
+ function reportPass(cwd, written, issueCount) {
1028
+ if (written.length === 0) {
1029
+ logger.info("No locale changes to sync.");
1030
+ } else {
1031
+ for (const path of written) {
1032
+ logger.success(`Synced ${chalk.bold(relative(cwd, path))}`);
1033
+ }
1034
+ }
1035
+ if (issueCount > 0) {
1036
+ logger.warn(`${chalk.yellow(String(issueCount))} validation issue(s) remaining.`);
1037
+ } else {
1038
+ logger.success("All locales are consistent.");
1039
+ }
1040
+ }
1041
+ function registerWatchCommand(program) {
1042
+ program.command("watch").description("Watch locale files and run incremental sync + validation on change.").option("--dry-run", "Report planned changes without writing files.", false).option("--debounce <ms>", "Debounce window for rapid file changes.", "200").action(async (flags) => {
1043
+ try {
1044
+ const cwd = process.cwd();
1045
+ const watchDir = await resolveWatchDir(cwd);
1046
+ const debounceMs = Number.parseInt(flags.debounce, 10) || 200;
1047
+ const initial = await runWatchPass({ cwd, dryRun: flags.dryRun });
1048
+ reportPass(cwd, initial.written, initial.issues.length);
1049
+ logger.info(`Watching ${chalk.cyan(relative(cwd, watchDir) || ".")} for changes...`);
1050
+ const watcher = chokidar.watch(join(watchDir, "*.json"), {
1051
+ ignoreInitial: true
1052
+ });
1053
+ let timer;
1054
+ let running = false;
1055
+ const trigger = () => {
1056
+ if (timer) clearTimeout(timer);
1057
+ timer = setTimeout(() => {
1058
+ void (async () => {
1059
+ if (running) return;
1060
+ running = true;
1061
+ try {
1062
+ const pass = await runWatchPass({ cwd, dryRun: flags.dryRun });
1063
+ reportPass(cwd, pass.written, pass.issues.length);
1064
+ } catch (error) {
1065
+ logger.error(error instanceof Error ? error.message : String(error));
1066
+ } finally {
1067
+ running = false;
1068
+ }
1069
+ })();
1070
+ }, debounceMs);
1071
+ };
1072
+ watcher.on("change", trigger).on("add", trigger).on("unlink", trigger);
1073
+ } catch (error) {
1074
+ const message = error instanceof Error ? error.message : String(error);
1075
+ logger.error(message);
1076
+ process.exitCode = 1;
1077
+ }
1078
+ });
1079
+ }
1080
+
672
1081
  // src/cli.ts
673
- var VERSION = "0.0.0";
1082
+ var VERSION = "0.4.0" ;
674
1083
  async function main() {
675
1084
  assertNodeVersion(22);
676
1085
  const program = new Command();
@@ -681,6 +1090,8 @@ async function main() {
681
1090
  registerSyncCommand(program);
682
1091
  registerValidateCommand(program);
683
1092
  registerFindMissingCommand(program);
1093
+ registerTranslateCommand(program);
1094
+ registerWatchCommand(program);
684
1095
  registerExportCommand(program);
685
1096
  registerImportCommand(program);
686
1097
  await program.parseAsync(process.argv);
package/dist/index.js CHANGED
@@ -12,7 +12,12 @@ z.object({
12
12
  excel: z.object({
13
13
  file: z.string().default("translations.xlsx"),
14
14
  sheetName: z.string().default("Translations")
15
- }).optional()
15
+ }).optional(),
16
+ ai: z.object({
17
+ provider: z.enum(["openai", "deepl", "anthropic", "gemini"]).default("openai").describe("AI translation provider."),
18
+ apiKey: z.string().optional().describe("API key. Falls back to the provider-specific env var."),
19
+ model: z.string().optional().describe("Provider model id (e.g. gpt-5-mini).")
20
+ }).optional().describe("AI translation settings.")
16
21
  });
17
22
  function defineConfig(config) {
18
23
  return config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariokreitz/langsync",
3
- "version": "0.1.1",
3
+ "version": "0.4.0",
4
4
  "description": "Modern localization workflow tooling for TypeScript applications.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -10,7 +10,10 @@
10
10
  "cli",
11
11
  "i18next",
12
12
  "ngx-translate",
13
- "react-intl"
13
+ "react-intl",
14
+ "ai-translation",
15
+ "openai",
16
+ "watch-mode"
14
17
  ],
15
18
  "license": "MIT",
16
19
  "author": "Mario Kreitz",
@@ -19,7 +22,7 @@
19
22
  "url": "git+https://github.com/mariokreitz/langsync.git",
20
23
  "directory": "packages/cli"
21
24
  },
22
- "homepage": "https://github.com/mariokreitz/langsync#readme",
25
+ "homepage": "https://docs.langsync.kreitz-webdev.de",
23
26
  "bugs": {
24
27
  "url": "https://github.com/mariokreitz/langsync/issues"
25
28
  },
@@ -49,6 +52,7 @@
49
52
  },
50
53
  "dependencies": {
51
54
  "chalk": "^5.3.0",
55
+ "chokidar": "^4.0.3",
52
56
  "commander": "^12.1.0",
53
57
  "cosmiconfig": "^9.0.0",
54
58
  "cosmiconfig-typescript-loader": "^6.1.0",
@@ -60,10 +64,11 @@
60
64
  },
61
65
  "devDependencies": {
62
66
  "@types/prompts": "^2.4.9",
63
- "memfs": "^4.15.1",
67
+ "memfs": "^4.57.3",
68
+ "@langsync/shared": "0.1.0",
64
69
  "@langsync/core": "0.1.0",
65
70
  "@langsync/excel-engine": "0.1.0",
66
- "@langsync/shared": "0.1.0"
71
+ "@langsync/ai-engine": "0.1.0"
67
72
  },
68
73
  "scripts": {
69
74
  "build": "tsup",