@mindstudio-ai/local-model-tunnel 0.3.1 → 0.3.2

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.
@@ -7,17 +7,19 @@ import {
7
7
  getConfigPath,
8
8
  getEnvironment,
9
9
  getProviderStatuses,
10
- getRegisteredModels,
10
+ getSyncedModels,
11
11
  pollDeviceAuth,
12
- registerLocalModel,
13
12
  requestDeviceAuth,
14
13
  requestEvents,
15
14
  setApiKey,
15
+ syncLocalModel,
16
+ updateLocalModel,
16
17
  verifyApiKey
17
- } from "./chunk-PTK4SJQK.js";
18
+ } from "./chunk-V3RKCMCQ.js";
18
19
 
19
20
  // src/tui/index.tsx
20
21
  import { render } from "ink";
22
+ import { execFileSync, execSync } from "child_process";
21
23
 
22
24
  // src/tui/App.tsx
23
25
  import { useEffect as useEffect13, useCallback as useCallback10, useState as useState12 } from "react";
@@ -146,7 +148,7 @@ function NavigationMenu({ items, onSelect, title }) {
146
148
  const separatorExtraLines = items.filter((item, idx) => item.isSeparator && idx > 0).length;
147
149
  const menuHeight = items.length + 4 + separatorExtraLines;
148
150
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, marginBottom: 1, borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray", children: [
149
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: title ?? "Actions" }) }),
151
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", children: title ?? "Actions" }) }),
150
152
  /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map((item, index) => {
151
153
  if (item.isSeparator) {
152
154
  return /* @__PURE__ */ jsx2(Box2, { marginTop: index > 0 ? 1 : 0, children: item.label ? /* @__PURE__ */ jsx2(Text2, { bold: true, color: item.color ?? "gray", wrap: "truncate-end", children: item.label }) : null }, item.id);
@@ -173,7 +175,7 @@ function NavigationMenu({ items, onSelect, title }) {
173
175
  ] })
174
176
  ] }, item.id);
175
177
  }) }),
176
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, wrap: "truncate-end", children: items.some((i) => i.id === "back") ? "Up/Down Navigate \u2022 Enter Select \u2022 q/Esc Back" : "Up/Down Navigate \u2022 Enter Select \u2022 q Quit" }) })
178
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", wrap: "truncate-end", children: items.some((i) => i.id === "back") ? "Up/Down Navigate \u2022 Enter Select \u2022 q/Esc Back" : "Up/Down Navigate \u2022 Enter Select \u2022 q Quit" }) })
177
179
  ] });
178
180
  }
179
181
 
