amalgm 0.1.135 → 0.1.136

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/runtime/lib/harnesses.js +61 -162
  3. package/runtime/scripts/amalgm-mcp/agent-bundles/app-entries.js +124 -0
  4. package/runtime/scripts/amalgm-mcp/agent-bundles/app-files.js +160 -0
  5. package/runtime/scripts/amalgm-mcp/agent-bundles/automation-entries.js +186 -0
  6. package/runtime/scripts/amalgm-mcp/agent-bundles/bundles.js +272 -32
  7. package/runtime/scripts/amalgm-mcp/agent-bundles/rest.js +25 -8
  8. package/runtime/scripts/amalgm-mcp/automations/rest.js +21 -11
  9. package/runtime/scripts/amalgm-mcp/automations/store.js +68 -1
  10. package/runtime/scripts/amalgm-mcp/automations/tools.js +13 -4
  11. package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +1 -0
  12. package/runtime/scripts/amalgm-mcp/lib/email-deeplinks.js +117 -0
  13. package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +42 -19
  14. package/runtime/scripts/amalgm-mcp/lib/email-md.js +24 -17
  15. package/runtime/scripts/amalgm-mcp/notify/index.js +6 -2
  16. package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
  17. package/runtime/scripts/amalgm-mcp/tests/agent-bundles.test.js +8 -8
  18. package/runtime/scripts/amalgm-mcp/tests/bundle-entries.test.js +288 -0
  19. package/runtime/scripts/amalgm-mcp/tests/desktop-release-automation-seed.test.js +37 -1
  20. package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +22 -0
  21. package/runtime/scripts/amalgm-mcp/tests/desktop-release-server.test.js +15 -5
  22. package/runtime/scripts/amalgm-mcp/tests/email-embeds.test.js +35 -0
  23. package/runtime/scripts/amalgm-mcp/workspace/rest.js +1 -0
  24. package/runtime/scripts/chat-core/adapters/acp-client.js +156 -0
  25. package/runtime/scripts/chat-core/adapters/cursor.js +254 -0
  26. package/runtime/scripts/chat-core/adapters/pi.js +302 -0
  27. package/runtime/scripts/chat-core/auth.js +20 -3
  28. package/runtime/scripts/chat-core/chat-payload.js +2 -0
  29. package/runtime/scripts/chat-core/contract.js +56 -1
  30. package/runtime/scripts/chat-core/normalizers/cursor.js +174 -0
  31. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +38 -0
  32. package/runtime/scripts/chat-core/normalizers/pi.js +143 -0
  33. package/runtime/scripts/chat-core/server.js +4 -0
  34. package/runtime/scripts/chat-core/tests/cursor.test.js +178 -0
  35. package/runtime/scripts/chat-core/tests/pi.test.js +182 -0
  36. package/runtime/scripts/chat-core/tooling/native-config.js +38 -0
  37. package/runtime/scripts/chat-core/usage.js +20 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.135",
3
+ "version": "0.1.136",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -60,7 +60,7 @@ var DEFAULT_MODEL_IDS = {
60
60
  opencode: "opencode-deepseek-deepseek-v4-pro",
61
61
  pi: "pi-anthropic-claude-sonnet-4.6",
62
62
  amp: "amp-default",
63
- cursor: "cursor/composer-2-fast"
63
+ cursor: "cursor/composer-2.5"
64
64
  };
65
65
  var CLAUDE_OPUS_4_8_1M_MODEL_ID = "anthropic/claude-opus-4.8-1m";
66
66
  var CLAUDE_LEGACY_ALIASES = {
@@ -291,14 +291,7 @@ function cursorCanonicalModelIdToCliModel(value) {
291
291
  return canonical.slice("xai/".length).replace(/^grok-4\.20/, "grok-4-20");
292
292
  }
293
293
  if (canonical.startsWith("anthropic/")) {
294
- const model = canonical.slice("anthropic/".length);
295
- const opus47Match = model.match(/^claude-opus-4\.7(?:-(.+))?$/);
296
- if (opus47Match) return `claude-opus-4-7${opus47Match[1] ? `-${opus47Match[1]}` : ""}`;
297
- const claudeMatch = model.match(/^claude-(opus|sonnet)-(4(?:\.\d+)?)(?:-(.+))?$/);
298
- if (claudeMatch) {
299
- const [, family, version, suffix] = claudeMatch;
300
- return `claude-${version}-${family}${suffix ? `-${suffix}` : ""}`;
301
- }
294
+ return canonical.slice("anthropic/".length).replace(/-(\d+)\.(\d+)(?=$|-)/, "-$1-$2");
302
295
  }
303
296
  return canonical;
304
297
  }