@@ -244,12 +246,14 @@ function useProviders(pollInterval = 1e4) {
244
246
  import { useState as useState4, useEffect as useEffect4, useCallback as useCallback3 } from "react";
245
247
  function useModels() {
246
248
  const [models, setModels] = useState4([]);
249
+ const [warnings, setWarnings] = useState4([]);
247
250
  const [loading, setLoading] = useState4(true);
248
251
  const refresh = useCallback3(async () => {
249
252
  setLoading(true);
250
253
  try {
251
254
  const discoveredModels = await discoverAllModels();
252
- setModels(discoveredModels);
255
+ setModels(discoveredModels.filter((m) => !m.statusHint));
256
+ setWarnings(discoveredModels.filter((m) => !!m.statusHint));
253
257
  } catch {
254
258
  } finally {
255
259
  setLoading(false);
@@ -260,6 +264,7 @@ function useModels() {
260
264
  }, [refresh]);
261
265
  return {
262
266
  models,
267
+ warnings,
263
268
  loading,
264
269
  refresh
265
270
  };
@@ -293,10 +298,12 @@ function useRequests(maxHistory = 50) {
293
298
  });
294
299
  const unsubProgress = requestEvents.onProgress((event) => {
295
300
  const existing = requestsRef.current.get(event.id);
296
- if (existing && existing.status === "processing" && event.content) {
301
+ if (existing && existing.status === "processing") {
297
302
  const updated = {
298
303
  ...existing,
299
- content: event.content
304
+ ...event.content !== void 0 && { content: event.content },
305
+ ...event.step !== void 0 && { step: event.step },
306
+ ...event.totalSteps !== void 0 && { totalSteps: event.totalSteps }
300
307
  };
301
308
  requestsRef.current.set(event.id, updated);
302
309
  setRequests(
@@ -341,18 +348,18 @@ function useRequests(maxHistory = 50) {
341
348
 
342
349
  // src/tui/hooks/useRegisteredModels.ts
343
350
  import { useState as useState6, useEffect as useEffect6, useCallback as useCallback5 } from "react";
344
- function useRegisteredModels(connectionStatus) {
345
- const [registeredNames, setRegisteredNames] = useState6(
351
+ function useSyncedModels(connectionStatus) {
352
+ const [syncedNames, setSyncedNames] = useState6(
346
353
  /* @__PURE__ */ new Set()
347
354
  );
348
355
  const refresh = useCallback5(async () => {
349
356
  if (connectionStatus !== "connected") {
350
- setRegisteredNames(/* @__PURE__ */ new Set());
357
+ setSyncedNames(/* @__PURE__ */ new Set());
351
358
  return;
352
359
  }
353
360
  try {
354
- const models = await getRegisteredModels();
355
- setRegisteredNames(new Set(models));
361
+ const models = await getSyncedModels();
362
+ setSyncedNames(new Set(models.map((m) => m.name)));
356
363
  } catch {
357
364
  }
358
365
  }, [connectionStatus]);
@@ -360,7 +367,7 @@ function useRegisteredModels(connectionStatus) {
360
367
  refresh();
361
368
  }, [refresh]);
362
369
  return {
363
- registeredNames,
370
+ syncedNames,
364
371
  refresh
365
372
  };
366
373
  }
@@ -392,9 +399,9 @@ function getRequestTypeLabel(type) {
392
399
  case "llm_chat":
393
400
  return { label: "text", color: "gray" };
394
401
  case "image_generation":
395
- return { label: "image", color: "magenta" };
402
+ return { label: "image", color: "gray" };
396
403
  case "video_generation":
397
- return { label: "video", color: "cyan" };
404
+ return { label: "video", color: "gray" };
398
405
  default:
399
406
  return { label: type, color: "gray" };
400
407
  }
@@ -412,6 +419,7 @@ function RequestItem({ request, width }) {
412
419
  if (request.status === "processing") {
413
420
  const elapsed = Date.now() - request.startTime;
414
421
  const snippet = request.content && request.requestType === "llm_chat" ? snippetLine(request.content, snippetWidth) : null;
422
+ const stepProgress = request.step !== void 0 && request.totalSteps ? `Step ${request.step}/${request.totalSteps}` : null;
415
423
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
416
424
  /* @__PURE__ */ jsxs3(Box3, { children: [
417
425
  /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: /* @__PURE__ */ jsx3(Spinner, { type: "dots" }) }),
@@ -429,9 +437,13 @@ function RequestItem({ request, width }) {
429
437
  "..."
430
438
  ] })
431
439
  ] }),
432
- snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, wrap: "truncate-end", children: [
440
+ snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", wrap: "truncate-end", children: [
433
441
  snippetIndent,
434
442
  snippet
443
+ ] }),
444
+ stepProgress && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
445
+ snippetIndent,
446
+ stepProgress
435
447
  ] })
436
448
  ] });
437
449
  }
@@ -463,7 +475,7 @@ function RequestItem({ request, width }) {
463
475
  resultInfo
464
476
  ] })
465
477
  ] }),
466
- snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, wrap: "truncate-end", children: [
478
+ snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", wrap: "truncate-end", children: [
467
479
  snippetIndent,
468
480
  snippet
469
481
  ] })
@@ -490,7 +502,11 @@ function RequestLog({ requests, maxVisible = 8, hasModels = true }) {
490
502
  const width = stdout?.columns ?? 80;
491
503
  const activeRequests = requests.filter((r) => r.status === "processing");
492
504
  const completedRequests = requests.filter((r) => r.status !== "processing");
493
- const itemLines = (r) => r.requestType === "llm_chat" && r.content ? 2 : 1;
505
+ const itemLines = (r) => {
506
+ if (r.requestType === "llm_chat" && r.content) return 2;
507
+ if (r.status === "processing" && r.step !== void 0) return 2;
508
+ return 1;
509
+ };
494
510
  let completedToShow = [];
495
511
  let linesUsed = activeRequests.reduce((sum, r) => sum + itemLines(r), 0);
496
512
  for (let i = completedRequests.length - 1; i >= 0 && linesUsed < maxVisible; i--) {
@@ -521,7 +537,7 @@ function RequestLog({ requests, maxVisible = 8, hasModels = true }) {
521
537
  " active)"
522
538
  ] })
523
539
  ] }),
524
- requests.length === 0 ? /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: hasModels ? "Tunnel is live \u2014 requests will appear here when models are used in MindStudio" : "Start a model to begin receiving generation requests." }) }) : /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: visibleRequests.map((request) => /* @__PURE__ */ jsx3(RequestItem, { request, width }, request.id)) })
540
+ requests.length === 0 ? /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: hasModels ? "Tunnel is live \u2014 requests will appear here when models are used in MindStudio" : "Start a model to begin receiving generation requests." }) }) : /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: visibleRequests.map((request) => /* @__PURE__ */ jsx3(RequestItem, { request, width }, request.id)) })
525
541
  ]
526
542
  }
527
543
  );
@@ -546,14 +562,19 @@ function useSetupProviders() {
546
562
 
547
563
  // src/tui/pages/DashboardPage.tsx
548
564
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
565
+ function getWorkflowCount(model) {
566
+ const param = model.parameters?.find((p) => p.type === "comfyWorkflow");
567
+ if (!param) return null;
568
+ return param.comfyWorkflowOptions.availableWorkflows.length;
569
+ }
549
570
  function getCapabilityLabel(capability) {
550
571
  switch (capability) {
551
572
  case "text":
552
573
  return { label: "Text Generation", color: "gray" };
553
574
  case "image":
554
- return { label: "Image Generation", color: "magenta" };
575
+ return { label: "Image Generation", color: "gray" };
555
576
  case "video":
556
- return { label: "Video Generation", color: "cyan" };
577
+ return { label: "Video Generation", color: "gray" };
557
578
  default:
558
579
  return { label: capability, color: "gray" };
559
580
  }
@@ -561,7 +582,8 @@ function getCapabilityLabel(capability) {
561
582
  function DashboardPage({
562
583
  requests,
563
584
  models,
564
- registeredNames,
585
+ modelWarnings = [],
586
+ syncedNames,
565
587
  modelsLoading,
566
588
  onNavigate
567
589
  }) {
@@ -574,7 +596,7 @@ function DashboardPage({
574
596
  );
575
597
  const provStatusWidth = "Local Server Running".length;
576
598
  const allModelNames = new Set(models.map((m) => m.name));
577
- const unavailableRegistered = [...registeredNames].filter(
599
+ const unavailableSynced = [...syncedNames].filter(
578
600
  (name) => !allModelNames.has(name)
579
601
  );
580
602
  const menuItems = useMemo(() => {
@@ -610,7 +632,7 @@ function DashboardPage({
610
632
  const headerLines = 14;
611
633
  const providerContentLines = setupLoading ? 1 : installedProviders.length === 0 ? 2 : installedProviders.length;
612
634
  const providersLines = 3 + providerContentLines;
613
- const modelContentLines = modelsLoading ? 1 : models.length === 0 && unavailableRegistered.length === 0 ? 2 : models.length + (unavailableRegistered.length > 0 ? 1 + unavailableRegistered.length : 0);
635
+ const modelContentLines = modelsLoading ? 1 : models.length === 0 && unavailableSynced.length === 0 && modelWarnings.length === 0 ? 2 : models.length + modelWarnings.length + (unavailableSynced.length > 0 ? 1 + unavailableSynced.length : 0);
614
636
  const modelsLines = 3 + modelContentLines;
615
637
  const requestLogOverhead = 3;
616
638
  const menuLines = menuItems.length + 6;
@@ -641,26 +663,40 @@ function DashboardPage({
641
663
  modelsLoading ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
642
664
  /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
643
665
  /* @__PURE__ */ jsx4(Text4, { children: " Discovering models..." })
644
- ] }) : models.length === 0 && unavailableRegistered.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
666
+ ] }) : models.length === 0 && unavailableSynced.length === 0 && modelWarnings.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
645
667
  /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "No models found." }),
646
668
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Download models using your provider (e.g., ollama pull llama3.2)" })
647
669
  ] }) : /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
648
670
  models.map((model) => {
649
671
  const cap = getCapabilityLabel(model.capability);
650
- const isRegistered = registeredNames.has(model.name);
672
+ const isSynced = syncedNames.has(model.name);
651
673
  const displayProvider = providers.find((p) => p.provider.name === model.provider)?.provider.displayName ?? model.provider;
674
+ const workflowCount = getWorkflowCount(model);
675
+ const workflowSuffix = workflowCount !== null ? ` (${workflowCount} workflow${workflowCount !== 1 ? "s" : ""}, ${isSynced ? workflowCount : 0} synced)` : "";
652
676
  return /* @__PURE__ */ jsxs4(Box4, { children: [
653
- /* @__PURE__ */ jsx4(Text4, { color: isRegistered ? "green" : "gray", children: isRegistered ? "\u25CF" : "\u25CB" }),
677
+ /* @__PURE__ */ jsx4(Text4, { color: isSynced ? "green" : "gray", children: isSynced ? "\u25CF" : "\u25CB" }),
654
678
  /* @__PURE__ */ jsx4(Text4, { color: "white", children: ` ${model.name}` }),
679
+ workflowSuffix && /* @__PURE__ */ jsx4(Text4, { color: "gray", children: workflowSuffix }),
655
680
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
656
681
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: displayProvider }),
657
682
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
658
683
  /* @__PURE__ */ jsx4(Text4, { color: cap.color, children: cap.label })
659
684
  ] }, model.name);
660
685
  }),