@@ -541,16 +534,30 @@ var OPENCODE_MODELS = [
541
534
  providerGroup: "mistral"
542
535
  }
543
536
  ];
544
- var PI_MODELS = [
545
- {
546
- id: "pi-anthropic-claude-sonnet-4.6",
547
- name: "Claude Sonnet 4.6",
548
- description: "Anthropic via AI Gateway",
549
- cliModel: "anthropic/claude-sonnet-4.6",
550
- provider: "pi",
551
- providerGroup: "anthropic"
552
- }
537
+ var PI_MODEL_ROWS = [
538
+ ["anthropic/claude-opus-4.6", "Claude Opus 4.6", "Most capable Anthropic model", "anthropic"],
539
+ ["anthropic/claude-sonnet-4.6", "Claude Sonnet 4.6", "Balanced Anthropic model", "anthropic"],
540
+ ["anthropic/claude-haiku-4.5", "Claude Haiku 4.5", "Fast Anthropic model", "anthropic"],
541
+ ["openai/gpt-5.4", "GPT-5.4", "Flagship OpenAI model", "openai"],
542
+ ["openai/gpt-5.3-codex", "GPT-5.3 Codex", "OpenAI coding model", "openai"],
543
+ ["google/gemini-3.1-pro-preview", "Gemini 3.1 Pro", "Latest Google flagship", "google"],
544
+ ["google/gemini-3-flash", "Gemini 3 Flash", "Fast Google model", "google"],
545
+ ["xai/grok-4", "Grok 4", "xAI flagship model", "xai"],
546
+ ["deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "DeepSeek flagship model", "deepseek"],
547
+ ["deepseek/deepseek-v3.2", "DeepSeek V3.2", "Latest DeepSeek model", "deepseek"],
548
+ ["mistral/devstral-2", "Devstral 2", "Mistral coding model", "mistral"]
553
549
  ];
550
+ var PI_MODELS = PI_MODEL_ROWS.map(
551
+ ([gatewayId, name, description, providerGroup]) => ({
552
+ id: `pi-${gatewayId.replace(/\//g, "-")}`,
553
+ name,
554
+ description,
555
+ gatewayModelId: gatewayId,
556
+ cliModel: buildGatewayCliModel(gatewayId, "pi"),
557
+ provider: "pi",
558
+ providerGroup
559
+ })
560
+ );
554
561
  var AMP_MODELS = [
555
562
  {
556
563
  id: DEFAULT_MODEL_IDS.amp,
@@ -563,101 +570,37 @@ var AMP_MODELS = [
563
570
  ];
564
571
  var CURSOR_MODEL_ROWS = [
565
572
  ["auto", "Auto", "cursor"],
566
- ["composer-2-fast", "Composer 2 Fast", "cursor"],
567
- ["composer-2", "Composer 2", "cursor"],
568
- ["composer-1.5", "Composer 1.5", "cursor"],
569
- ["gpt-5.3-codex-low", "Codex 5.3 Low", "openai"],
570
- ["gpt-5.3-codex-low-fast", "Codex 5.3 Low Fast", "openai"],
573
+ ["composer-2.5", "Composer 2.5", "cursor"],
574
+ ["claude-fable-5", "Fable 5 Thinking", "anthropic"],
575
+ ["claude-opus-4-8", "Opus 4.8 Thinking", "anthropic"],
576
+ ["claude-opus-4-7", "Opus 4.7 Thinking", "anthropic"],
577
+ ["claude-opus-4-6", "Opus 4.6 Thinking", "anthropic"],
578
+ ["claude-opus-4-5", "Opus 4.5 Thinking", "anthropic"],
579
+ ["claude-sonnet-5", "Sonnet 5 Thinking", "anthropic"],
580
+ ["claude-sonnet-4-6", "Sonnet 4.6 Thinking", "anthropic"],
581
+ ["claude-sonnet-4-5", "Sonnet 4.5 Thinking", "anthropic"],
582
+ ["claude-sonnet-4", "Sonnet 4", "anthropic"],
583
+ ["claude-haiku-4-5", "Haiku 4.5 Thinking", "anthropic"],
584
+ ["gpt-5.5", "GPT-5.5", "openai"],
585
+ ["gpt-5.4", "GPT-5.4", "openai"],
586
+ ["gpt-5.4-mini", "GPT-5.4 Mini", "openai"],
587
+ ["gpt-5.4-nano", "GPT-5.4 Nano", "openai"],
571
588
  ["gpt-5.3-codex", "Codex 5.3", "openai"],
572
- ["gpt-5.3-codex-fast", "Codex 5.3 Fast", "openai"],
573
- ["gpt-5.3-codex-high", "Codex 5.3 High", "openai"],
574
- ["gpt-5.3-codex-high-fast", "Codex 5.3 High Fast", "openai"],
575
- ["gpt-5.3-codex-xhigh", "Codex 5.3 Extra High", "openai"],
576
- ["gpt-5.3-codex-xhigh-fast", "Codex 5.3 Extra High Fast", "openai"],
577
- ["gpt-5.2", "GPT-5.2", "openai"],
578
- ["gpt-5.3-codex-spark-preview-low", "Codex 5.3 Spark Low", "openai"],
579
- ["gpt-5.3-codex-spark-preview", "Codex 5.3 Spark", "openai"],
580
- ["gpt-5.3-codex-spark-preview-high", "Codex 5.3 Spark High", "openai"],
581
- ["gpt-5.3-codex-spark-preview-xhigh", "Codex 5.3 Spark Extra High", "openai"],
582
- ["gpt-5.2-codex-low", "Codex 5.2 Low", "openai"],
583
- ["gpt-5.2-codex-low-fast", "Codex 5.2 Low Fast", "openai"],
584
589
  ["gpt-5.2-codex", "Codex 5.2", "openai"],
585
- ["gpt-5.2-codex-fast", "Codex 5.2 Fast", "openai"],
586
- ["gpt-5.2-codex-high", "Codex 5.2 High", "openai"],
587
- ["gpt-5.2-codex-high-fast", "Codex 5.2 High Fast", "openai"],
588
- ["gpt-5.2-codex-xhigh", "Codex 5.2 Extra High", "openai"],
589
- ["gpt-5.2-codex-xhigh-fast", "Codex 5.2 Extra High Fast", "openai"],
590
- ["gpt-5.1-codex-max-low", "Codex 5.1 Max Low", "openai"],
591
- ["gpt-5.1-codex-max-low-fast", "Codex 5.1 Max Low Fast", "openai"],
592
- ["gpt-5.1-codex-max-medium", "Codex 5.1 Max", "openai"],
593
- ["gpt-5.1-codex-max-medium-fast", "Codex 5.1 Max Medium Fast", "openai"],
594
- ["gpt-5.1-codex-max-high", "Codex 5.1 Max High", "openai"],
595
- ["gpt-5.1-codex-max-high-fast", "Codex 5.1 Max High Fast", "openai"],
596
- ["gpt-5.1-codex-max-xhigh", "Codex 5.1 Max Extra High", "openai"],
597
- ["gpt-5.1-codex-max-xhigh-fast", "Codex 5.1 Max Extra High Fast", "openai"],
598
- ["gpt-5.5-medium", "GPT-5.5 1M", "openai"],
599
- ["claude-opus-4-7-thinking-high", "Opus 4.7 1M High Thinking", "anthropic"],
600
- ["gpt-5.4-high", "GPT-5.4 1M High", "openai"],
601
- ["gpt-5.4-high-fast", "GPT-5.4 High Fast", "openai"],
602
- ["claude-4.6-opus-high-thinking-fast", "Opus 4.6 1M Thinking Fast", "anthropic"],
603
- ["gpt-5.5-high", "GPT-5.5 1M High", "openai"],
604
- ["gpt-5.5-extra-high", "GPT-5.5 1M Extra High", "openai"],
605
- ["claude-4.6-sonnet-medium", "Sonnet 4.6 1M", "anthropic"],
606
- ["claude-4.6-sonnet-medium-thinking", "Sonnet 4.6 1M Thinking", "anthropic"],
607
- ["claude-opus-4-7-low", "Opus 4.7 1M Low", "anthropic"],
608
- ["claude-opus-4-7-medium", "Opus 4.7 1M Medium", "anthropic"],
609
- ["claude-opus-4-7-high", "Opus 4.7 1M High", "anthropic"],
610
- ["claude-opus-4-7-xhigh", "Opus 4.7 1M", "anthropic"],
611
- ["claude-opus-4-7-max", "Opus 4.7 1M Max", "anthropic"],
612
- ["claude-opus-4-7-thinking-low", "Opus 4.7 1M Low Thinking", "anthropic"],
613
- ["claude-opus-4-7-thinking-medium", "Opus 4.7 1M Medium Thinking", "anthropic"],
614
- ["claude-opus-4-7-thinking-xhigh", "Opus 4.7 1M Thinking", "anthropic"],
615
- ["claude-opus-4-7-thinking-max", "Opus 4.7 1M Max Thinking", "anthropic"],
616
- ["gpt-5.4-low", "GPT-5.4 1M Low", "openai"],
617
- ["gpt-5.4-medium", "GPT-5.4 1M", "openai"],
618
- ["gpt-5.4-medium-fast", "GPT-5.4 Fast", "openai"],
619
- ["gpt-5.4-xhigh", "GPT-5.4 1M Extra High", "openai"],
620
- ["gpt-5.4-xhigh-fast", "GPT-5.4 Extra High Fast", "openai"],
621
- ["claude-4.6-opus-high", "Opus 4.6 1M", "anthropic"],
622
- ["claude-4.6-opus-max", "Opus 4.6 1M Max", "anthropic"],
623
- ["claude-4.6-opus-high-thinking", "Opus 4.6 1M Thinking", "anthropic"],
624
- ["claude-4.6-opus-max-thinking", "Opus 4.6 1M Max Thinking", "anthropic"],
625
- ["claude-4.6-opus-max-thinking-fast", "Opus 4.6 1M Max Thinking Fast", "anthropic"],
626
- ["claude-4.5-opus-high", "Opus 4.5", "anthropic"],
627
- ["claude-4.5-opus-high-thinking", "Opus 4.5 Thinking", "anthropic"],
628
- ["gpt-5.2-low", "GPT-5.2 Low", "openai"],
629
- ["gpt-5.2-low-fast", "GPT-5.2 Low Fast", "openai"],
630
- ["gpt-5.2-fast", "GPT-5.2 Fast", "openai"],
631
- ["gpt-5.2-high", "GPT-5.2 High", "openai"],
632
- ["gpt-5.2-high-fast", "GPT-5.2 High Fast", "openai"],
633
- ["gpt-5.2-xhigh", "GPT-5.2 Extra High", "openai"],
634
- ["gpt-5.2-xhigh-fast", "GPT-5.2 Extra High Fast", "openai"],
635
- ["gemini-3.1-pro", "Gemini 3.1 Pro", "google"],
636
- ["gpt-5.4-mini-none", "GPT-5.4 Mini None", "openai"],
637
- ["gpt-5.4-mini-low", "GPT-5.4 Mini Low", "openai"],
638
- ["gpt-5.4-mini-medium", "GPT-5.4 Mini", "openai"],
639
- ["gpt-5.4-mini-high", "GPT-5.4 Mini High", "openai"],
640
- ["gpt-5.4-mini-xhigh", "GPT-5.4 Mini Extra High", "openai"],
641
- ["gpt-5.4-nano-none", "GPT-5.4 Nano None", "openai"],
642
- ["gpt-5.4-nano-low", "GPT-5.4 Nano Low", "openai"],
643
- ["gpt-5.4-nano-medium", "GPT-5.4 Nano", "openai"],
644
- ["gpt-5.4-nano-high", "GPT-5.4 Nano High", "openai"],
645
- ["gpt-5.4-nano-xhigh", "GPT-5.4 Nano Extra High", "openai"],
646
- ["grok-4-20", "Grok 4.20", "xai"],
647
- ["grok-4-20-thinking", "Grok 4.20 Thinking", "xai"],
648
- ["claude-4.5-sonnet", "Sonnet 4.5", "anthropic"],
649
- ["claude-4.5-sonnet-thinking", "Sonnet 4.5 Thinking", "anthropic"],
650
- ["gpt-5.1-low", "GPT-5.1 Low", "openai"],
651
- ["gpt-5.1", "GPT-5.1", "openai"],
652
- ["gpt-5.1-high", "GPT-5.1 High", "openai"],
653
- ["gemini-3-flash", "Gemini 3 Flash", "google"],
654
- ["gpt-5.1-codex-mini-low", "Codex 5.1 Mini Low", "openai"],
590
+ ["gpt-5.2", "GPT-5.2", "openai"],
591
+ ["gpt-5.1-codex-max", "Codex 5.1 Max", "openai"],
655
592
  ["gpt-5.1-codex-mini", "Codex 5.1 Mini", "openai"],
656
- ["gpt-5.1-codex-mini-high", "Codex 5.1 Mini High", "openai"],
657
- ["claude-4-sonnet", "Sonnet 4", "anthropic"],
658
- ["claude-4-sonnet-thinking", "Sonnet 4 Thinking", "anthropic"],
593
+ ["gpt-5.1", "GPT-5.1", "openai"],
659
594
  ["gpt-5-mini", "GPT-5 Mini", "openai"],
660
- ["kimi-k2.5", "Kimi K2.5", "moonshotai"]
595
+ ["gemini-3.5-flash", "Gemini 3.5 Flash", "google"],
596
+ ["gemini-3.1-pro", "Gemini 3.1 Pro", "google"],
597
+ ["gemini-3-flash", "Gemini 3 Flash", "google"],
598
+ ["gemini-2.5-flash", "Gemini 2.5 Flash", "google"],
599
+ ["grok-4.3", "Grok 4.3", "xai"],
600
+ ["grok-build-0.1", "Grok Build 0.1", "xai"],
601
+ ["kimi-k2.7-code", "Kimi K2.7 Code", "moonshotai"],
602
+ ["kimi-k2.5", "Kimi K2.5", "moonshotai"],
603
+ ["glm-5.2", "GLM-5.2", "zai"]
661
604
  ];
662
605
  function cursorModelDescription(providerGroup) {
663
606
  switch (providerGroup) {
@@ -673,47 +616,26 @@ function cursorModelDescription(providerGroup) {
673
616
  return "xAI model through Cursor Agent";
674
617
  case "moonshotai":
675
618
  return "Moonshot model through Cursor Agent";
619
+ case "zai":
620
+ return "Z.ai model through Cursor Agent";
676
621
  default:
677
622
  return "Model through Cursor Agent";
678
623
  }
679
624
  }
680
- function cursorModelPricing(cliModel) {
681
- switch (cliModel) {
682
- case "auto":
683
- return { inputPerMTok: 1.25, outputPerMTok: 6, cacheReadPerMTok: 0.25, cacheWritePerMTok: 0 };
684
- case "composer-2-fast":
685
- return { inputPerMTok: 1.5, outputPerMTok: 7.5, cacheReadPerMTok: 0.35, cacheWritePerMTok: 0 };
686
- case "composer-2":
687
- return { inputPerMTok: 0.5, outputPerMTok: 2.5, cacheReadPerMTok: 0.2, cacheWritePerMTok: 0 };
688
- case "composer-1.5":
689
- return { inputPerMTok: 3.5, outputPerMTok: 17.5, cacheReadPerMTok: 0.35, cacheWritePerMTok: 0 };
690
- default:
691
- return void 0;
692
- }
693
- }
694
625
  var CURSOR_MODELS = CURSOR_MODEL_ROWS.map(
695
626
  ([cliModel, name, providerGroup]) => {
696
627
  const id = cursorCliModelToModelId(cliModel);
697
- const pricing = cursorModelPricing(cliModel);
698
628
  return {
699
629
  id,
700
630
  name,
701
631
  description: cursorModelDescription(providerGroup),
702
632
  gatewayModelId: modelIdToGatewayModelId(id, "cursor") ?? id,
703
- ...pricing ? { pricing } : {},
704
633
  cliModel,
705
634
  provider: "cursor",
706
635
  providerGroup
707
636
  };
708
637
  }
709
638
  );
710
- var THINKING_LEVELS = ["xhigh", "high", "medium", "low"];
711
- var THINKING_LEVEL_LABELS = {
712
- low: "Low",
713
- medium: "Medium",
714
- high: "High",
715
- xhigh: "xHigh"
716
- };
717
639
  function codexModel(id, name, description, reasoningEffort, gatewayModelId = id.replace(/(?::thinking-|-thinking-)(low|medium|high|xhigh)$/i, ""), providerGroup = id.includes("gpt-5.5") ? "gpt-5.5" : "gpt-5.4") {
718
640
  return {
719
641
  id,
@@ -726,23 +648,7 @@ function codexModel(id, name, description, reasoningEffort, gatewayModelId = id.
726
648
  ...reasoningEffort ? { reasoningEffort } : {}
727
649
  };
728
650
  }
729
- function codexThinkingModels(baseId, baseName, description) {
730
- return THINKING_LEVELS.map(
731
- (level) => codexModel(
732
- `${baseId}-thinking-${level}`,
733
- `${baseName} Thinking ${THINKING_LEVEL_LABELS[level]}`,
734
- `${description} with ${THINKING_LEVEL_LABELS[level]} thinking`,
735
- level,
736
- baseId
737
- )
738
- );
739
- }
740
651
  var CODEX_MODELS = [
741
- ...codexThinkingModels(
742
- "openai/gpt-5.5",
743
- "GPT-5.5",
744
- "Latest frontier model for coding and agentic work"
745
- ),
746
652
  codexModel(
747
653
  "openai/gpt-5.5",
748
654
  "GPT-5.5",
@@ -753,11 +659,6 @@ var CODEX_MODELS = [
753
659
  "GPT-5.5 Pro",
754
660
  "Highest-accuracy GPT-5.5 variant for difficult work"
755
661
  ),
756
- ...codexThinkingModels(
757
- "openai/gpt-5.4",
758
- "GPT-5.4",
759
- "Frontier coding model"
760
- ),
761
662
  codexModel(
762
663
  "openai/gpt-5.4",
763
664
  "GPT-5.4",
@@ -851,9 +752,8 @@ var VISIBLE_HARNESS_IDS = [
851
752
  "claude_code",
852
753
  "codex",
853
754
  "opencode",
854
- "pi",
855
- "amp",
856
- "cursor"
755
+ "cursor",
756
+ "pi"
857
757
  ];
858
758
  var DISABLED_HARNESS_IDS = /* @__PURE__ */ new Set([]);
859
759
  var dynamicModelCache = /* @__PURE__ */ new Map();
@@ -896,13 +796,11 @@ function isHarnessId(value) {
896
796
  function resolvePreferredHarnessModel({
897
797
  explicitHarness,
898
798
  explicitModel,
899
- pinnedHarness,
900
- pinnedModel,
901
799
  lastUsedHarness,
902
800
  selectedModels
903
801
  }) {
904
- const harness = (isHarnessId(explicitHarness) ? explicitHarness : null) || (isHarnessId(pinnedHarness) ? pinnedHarness : null) || (isHarnessId(lastUsedHarness) ? lastUsedHarness : null) || "claude_code";
905
- const rawModel = explicitModel || (pinnedHarness === harness ? pinnedModel : null) || selectedModels?.[harness] || getDefaultModel(harness);
802
+ const harness = (isHarnessId(explicitHarness) ? explicitHarness : null) || (isHarnessId(lastUsedHarness) ? lastUsedHarness : null) || "claude_code";
803
+ const rawModel = explicitModel || selectedModels?.[harness] || getDefaultModel(harness);
906
804
  const model = normalizeModelId(rawModel, harness) ?? rawModel;
907
805
  return { harness, model };
908
806
  }
@@ -942,11 +840,12 @@ function coerceAuthMethodForHarness(harnessId, authMethod) {
942
840
  function resolveHarnessModel(harnessId, modelId) {
943
841
  const selection = normalizeModelSelection(modelId ?? getDefaultModel(harnessId), harnessId);
944
842
  const normalizedModelId = selection?.modelId ?? getDefaultModel(harnessId);
945
- const model = getModelById(normalizedModelId);
843
+ const localModel = HARNESSES[harnessId]?.models.find((m) => m.id === normalizedModelId);
844
+ const model = localModel ?? getModelById(normalizedModelId);
946
845
  const adaptedModel = harnessId === "claude_code" || harnessId === "codex" || harnessId === "cursor" ? modelIdToHarnessModel(normalizedModelId, harnessId) : null;
947
846
  return {
948
847
  modelId: normalizedModelId,
949
- cliModel: model?.cliModel ?? adaptedModel,
848
+ cliModel: localModel?.cliModel ?? adaptedModel ?? model?.cliModel ?? null,
950
849
  reasoningEffort: selection?.reasoningEffort ?? model?.reasoningEffort ?? (harnessId === "codex" ? "xhigh" : void 0)
951
850
  };
952
851
  }
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Portable app bundle entries.
5
+ *
6
+ * An exported entry carries the app's registration record (commands, lifecycle
7
+ * flags) plus its packed file tree. Machine-bound state — cwd, port, appRef,
8
+ * public URL, pid/status — stays behind: install writes the files into a fresh
9
+ * local directory and registers through the normal supervisor path, which
10
+ * allocates a new port and DNS ref, runs the build, and starts the process.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const { getApp } = require('../apps/store');
17
+ const { registerApp } = require('../apps/supervisor');
18
+ const { packAppFiles, unpackAppFiles } = require('./app-files');
19
+
20
+ function isObject(value) {
21
+ return !!value && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+
24
+ function cleanString(value) {
25
+ return typeof value === 'string' ? value.trim() : '';
26
+ }
27
+
28
+ function slugify(value) {
29
+ return String(value || '')
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9._-]+/g, '-')
32
+ .replace(/^-+|-+$/g, '')
33
+ .slice(0, 60) || 'app';
34
+ }
35
+
36
+ function exportAppEntry(appId, options = {}) {
37
+ const app = getApp(appId);
38
+ if (!app) throw new Error(`App not found: ${appId}`);
39
+ if (!cleanString(app.startCommand)) {
40
+ throw new Error(`App has no start command and cannot be shared: ${appId}`);
41
+ }
42
+ if (!app.cwd) throw new Error(`App has no working directory: ${appId}`);
43
+
44
+ const packed = packAppFiles(app.cwd, options);
45
+ if (packed.files.length === 0) {
46
+ throw new Error(`App directory has no shareable files: ${app.cwd}`);
47
+ }
48
+
49
+ return {
50
+ entry: {
51
+ sourceAppId: app.id,
52
+ app: {
53
+ name: app.name || 'App',
54
+ description: app.description || '',
55
+ buildCommand: app.buildCommand || null,
56
+ startCommand: app.startCommand,
57
+ autostart: app.autostart !== false,
58
+ keepAlive: app.keepAlive !== false,
59
+ },
60
+ files: packed.files,
61
+ totalBytes: packed.totalBytes,
62
+ skipped: packed.skipped,
63
+ },
64
+ skipped: packed.skipped,
65
+ };
66
+ }
67
+
68
+ function assertInstallableEntry(entry) {
69
+ if (!isObject(entry)) throw new Error('app entry must be an object');
70
+ if (!isObject(entry.app)) throw new Error('app entry is missing its app record');
71
+ if (!cleanString(entry.app.startCommand)) {
72
+ throw new Error('app entry must include a start command');
73
+ }
74
+ if (!Array.isArray(entry.files) || entry.files.length === 0) {
75
+ throw new Error('app entry must include files');
76
+ }
77
+ }
78
+
79
+ function allocateInstallDir(rootDir, name) {
80
+ const base = slugify(name);
81
+ fs.mkdirSync(rootDir, { recursive: true });
82
+ for (let index = 0; index < 1000; index += 1) {
83
+ const candidate = path.join(rootDir, index === 0 ? base : `${base}-${index + 1}`);
84
+ if (!fs.existsSync(candidate)) return candidate;
85
+ }
86
+ throw new Error(`Could not allocate an install directory for app: ${name}`);
87
+ }
88
+
89
+ /**
90
+ * Write the entry's files into a fresh directory and return the registration
91
+ * input for the app supervisor. Separated from installAppEntry so the file
92
+ * step can be exercised without spawning processes.
93
+ */
94
+ function materializeAppEntry(entry, options = {}) {
95
+ assertInstallableEntry(entry);
96
+ if (!cleanString(options.rootDir)) throw new Error('rootDir is required');
97
+ const targetDir = allocateInstallDir(path.resolve(options.rootDir), entry.app.name);
98
+ unpackAppFiles(entry.files, targetDir);
99
+ return {
100
+ name: entry.app.name || 'App',
101
+ description: entry.app.description || '',
102
+ cwd: targetDir,
103
+ build_command: entry.app.buildCommand || null,
104
+ start_command: entry.app.startCommand,
105
+ autostart: entry.app.autostart !== false,
106
+ keep_alive: entry.app.keepAlive !== false,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Install a portable app entry: materialize files, then register — which
112
+ * builds, starts the process, and connects a fresh appRef to app DNS.
113
+ */
114
+ async function installAppEntry(entry, options = {}) {
115
+ const register = options.register || registerApp;
116
+ const input = materializeAppEntry(entry, options);
117
+ return register(input);
118
+ }
119
+
120
+ module.exports = {
121
+ exportAppEntry,
122
+ installAppEntry,
123
+ materializeAppEntry,
124
+ };
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pack and unpack an app's file tree for portable bundles.
5
+ *
6
+ * Files travel inline in the bundle JSON (utf8 or base64), so packing is
7
+ * conservative: dependency dirs, VCS state, env files, and build outputs are
8
+ * excluded — the recipient's install re-runs the app's build command — and
9
+ * per-file/total size caps keep the bundle under the storage object limit.
10
+ * Everything skipped is reported so the share UI can surface it.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024;
17
+ const DEFAULT_MAX_TOTAL_BYTES = 12 * 1024 * 1024;
18
+
19
+ const EXCLUDED_DIR_NAMES = new Set([
20
+ 'node_modules',
21
+ '.git',
22
+ '.hg',
23
+ '.svn',
24
+ '.amalgm',
25
+ '.next',
26
+ '.nuxt',
27
+ '.turbo',
28
+ '.vercel',
29
+ '.cache',
30
+ '.parcel-cache',
31
+ '.venv',
32
+ 'venv',
33
+ '__pycache__',
34
+ 'dist',
35
+ 'build',
36
+ 'out',
37
+ 'coverage',
38
+ 'tmp',
39
+ ]);
40
+
41
+ const EXCLUDED_FILE_NAMES = new Set([
42
+ '.DS_Store',
43
+ 'Thumbs.db',
44
+ 'npm-debug.log',
45
+ 'yarn-error.log',
46
+ ]);
47
+
48
+ function isExcludedFileName(name) {
49
+ if (EXCLUDED_FILE_NAMES.has(name)) return true;
50
+ // .env, .env.local, .env.production, … — never ship credentials.
51
+ if (name === '.env' || name.startsWith('.env.')) return true;
52
+ if (name.endsWith('.log')) return true;
53
+ return false;
54
+ }
55
+
56
+ function isBinary(buffer) {
57
+ const sample = buffer.subarray(0, 8192);
58
+ if (sample.includes(0)) return true;
59
+ return !buffer.equals(Buffer.from(buffer.toString('utf8'), 'utf8'));
60
+ }
61
+
62
+ function toPosixPath(relativePath) {
63
+ return relativePath.split(path.sep).join('/');
64
+ }
65
+
66
+ function packAppFiles(rootDir, options = {}) {
67
+ const root = path.resolve(rootDir);
68
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
69
+ throw new Error(`App directory not found: ${root}`);
70
+ }
71
+
72
+ const maxFileBytes = options.maxFileBytes || DEFAULT_MAX_FILE_BYTES;
73
+ const maxTotalBytes = options.maxTotalBytes || DEFAULT_MAX_TOTAL_BYTES;
74
+ const files = [];
75
+ const skipped = [];
76
+ let totalBytes = 0;
77
+
78
+ function visit(dir) {
79
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
80
+ .sort((left, right) => left.name.localeCompare(right.name));
81
+ for (const dirent of entries) {
82
+ const absolute = path.join(dir, dirent.name);
83
+ const relative = toPosixPath(path.relative(root, absolute));
84
+
85
+ if (dirent.isSymbolicLink()) {
86
+ skipped.push({ path: relative, reason: 'symlink' });
87
+ continue;
88
+ }
89
+ if (dirent.isDirectory()) {
90
+ if (EXCLUDED_DIR_NAMES.has(dirent.name)) {
91
+ skipped.push({ path: `${relative}/`, reason: 'excluded' });
92
+ continue;
93
+ }
94
+ visit(absolute);
95
+ continue;
96
+ }
97
+ if (!dirent.isFile()) continue;
98
+ if (isExcludedFileName(dirent.name)) {
99
+ skipped.push({ path: relative, reason: 'excluded' });
100
+ continue;
101
+ }
102
+
103
+ const stat = fs.statSync(absolute);
104
+ if (stat.size > maxFileBytes) {
105
+ skipped.push({ path: relative, reason: 'file-too-large', size: stat.size });
106
+ continue;
107
+ }
108
+ if (totalBytes + stat.size > maxTotalBytes) {
109
+ skipped.push({ path: relative, reason: 'bundle-size-limit', size: stat.size });
110
+ continue;
111
+ }
112
+
113
+ const buffer = fs.readFileSync(absolute);
114
+ const binary = isBinary(buffer);
115
+ totalBytes += stat.size;
116
+ files.push({
117
+ path: relative,
118
+ encoding: binary ? 'base64' : 'utf8',
119
+ content: binary ? buffer.toString('base64') : buffer.toString('utf8'),
120
+ size: stat.size,
121
+ ...(stat.mode & 0o111 ? { executable: true } : {}),
122
+ });
123
+ }
124
+ }
125
+
126
+ visit(root);
127
+ return { files, totalBytes, skipped };
128
+ }
129
+
130
+ function assertInsideTarget(targetRoot, relativePath) {
131
+ const normalized = String(relativePath || '');
132
+ const resolved = path.resolve(targetRoot, normalized);
133
+ if (resolved !== targetRoot && !resolved.startsWith(targetRoot + path.sep)) {
134
+ throw new Error(`App file escapes the install directory: ${normalized}`);
135
+ }
136
+ return resolved;
137
+ }
138
+
139
+ function unpackAppFiles(files, targetDir) {
140
+ const targetRoot = path.resolve(targetDir);
141
+ fs.mkdirSync(targetRoot, { recursive: true });
142
+
143
+ let written = 0;
144
+ for (const file of Array.isArray(files) ? files : []) {
145
+ if (!file || typeof file.path !== 'string' || typeof file.content !== 'string') continue;
146
+ const destination = assertInsideTarget(targetRoot, file.path);
147
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
148
+ const buffer = file.encoding === 'base64'
149
+ ? Buffer.from(file.content, 'base64')
150
+ : Buffer.from(file.content, 'utf8');
151
+ fs.writeFileSync(destination, buffer, { mode: file.executable ? 0o755 : 0o644 });
152
+ written += 1;
153
+ }
154
+ return { written, targetDir: targetRoot };
155
+ }
156
+
157
+ module.exports = {
158
+ packAppFiles,
159
+ unpackAppFiles,
160
+ };