661
- unavailableRegistered.length > 0 && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: models.length > 0 ? 1 : 0, children: [
662
- /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Registered but not currently available:" }),
663
- unavailableRegistered.map((name) => /* @__PURE__ */ jsxs4(Box4, { children: [
686
+ modelWarnings.map((warning) => {
687
+ const displayProvider = providers.find((p) => p.provider.name === warning.provider)?.provider.displayName ?? warning.provider;
688
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
689
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u25CB" }),
690
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: ` ${warning.name}` }),
691
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
692
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: displayProvider }),
693
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
694
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: warning.statusHint })
695
+ ] }, warning.name);
696
+ }),
697
+ unavailableSynced.length > 0 && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: models.length > 0 ? 1 : 0, children: [
698
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Synced but not currently available:" }),
699
+ unavailableSynced.map((name) => /* @__PURE__ */ jsxs4(Box4, { children: [
664
700
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u25CB" }),
665
701
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: ` ${name}` })
666
702
  ] }, name))
@@ -691,13 +727,13 @@ var MODEL_TYPE_MAP = {
691
727
  image: "image_generation",
692
728
  video: "video_generation"
693
729
  };
694
- function useRegister() {
730
+ function useSync() {
695
731
  const [status, setStatus] = useState8("idle");
696
732
  const [progress, setProgress] = useState8({
697
733
  current: 0,
698
734
  total: 0
699
735
  });
700
- const [registeredModels, setRegisteredModels] = useState8(
736
+ const [syncedModels, setSyncedModels] = useState8(
701
737
  []
702
738
  );
703
739
  const [error, setError] = useState8(null);
@@ -711,10 +747,10 @@ function useRegister() {
711
747
  cancelledRef.current = true;
712
748
  setStatus("idle");
713
749
  }, []);
714
- const startRegister = useCallback7(() => {
750
+ const startSync = useCallback7(() => {
715
751
  cancelledRef.current = false;
716
752
  setError(null);
717
- setRegisteredModels([]);
753
+ setSyncedModels([]);
718
754
  const run = async () => {
719
755
  try {
720
756
  setStatus("discovering");
@@ -725,50 +761,48 @@ function useRegister() {
725
761
  setStatus("error");
726
762
  return;
727
763
  }
728
- const existingRegistered = await getRegisteredModels();
764
+ const existingSynced = await getSyncedModels();
729
765
  if (cancelledRef.current) return;
730
- const registeredNames = new Set(existingRegistered);
731
- const unregisteredModels = localModels.filter(
732
- (m) => !registeredNames.has(m.name)
766
+ const remoteByName = new Map(
767
+ existingSynced.map((m) => [m.name, m.id])
733
768
  );
734
- const allModels = localModels.map((m) => ({
735
- name: m.name,
736
- provider: m.provider,
737
- capability: m.capability,
738
- isNew: !registeredNames.has(m.name)
739
- }));
740
- if (unregisteredModels.length === 0) {
741
- setRegisteredModels(allModels);
742
- setProgress({ current: 0, total: 0 });
743
- setStatus("done");
744
- return;
745
- }
746
- setStatus("registering");
747
- setProgress({ current: 0, total: unregisteredModels.length });
748
- for (let i = 0; i < unregisteredModels.length; i++) {
769
+ setStatus("syncing");
770
+ setProgress({ current: 0, total: localModels.length });
771
+ for (let i = 0; i < localModels.length; i++) {
749
772
  if (cancelledRef.current) return;
750
- const model = unregisteredModels[i];
773
+ const model = localModels[i];
751
774
  const modelType = MODEL_TYPE_MAP[model.capability];
752
- await registerLocalModel({
753
- modelName: model.name,
754
- provider: model.provider,
755
- modelType,
756
- parameters: model.parameters
757
- });
758
- setProgress({ current: i + 1, total: unregisteredModels.length });
775
+ const existingId = remoteByName.get(model.name);
776
+ if (existingId) {
777
+ await updateLocalModel({
778
+ modelId: existingId,
779
+ modelName: model.name,
780
+ provider: model.provider,
781
+ modelType,
782
+ parameters: model.parameters
783
+ });
784
+ } else {
785
+ await syncLocalModel({
786
+ modelName: model.name,
787
+ provider: model.provider,
788
+ modelType,
789
+ parameters: model.parameters
790
+ });
791
+ }
792
+ setProgress({ current: i + 1, total: localModels.length });
759
793
  }
760
794
  if (cancelledRef.current) return;
761
795
  const finalModels = localModels.map((m) => ({
762
796
  name: m.name,
763
797
  provider: m.provider,
764
798
  capability: m.capability,
765
- isNew: !registeredNames.has(m.name)
799
+ isNew: !remoteByName.has(m.name)
766
800
  }));
767
- setRegisteredModels(finalModels);
801
+ setSyncedModels(finalModels);
768
802
  setStatus("done");
769
803
  } catch (err) {
770
804
  if (!cancelledRef.current) {
771
- setError(err instanceof Error ? err.message : "Registration failed");
805
+ setError(err instanceof Error ? err.message : "Sync failed");
772
806
  setStatus("error");
773
807
  }
774
808
  }
@@ -778,27 +812,27 @@ function useRegister() {
778
812
  return {
779
813
  status,
780
814
  progress,
781
- registeredModels,
815
+ syncedModels,
782
816
  error,
783
- startRegister,
817
+ startSync,
784
818
  cancel
785
819
  };
786
820
  }
787
821
 
788
822
  // src/tui/pages/RegisterPage.tsx
789
823
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
790
- function RegisterPage() {
791
- const { status, progress, registeredModels, error, startRegister, cancel } = useRegister();
824
+ function SyncPage() {
825
+ const { status, progress, syncedModels, error, startSync, cancel } = useSync();
792
826
  useEffect9(() => {
793
- startRegister();
827
+ startSync();
794
828
  return () => cancel();
795
829
  }, []);
796
830
  if (status === "idle") {
797
- return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Starting model registration..." }) }) });
831
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Starting model sync..." }) }) });
798
832
  }
799
833
  if (status === "error") {
800
834
  return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
801
- "Registration failed: ",
835
+ "Sync failed: ",
802
836
  error
803
837
  ] }) }) });
804
838
  }
@@ -808,12 +842,12 @@ function RegisterPage() {
808
842
  /* @__PURE__ */ jsx5(Text5, { children: " Discovering local models..." })
809
843
  ] }) });
810
844
  }
811
- if (status === "registering") {
845
+ if (status === "syncing") {
812
846
  return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
813
847
  /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: /* @__PURE__ */ jsx5(Spinner3, { type: "dots" }) }),
814
848
  /* @__PURE__ */ jsxs5(Text5, { children: [
815
849
  " ",
816
- "Registering ",
850
+ "Syncing ",
817
851
  progress.current,
818
852
  "/",
819
853
  progress.total,
@@ -821,12 +855,12 @@ function RegisterPage() {
821
855
  ] })
822
856
  ] }) });
823
857
  }
824
- const newModels = registeredModels.filter((m) => m.isNew);
825
- const existingModels = registeredModels.filter((m) => !m.isNew);
858
+ const newModels = syncedModels.filter((m) => m.isNew);
859
+ const resyncedModels = syncedModels.filter((m) => !m.isNew);
826
860
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: [
827
- newModels.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
861
+ newModels.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
828
862
  /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
829
- "Registered ",
863
+ "Synced ",
830
864
  newModels.length,
831
865
  " new model",
832
866
  newModels.length !== 1 ? "s" : "",
@@ -844,18 +878,23 @@ function RegisterPage() {
844
878
  "]"
845
879
  ] })
846
880
  ] }, m.name))
847
- ] }) : /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "green", children: "All models already registered." }) }),
848
- existingModels.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
849
- /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
850
- "Already registered (",
851
- existingModels.length,
852
- "):"
881
+ ] }),
882
+ resyncedModels.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
883
+ /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
884
+ "Resynced ",
885
+ resyncedModels.length,
886
+ " existing model",
887
+ resyncedModels.length !== 1 ? "s" : "",
888
+ ":"
853
889
  ] }),
854
- existingModels.map((m) => /* @__PURE__ */ jsxs5(Box5, { children: [
855
- /* @__PURE__ */ jsx5(Text5, { color: "gray", children: " \u25CF " }),
856
- /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
890
+ resyncedModels.map((m) => /* @__PURE__ */ jsxs5(Box5, { children: [
891
+ /* @__PURE__ */ jsx5(Text5, { color: "green", children: " \u2713 " }),
892
+ /* @__PURE__ */ jsxs5(Text5, { children: [
857
893
  m.name,
858
- " [",
894
+ " "
895
+ ] }),
896
+ /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
897
+ "[",
859
898
  m.provider,
860
899
  "]"
861
900
  ] })
@@ -984,7 +1023,7 @@ function ProviderDetailView({
984
1023
  borderRight: false,
985
1024
  borderColor: "gray",
986
1025
  children: [
987
- /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Actions" }) }),
1026
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "Actions" }) }),
988
1027
  /* @__PURE__ */ jsxs6(Box6, { children: [
989
1028
  /* @__PURE__ */ jsxs6(Text7, { color: "cyan", bold: true, children: [
990
1029
  "\u276F",
@@ -992,7 +1031,7 @@ function ProviderDetailView({
992
1031
  ] }),
993
1032
  /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " - Return to providers" })
994
1033
  ] }),
995
- /* @__PURE__ */ jsx7(Box6, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "gray", dimColor: true, wrap: "truncate-end", children: [
1034
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "gray", wrap: "truncate-end", children: [
996
1035
  "Up/Down Scroll ",
997
1036
  "\u2022",
998
1037
  " Enter/q/Esc Back",
@@ -1194,7 +1233,7 @@ function SetupPage({ onBack }) {
1194
1233
  }
1195
1234
  ) })
1196
1235
  ] }),
1197
- /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "gray", dimColor: true, children: [
1236
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "gray", children: [
1198
1237
  "Up/Down Navigate ",
1199
1238
  "\u2022",
1200
1239
  " Enter Select ",
@@ -1435,11 +1474,12 @@ function App({ runner }) {
1435
1474
  const { refresh: refreshProviders } = useProviders();
1436
1475
  const {
1437
1476
  models,
1477
+ warnings: modelWarnings,
1438
1478
  loading: modelsLoading,
1439
1479
  refresh: refreshModels
1440
1480
  } = useModels();
1441
1481
  const { requests } = useRequests();
1442
- const { registeredNames, refresh: refreshRegistered } = useRegisteredModels(connectionStatus);
1482
+ const { syncedNames, refresh: refreshSynced } = useSyncedModels(connectionStatus);
1443
1483
  const shouldOnboard = getApiKey() === void 0;
1444
1484
  const [page, setPage] = useState12(
1445
1485
  shouldOnboard ? "onboarding" : "dashboard"
@@ -1459,9 +1499,9 @@ function App({ runner }) {
1459
1499
  await Promise.all([
1460
1500
  refreshProviders(),
1461
1501
  refreshModels(),
1462
- refreshRegistered()
1502
+ refreshSynced()
1463
1503
  ]);
1464
- }, [refreshProviders, refreshModels, refreshRegistered]);
1504
+ }, [refreshProviders, refreshModels, refreshSynced]);
1465
1505
  const handleQuit = useCallback10(() => {
1466
1506
  runner.stop();
1467
1507
  exit();
@@ -1478,7 +1518,7 @@ function App({ runner }) {
1478
1518
  setPage("onboarding");
1479
1519
  break;
1480
1520
  case "register":
1481
- setPage("register");
1521
+ setPage("sync");
1482
1522
  break;
1483
1523
  case "setup":
1484
1524
  setPage("setup");
@@ -1491,7 +1531,7 @@ function App({ runner }) {
1491
1531
  break;
1492
1532
  }
1493
1533
  },
1494
- [refreshModels, refreshRegistered, refreshAll, handleQuit]
1534
+ [refreshModels, refreshSynced, refreshAll, handleQuit]
1495
1535
  );
1496
1536
  const subpageMenuItems = [
1497
1537
  { id: "back", label: "Back", description: "Return to dashboard" }
@@ -1522,13 +1562,14 @@ function App({ runner }) {
1522
1562
  {
1523
1563
  requests,
1524
1564
  models,
1525
- registeredNames,
1565
+ modelWarnings,
1566
+ syncedNames,
1526
1567
  modelsLoading,
1527
1568
  onNavigate: handleNavigate
1528
1569
  }
1529
1570
  ),
1530
1571
  page === "setup" && /* @__PURE__ */ jsx9(SetupPage, { onBack: () => setPage("dashboard") }),
1531
- page === "register" && /* @__PURE__ */ jsx9(RegisterPage, {}),
1572
+ page === "sync" && /* @__PURE__ */ jsx9(SyncPage, {}),
1532
1573
  page !== "dashboard" && page !== "setup" && /* @__PURE__ */ jsx9(Box8, { flexGrow: 1 }),
1533
1574
  page !== "dashboard" && page !== "setup" && /* @__PURE__ */ jsx9(
1534
1575
  NavigationMenu,
@@ -1540,13 +1581,129 @@ function App({ runner }) {
1540
1581
  ] }) });
1541
1582
  }
1542
1583
 
1584
+ // src/update.ts
1585
+ import { createRequire as createRequire2 } from "module";
1586
+ var require3 = createRequire2(import.meta.url);
1587
+ var pkg2 = require3("../package.json");
1588
+ function getCurrentVersion() {
1589
+ return pkg2.version;
1590
+ }
1591
+ async function fetchLatestVersion() {
1592
+ try {
1593
+ const res = await fetch(
1594
+ "https://registry.npmjs.org/@mindstudio-ai/local-model-tunnel/latest",
1595
+ { signal: AbortSignal.timeout(5e3) }
1596
+ );
1597
+ if (!res.ok) return null;
1598
+ const data = await res.json();
1599
+ return data.version ?? null;
1600
+ } catch {
1601
+ return null;
1602
+ }
1603
+ }
1604
+ function isNewerVersion(current, latest) {
1605
+ const currentParts = current.split(".").map(Number);
1606
+ const latestParts = latest.split(".").map(Number);
1607
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
1608
+ const c = currentParts[i] ?? 0;
1609
+ const l = latestParts[i] ?? 0;
1610
+ if (l > c) return true;
1611
+ if (l < c) return false;
1612
+ }
1613
+ return false;
1614
+ }
1615
+ async function checkForUpdate() {
1616
+ const currentVersion = getCurrentVersion();
1617
+ const latestVersion = await fetchLatestVersion();
1618
+ if (!latestVersion) return null;
1619
+ if (!isNewerVersion(currentVersion, latestVersion)) return null;
1620
+ return { currentVersion, latestVersion };
1621
+ }
1622
+
1623
+ // src/tui/components/UpdatePrompt.tsx
1624
+ import { Box as Box9, Text as Text9, useInput as useInput4 } from "ink";
1625
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1626
+ function UpdatePrompt({
1627
+ currentVersion,
1628
+ latestVersion,
1629
+ onChoice
1630
+ }) {
1631
+ useInput4((input) => {
1632
+ if (input.toLowerCase() === "y") {
1633
+ onChoice(true);
1634
+ } else {
1635
+ onChoice(false);
1636
+ }
1637
+ });
1638
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", paddingY: 1, paddingX: 2, children: [
1639
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1640
+ /* @__PURE__ */ jsx10(Text9, { color: "yellow", bold: true, children: "Update available:" }),
1641
+ /* @__PURE__ */ jsxs9(Text9, { children: [
1642
+ " ",
1643
+ "v",
1644
+ currentVersion,
1645
+ " ",
1646
+ "\u2192",
1647
+ " v",
1648
+ latestVersion
1649
+ ] })
1650
+ ] }),
1651
+ /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
1652
+ "Press ",
1653
+ /* @__PURE__ */ jsx10(Text9, { bold: true, color: "cyan", children: "y" }),
1654
+ " to update, any other key to skip"
1655
+ ] }) })
1656
+ ] });
1657
+ }
1658
+
1543
1659
  // src/tui/index.tsx
1544
- import { jsx as jsx10 } from "react/jsx-runtime";
1660
+ import { jsx as jsx11 } from "react/jsx-runtime";
1661
+ async function promptForUpdate(currentVersion, latestVersion) {
1662
+ return new Promise((resolve) => {
1663
+ const { unmount } = render(
1664
+ /* @__PURE__ */ jsx11(
1665
+ UpdatePrompt,
1666
+ {
1667
+ currentVersion,
1668
+ latestVersion,
1669
+ onChoice: (shouldUpdate) => {
1670
+ unmount();
1671
+ resolve(shouldUpdate);
1672
+ }
1673
+ }
1674
+ ),
1675
+ { exitOnCtrlC: true }
1676
+ );
1677
+ });
1678
+ }
1545
1679
  async function startTUI() {
1546
1680
  console.clear();
1681
+ const update = await checkForUpdate();
1682
+ if (update) {
1683
+ const shouldUpdate = await promptForUpdate(
1684
+ update.currentVersion,
1685
+ update.latestVersion
1686
+ );
1687
+ if (shouldUpdate) {
1688
+ console.log("\nUpdating to v" + update.latestVersion + "...\n");
1689
+ try {
1690
+ execSync("npm install -g @mindstudio-ai/local-model-tunnel@latest", {
1691
+ stdio: "inherit"
1692
+ });
1693
+ console.log("\nRestarting...\n");
1694
+ execFileSync(process.execPath, process.argv.slice(1), {
1695
+ stdio: "inherit"
1696
+ });
1697
+ } catch {
1698
+ console.error("\nUpdate failed. Continuing with current version.\n");
1699
+ }
1700
+ return;
1701
+ }
1702
+ console.clear();
1703
+ }
1547
1704
  const runner = new TunnelRunner();
1548
1705
  const { waitUntilExit } = render(
1549
- /* @__PURE__ */ jsx10(App, { runner }),
1706
+ /* @__PURE__ */ jsx11(App, { runner }),
1550
1707
  {
1551
1708
  exitOnCtrlC: true
1552
1709
  }
@@ -1557,4 +1714,4 @@ async function startTUI() {
1557
1714
  export {
1558
1715
  startTUI
1559
1716
  };
1560
- //# sourceMappingURL=tui-QOSKXZWU.js.map
1717
+ //# sourceMappingURL=tui-YFUZJIGF.js.map