oysterun 1.5.2 → 1.5.3-beta.1

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.
@@ -1145,6 +1145,48 @@
1145
1145
  justify-content: center;
1146
1146
  }
1147
1147
 
1148
+ .agent-store-demo-backdrop {
1149
+ z-index: 10020;
1150
+ }
1151
+
1152
+ .agent-store-demo-status {
1153
+ min-height: 42px;
1154
+ margin-top: 14px;
1155
+ display: flex;
1156
+ align-items: center;
1157
+ gap: 10px;
1158
+ color: var(--ink-soft);
1159
+ font-size: 14px;
1160
+ line-height: 1.45;
1161
+ }
1162
+
1163
+ .agent-store-demo-spinner {
1164
+ width: 16px;
1165
+ height: 16px;
1166
+ flex: 0 0 16px;
1167
+ border: 2px solid rgba(151, 89, 49, 0.22);
1168
+ border-top-color: var(--accent);
1169
+ border-radius: 50%;
1170
+ animation: oysterun-auth-spin 800ms linear infinite;
1171
+ }
1172
+
1173
+ .browse-create-folder-modal .agent-store-demo-actions {
1174
+ display: grid;
1175
+ grid-template-columns: minmax(0, 1fr);
1176
+ gap: 10px;
1177
+ }
1178
+
1179
+ .agent-store-demo-actions > button {
1180
+ min-width: 0;
1181
+ justify-content: center;
1182
+ }
1183
+
1184
+ #agent-store-demo-submit-btn {
1185
+ display: inline-flex;
1186
+ align-items: center;
1187
+ gap: 9px;
1188
+ }
1189
+
1148
1190
  #app {
1149
1191
  display: none;
1150
1192
  height: 100vh;
@@ -7846,6 +7888,26 @@
7846
7888
  >
7847
7889
  </div>
7848
7890
  <div class="topbar-right">
7891
+ <button
7892
+ class="sessions-compose-button topbar-agent-store hidden"
7893
+ id="entry-agent-store-btn"
7894
+ type="button"
7895
+ aria-label="Agent Store"
7896
+ title="Agent Store"
7897
+ data-testid="oysterun-agent-store-demo"
7898
+ onClick=${call("openAgentStoreDemo")}
7899
+ >
7900
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
7901
+ <path
7902
+ d="M4 9.25h16v10.5H4V9.25Zm2-5h12l2 5H4l2-5Zm4 5v10.5m4-10.5v10.5"
7903
+ fill="none"
7904
+ stroke="currentColor"
7905
+ stroke-width="1.7"
7906
+ stroke-linecap="round"
7907
+ stroke-linejoin="round"
7908
+ ></path>
7909
+ </svg>
7910
+ </button>
7849
7911
  <button
7850
7912
  class="sessions-compose-button topbar-sessions-compose hidden"
7851
7913
  id="entry-start-session-btn"
@@ -11040,6 +11102,126 @@ Terminal output will appear here.</pre
11040
11102
  `;
11041
11103
  }
11042
11104
 
11105
+ function AgentStoreDemoModal() {
11106
+ return html`
11107
+ <div
11108
+ id="agent-store-demo-backdrop"
11109
+ class="modal-backdrop sheet browse-create-folder-backdrop agent-store-demo-backdrop hidden"
11110
+ onClick=${closeBackdrop("closeAgentStoreDemo")}
11111
+ >
11112
+ <div
11113
+ class="modal sheet narrow browse-create-folder-modal agent-store-demo-modal"
11114
+ role="dialog"
11115
+ aria-modal="true"
11116
+ aria-labelledby="agent-store-demo-title"
11117
+ data-oysterun-agent-store-demo="true"
11118
+ >
11119
+ <button
11120
+ class="modal-close-btn browse-create-folder-close-btn"
11121
+ id="agent-store-demo-close-btn"
11122
+ type="button"
11123
+ aria-label="Close"
11124
+ title="Close"
11125
+ onClick=${call("closeAgentStoreDemo")}
11126
+ >
11127
+ x
11128
+ </button>
11129
+ <div class="modal-scroll">
11130
+ <span class="eyebrow">Agent Store Demo</span>
11131
+ <h2 id="agent-store-demo-title">Clone GitHub Agent</h2>
11132
+ <div class="field" style="margin-top:14px">
11133
+ <label for="agent-store-demo-repository-select"
11134
+ >Demo Repository</label
11135
+ >
11136
+ <select
11137
+ id="agent-store-demo-repository-select"
11138
+ onChange=${(event) =>
11139
+ window.handleAgentStoreDemoRepositorySelect?.(event)}
11140
+ >
11141
+ <option value="">Choose a demo repository</option>
11142
+ <option
11143
+ value="https://github.com/GoatWang/oysterun-ccstock-workenv"
11144
+ >
11145
+ CCStock WorkEnv
11146
+ </option>
11147
+ <option
11148
+ value="https://github.com/GoatWang/oysterun-ccstock-workenv-en"
11149
+ >
11150
+ CCStock WorkEnv (English)
11151
+ </option>
11152
+ <option
11153
+ value="https://github.com/GoatWang/oysterun-github-tracker"
11154
+ >
11155
+ GitHub Tracker
11156
+ </option>
11157
+ </select>
11158
+ </div>
11159
+ <div class="field" style="margin-top:14px">
11160
+ <label for="agent-store-demo-url-input"
11161
+ >GitHub Repository URL</label
11162
+ >
11163
+ <input
11164
+ id="agent-store-demo-url-input"
11165
+ type="url"
11166
+ inputmode="url"
11167
+ autocomplete="url"
11168
+ autocapitalize="none"
11169
+ spellcheck="false"
11170
+ placeholder="https://github.com/owner/repository"
11171
+ onInput=${(event) =>
11172
+ window.handleAgentStoreDemoUrlInput?.(event)}
11173
+ onKeyDown=${(event) =>
11174
+ window.handleAgentStoreDemoUrlKeyDown?.(event)}
11175
+ />
11176
+ </div>
11177
+ <div
11178
+ id="agent-store-demo-status"
11179
+ class="agent-store-demo-status hidden"
11180
+ role="status"
11181
+ aria-live="polite"
11182
+ aria-atomic="true"
11183
+ >
11184
+ <span
11185
+ id="agent-store-demo-status-spinner"
11186
+ class="agent-store-demo-spinner"
11187
+ aria-hidden="true"
11188
+ ></span>
11189
+ <span id="agent-store-demo-status-text"></span>
11190
+ </div>
11191
+ <div
11192
+ id="agent-store-demo-error"
11193
+ class="error-box hidden"
11194
+ style="margin-top:12px"
11195
+ ></div>
11196
+ </div>
11197
+ <div class="modal-actions agent-store-demo-actions">
11198
+ <button
11199
+ class="btn-primary"
11200
+ id="agent-store-demo-submit-btn"
11201
+ type="button"
11202
+ onClick=${call("submitAgentStoreDemo")}
11203
+ >
11204
+ <span
11205
+ id="agent-store-demo-submit-spinner"
11206
+ class="agent-store-demo-spinner hidden"
11207
+ aria-hidden="true"
11208
+ ></span>
11209
+ <span id="agent-store-demo-submit-label">OK</span>
11210
+ </button>
11211
+ <button
11212
+ class="btn-secondary hidden"
11213
+ id="agent-store-demo-session-setup-btn"
11214
+ type="button"
11215
+ onClick=${call("openAgentStoreDemoSessionSetup")}
11216
+ >
11217
+ Open Session Setup
11218
+ </button>
11219
+ </div>
11220
+ </div>
11221
+ </div>
11222
+ `;
11223
+ }
11224
+
11043
11225
  function ProviderAuthModal() {
11044
11226
  return html`
11045
11227
  <div
@@ -11380,6 +11562,7 @@ Waiting for Claude login output…</pre
11380
11562
  <div id="browse-context-menu" class="browse-context-menu hidden" role="menu" aria-hidden="true" tabindex="-1"></div>
11381
11563
  <div id="app-toast" class="app-toast hidden" role="status" aria-live="polite" aria-atomic="true"></div>
11382
11564
  <${BrowseCreateFolderModal} />
11565
+ <${AgentStoreDemoModal} />
11383
11566
  <${ProviderAuthModal} />
11384
11567
  <${ChatImagePreviewModal} />
11385
11568
  <${HiddenCache} />
@@ -13211,6 +13394,18 @@ Waiting for Claude login output…</pre
13211
13394
  error: "",
13212
13395
  };
13213
13396
  }
13397
+
13398
+ function createAgentStoreDemoState() {
13399
+ return {
13400
+ visible: false,
13401
+ phase: "idle",
13402
+ repositoryUrl: "",
13403
+ status: "",
13404
+ error: "",
13405
+ clone: null,
13406
+ requestId: 0,
13407
+ };
13408
+ }
13214
13409
  function createTerminalViewState() {
13215
13410
  return {
13216
13411
  terminal: null,
@@ -13386,6 +13581,7 @@ Waiting for Claude login output…</pre
13386
13581
  updatedAt: 0,
13387
13582
  };
13388
13583
  let providerAuthModalState = createProviderAuthModalState();
13584
+ let agentStoreDemoState = createAgentStoreDemoState();
13389
13585
  let chatImagePreviewState = {
13390
13586
  visible: false,
13391
13587
  requestKey: "",
@@ -18218,6 +18414,376 @@ Waiting for Claude login output…</pre
18218
18414
  window.openExplorerFromSessionsEmptyState =
18219
18415
  openExplorerFromSessionsEmptyState;
18220
18416
 
18417
+ const AGENT_STORE_DEMO_PENDING_PHASES = new Set([
18418
+ "validating",
18419
+ "cloning",
18420
+ "loading_config",
18421
+ "starting_session",
18422
+ ]);
18423
+
18424
+ const AGENT_STORE_DEMO_REPOSITORY_URLS = new Set([
18425
+ "https://github.com/GoatWang/oysterun-ccstock-workenv",
18426
+ "https://github.com/GoatWang/oysterun-ccstock-workenv-en",
18427
+ "https://github.com/GoatWang/oysterun-github-tracker",
18428
+ ]);
18429
+
18430
+ function isAgentStoreDemoPending() {
18431
+ return AGENT_STORE_DEMO_PENDING_PHASES.has(agentStoreDemoState.phase);
18432
+ }
18433
+
18434
+ function getAgentStoreDemoPendingLabel() {
18435
+ if (agentStoreDemoState.phase === "validating") return "Validating...";
18436
+ if (agentStoreDemoState.phase === "cloning") return "Cloning...";
18437
+ if (agentStoreDemoState.phase === "loading_config")
18438
+ return "Loading config...";
18439
+ if (agentStoreDemoState.phase === "starting_session")
18440
+ return "Starting session...";
18441
+ return "OK";
18442
+ }
18443
+
18444
+ function renderAgentStoreDemo() {
18445
+ const backdrop = $("agent-store-demo-backdrop");
18446
+ if (!backdrop) return;
18447
+ const pending = isAgentStoreDemoPending();
18448
+ const cloneComplete = Boolean(agentStoreDemoState.clone?.agent_folder);
18449
+ const input = $("agent-store-demo-url-input");
18450
+ const repositorySelect = $("agent-store-demo-repository-select");
18451
+ const closeButton = $("agent-store-demo-close-btn");
18452
+ const submitButton = $("agent-store-demo-submit-btn");
18453
+ const submitLabel = $("agent-store-demo-submit-label");
18454
+
18455
+ backdrop.classList.toggle(
18456
+ "hidden",
18457
+ agentStoreDemoState.visible !== true
18458
+ );
18459
+ backdrop.dataset.phase = agentStoreDemoState.phase;
18460
+ if (input) {
18461
+ if (input.value !== agentStoreDemoState.repositoryUrl) {
18462
+ input.value = agentStoreDemoState.repositoryUrl;
18463
+ }
18464
+ input.disabled = pending || cloneComplete;
18465
+ }
18466
+ if (repositorySelect) {
18467
+ repositorySelect.value = AGENT_STORE_DEMO_REPOSITORY_URLS.has(
18468
+ agentStoreDemoState.repositoryUrl
18469
+ )
18470
+ ? agentStoreDemoState.repositoryUrl
18471
+ : "";
18472
+ repositorySelect.disabled = pending || cloneComplete;
18473
+ }
18474
+ if (closeButton) closeButton.disabled = pending;
18475
+ if (submitButton) {
18476
+ submitButton.disabled =
18477
+ pending ||
18478
+ cloneComplete ||
18479
+ !agentStoreDemoState.repositoryUrl.trim();
18480
+ submitButton.classList.toggle(
18481
+ "hidden",
18482
+ agentStoreDemoState.phase === "failed_after_clone"
18483
+ );
18484
+ }
18485
+ if (submitLabel)
18486
+ submitLabel.textContent = getAgentStoreDemoPendingLabel();
18487
+ setVisible("agent-store-demo-submit-spinner", pending);
18488
+ setVisible("agent-store-demo-status", pending);
18489
+ setText(
18490
+ "agent-store-demo-status-text",
18491
+ agentStoreDemoState.status || ""
18492
+ );
18493
+ showError("agent-store-demo-error", agentStoreDemoState.error || "");
18494
+ setVisible(
18495
+ "agent-store-demo-session-setup-btn",
18496
+ agentStoreDemoState.phase === "failed_after_clone" && cloneComplete
18497
+ );
18498
+ }
18499
+
18500
+ function openAgentStoreDemo() {
18501
+ if (isAgentStoreDemoPending()) return;
18502
+ agentStoreDemoState = {
18503
+ ...createAgentStoreDemoState(),
18504
+ visible: true,
18505
+ requestId: agentStoreDemoState.requestId + 1,
18506
+ };
18507
+ renderAgentStoreDemo();
18508
+ requestAnimationFrame(() => {
18509
+ $("agent-store-demo-url-input")?.focus({ preventScroll: true });
18510
+ });
18511
+ }
18512
+
18513
+ function closeAgentStoreDemo(opts = {}) {
18514
+ if (isAgentStoreDemoPending() && opts.force !== true) return;
18515
+ agentStoreDemoState = {
18516
+ ...createAgentStoreDemoState(),
18517
+ requestId: agentStoreDemoState.requestId + 1,
18518
+ };
18519
+ renderAgentStoreDemo();
18520
+ }
18521
+
18522
+ function handleAgentStoreDemoUrlInput(event) {
18523
+ if (isAgentStoreDemoPending() || agentStoreDemoState.clone) return;
18524
+ agentStoreDemoState = {
18525
+ ...agentStoreDemoState,
18526
+ phase: "idle",
18527
+ repositoryUrl: event?.target?.value || "",
18528
+ status: "",
18529
+ error: "",
18530
+ };
18531
+ renderAgentStoreDemo();
18532
+ }
18533
+
18534
+ function handleAgentStoreDemoRepositorySelect(event) {
18535
+ if (isAgentStoreDemoPending() || agentStoreDemoState.clone) return;
18536
+ const repositoryUrl = String(event?.target?.value || "").trim();
18537
+ if (!repositoryUrl) return;
18538
+ agentStoreDemoState = {
18539
+ ...agentStoreDemoState,
18540
+ phase: "idle",
18541
+ repositoryUrl,
18542
+ status: "",
18543
+ error: "",
18544
+ };
18545
+ renderAgentStoreDemo();
18546
+ requestAnimationFrame(() => {
18547
+ $("agent-store-demo-url-input")?.focus({ preventScroll: true });
18548
+ });
18549
+ }
18550
+
18551
+ function handleAgentStoreDemoUrlKeyDown(event) {
18552
+ if (event?.key !== "Enter" || event.isComposing) return;
18553
+ event.preventDefault();
18554
+ if (!isAgentStoreDemoPending()) void submitAgentStoreDemo();
18555
+ }
18556
+
18557
+ function validateAgentStoreDemoUrl(value) {
18558
+ let parsed;
18559
+ try {
18560
+ parsed = new URL(String(value || "").trim());
18561
+ } catch {
18562
+ throw new Error("Enter a valid public GitHub HTTPS repository URL.");
18563
+ }
18564
+ if (
18565
+ parsed.protocol !== "https:" ||
18566
+ parsed.hostname.toLowerCase() !== "github.com" ||
18567
+ parsed.username ||
18568
+ parsed.password ||
18569
+ parsed.port ||
18570
+ parsed.search ||
18571
+ parsed.hash ||
18572
+ !/^\/[^/]+\/[^/]+\/?$/.test(parsed.pathname)
18573
+ ) {
18574
+ throw new Error(
18575
+ "Only public https://github.com/<owner>/<repo> URLs are supported."
18576
+ );
18577
+ }
18578
+ return parsed.toString();
18579
+ }
18580
+
18581
+ async function getAgentStoreDemoProviderSkillInstallDecision({
18582
+ agentId,
18583
+ agentFolder,
18584
+ provider,
18585
+ }) {
18586
+ const params = new URLSearchParams({
18587
+ agent_id: agentId,
18588
+ agent_folder: agentFolder,
18589
+ provider,
18590
+ });
18591
+ try {
18592
+ const status = await apiWithAuthRetry(
18593
+ "GET",
18594
+ `/agent/provider-skill-status?${params.toString()}`
18595
+ );
18596
+ return (
18597
+ status?.default_checked === true &&
18598
+ status?.can_install === true &&
18599
+ status?.disabled !== true
18600
+ );
18601
+ } catch {
18602
+ return false;
18603
+ }
18604
+ }
18605
+
18606
+ function buildAgentStoreDemoSessionStartPayload({
18607
+ clone,
18608
+ folderConfig,
18609
+ installOysterunSkills,
18610
+ }) {
18611
+ const provider = String(folderConfig?.provider || "").trim();
18612
+ const model = String(folderConfig?.model || "").trim();
18613
+ if (!provider || !model) {
18614
+ throw new Error(
18615
+ "The cloned folder did not resolve a provider and model."
18616
+ );
18617
+ }
18618
+ const payload = {
18619
+ agent_id: clone.agent_id,
18620
+ agent_folder: clone.agent_folder,
18621
+ provider,
18622
+ model,
18623
+ interface_type: normalizeFirstPartyInterfaceType(
18624
+ folderConfig.interfaceType || "coding"
18625
+ ),
18626
+ reasoning_effort: folderConfig.reasoningEffort || undefined,
18627
+ dangerous_mode: folderConfig.dangerousMode === true,
18628
+ allow_dangerously_skip_permissions:
18629
+ folderConfig.allowDangerouslySkipPermissions === true,
18630
+ install_oysterun_skills: installOysterunSkills === true,
18631
+ };
18632
+ if (provider === "claude") {
18633
+ payload.permission_mode = folderConfig.permissionMode;
18634
+ } else if (provider === "codex") {
18635
+ payload.approval_policy = folderConfig.approvalPolicy;
18636
+ }
18637
+ const proof = {
18638
+ ...buildCreateSessionSetupProviderModelPermissionProof(payload),
18639
+ proof_surface: "visible_agent_store_clone_modal_state",
18640
+ };
18641
+ payload.session_setup_provider_model_permission_proof = proof;
18642
+ return { payload, proof };
18643
+ }
18644
+
18645
+ function finalizeAgentStoreDemoStartedSession(data, expectedProof) {
18646
+ const proofValidation =
18647
+ validateSessionSetupProviderModelPermissionProofResponse(
18648
+ data,
18649
+ expectedProof,
18650
+ { requireCountableStartResponseProof: true }
18651
+ );
18652
+ if (!proofValidation.ok) {
18653
+ throw new Error(
18654
+ `Session Setup proof validation failed: ${proofValidation.errors.join(
18655
+ " "
18656
+ )}`
18657
+ );
18658
+ }
18659
+ const normalizedData = {
18660
+ ...data,
18661
+ session_setup_provider_model_permission_proof: proofValidation.proof,
18662
+ };
18663
+ clearPendingAttachments();
18664
+ primeSelectedAgentFromStart(normalizedData);
18665
+ const startedLiveSession =
18666
+ upsertStartedSessionIntoLiveCatalog(normalizedData);
18667
+ invalidateDashboardDataCaches({
18668
+ catalog: true,
18669
+ sessions: true,
18670
+ history: true,
18671
+ transcripts: true,
18672
+ });
18673
+ if (
18674
+ !startedLiveSession ||
18675
+ startedLiveSession.sessionId !== normalizedData.session_id
18676
+ ) {
18677
+ throw new Error(
18678
+ "Agent Store could not materialize the started Host session before opening chat."
18679
+ );
18680
+ }
18681
+ return normalizedData.session_id;
18682
+ }
18683
+
18684
+ async function submitAgentStoreDemo() {
18685
+ if (isAgentStoreDemoPending() || agentStoreDemoState.clone) return;
18686
+ const requestId = agentStoreDemoState.requestId + 1;
18687
+ agentStoreDemoState = {
18688
+ ...agentStoreDemoState,
18689
+ phase: "validating",
18690
+ status: "Validating GitHub repository...",
18691
+ error: "",
18692
+ requestId,
18693
+ };
18694
+ renderAgentStoreDemo();
18695
+
18696
+ try {
18697
+ const repositoryUrl = validateAgentStoreDemoUrl(
18698
+ agentStoreDemoState.repositoryUrl
18699
+ );
18700
+ agentStoreDemoState = {
18701
+ ...agentStoreDemoState,
18702
+ phase: "cloning",
18703
+ status: "Cloning the repository into Default Browse Root...",
18704
+ };
18705
+ renderAgentStoreDemo();
18706
+ const clone = await apiWithAuthRetry(
18707
+ "POST",
18708
+ "/agent-store/demo/clone",
18709
+ { repository_url: repositoryUrl }
18710
+ );
18711
+ if (agentStoreDemoState.requestId !== requestId) return;
18712
+
18713
+ agentStoreDemoState = {
18714
+ ...agentStoreDemoState,
18715
+ phase: "loading_config",
18716
+ status: "Loading the cloned agent configuration...",
18717
+ clone,
18718
+ };
18719
+ renderAgentStoreDemo();
18720
+ const folderConfig = await apiWithAuthRetry(
18721
+ "GET",
18722
+ `/dev/folder-config?path=${encodeURIComponent(clone.agent_folder)}`
18723
+ );
18724
+ if (agentStoreDemoState.requestId !== requestId) return;
18725
+ const installOysterunSkills =
18726
+ await getAgentStoreDemoProviderSkillInstallDecision({
18727
+ agentId: clone.agent_id,
18728
+ agentFolder: clone.agent_folder,
18729
+ provider: folderConfig.provider,
18730
+ });
18731
+ const { payload, proof } = buildAgentStoreDemoSessionStartPayload({
18732
+ clone,
18733
+ folderConfig,
18734
+ installOysterunSkills,
18735
+ });
18736
+
18737
+ agentStoreDemoState = {
18738
+ ...agentStoreDemoState,
18739
+ phase: "starting_session",
18740
+ status: "Starting a session with the resolved agent settings...",
18741
+ };
18742
+ renderAgentStoreDemo();
18743
+ const data = await apiWithAuthRetry(
18744
+ "POST",
18745
+ "/session/start",
18746
+ payload
18747
+ );
18748
+ if (agentStoreDemoState.requestId !== requestId) return;
18749
+ const sessionId = finalizeAgentStoreDemoStartedSession(data, proof);
18750
+ closeAgentStoreDemo({ force: true });
18751
+ openRouteCForStartedSession(sessionId);
18752
+ } catch (err) {
18753
+ if (agentStoreDemoState.requestId !== requestId) return;
18754
+ const cloneComplete = Boolean(
18755
+ agentStoreDemoState.clone?.agent_folder
18756
+ );
18757
+ const detail = String(err?.data?.detail || "").trim();
18758
+ const message = cloneComplete
18759
+ ? formatCreateStepError("Could not start session", err)
18760
+ : err?.message || "Could not clone the GitHub repository.";
18761
+ agentStoreDemoState = {
18762
+ ...agentStoreDemoState,
18763
+ phase: cloneComplete ? "failed_after_clone" : "failed_before_clone",
18764
+ status: "",
18765
+ error: detail ? `${message}\n${detail}` : message,
18766
+ };
18767
+ renderAgentStoreDemo();
18768
+ }
18769
+ }
18770
+
18771
+ function openAgentStoreDemoSessionSetup() {
18772
+ const folder = agentStoreDemoState.clone?.agent_folder || "";
18773
+ if (!folder || isAgentStoreDemoPending()) return;
18774
+ closeAgentStoreDemo({ force: true });
18775
+ openCreateModal(folder);
18776
+ }
18777
+
18778
+ window.openAgentStoreDemo = openAgentStoreDemo;
18779
+ window.closeAgentStoreDemo = closeAgentStoreDemo;
18780
+ window.handleAgentStoreDemoUrlInput = handleAgentStoreDemoUrlInput;
18781
+ window.handleAgentStoreDemoRepositorySelect =
18782
+ handleAgentStoreDemoRepositorySelect;
18783
+ window.handleAgentStoreDemoUrlKeyDown = handleAgentStoreDemoUrlKeyDown;
18784
+ window.submitAgentStoreDemo = submitAgentStoreDemo;
18785
+ window.openAgentStoreDemoSessionSetup = openAgentStoreDemoSessionSetup;
18786
+
18221
18787
  function getHostDefaultRuntime() {
18222
18788
  return (
18223
18789
  hostPreferencesState.effectiveDefaultProvider ||
@@ -29060,6 +29626,7 @@ Waiting for Claude login output…</pre
29060
29626
  setVisible("browser-site-next-btn", isBrowser);
29061
29627
  setVisible("browser-site-share-btn", isBrowser);
29062
29628
  setVisible("browser-site-exit-btn", isBrowser);
29629
+ setVisible("entry-agent-store-btn", screen === "sessions");
29063
29630
  setVisible("entry-start-session-btn", screen === "sessions");
29064
29631
  setVisible("topbar-scheduler-new-btn", isSchedulerList);
29065
29632
  setVisible("topbar-scheduler-refresh-btn", isSchedulerList);
@@ -29570,6 +30137,7 @@ Waiting for Claude login output…</pre
29570
30137
  "browse-context-menu",
29571
30138
  "picker-modal-backdrop",
29572
30139
  "browse-create-folder-backdrop",
30140
+ "agent-store-demo-backdrop",
29573
30141
  "provider-auth-backdrop",
29574
30142
  "chat-image-preview-backdrop",
29575
30143
  "command-palette",
@@ -0,0 +1,369 @@
1
+ import { randomUUID } from "crypto";
2
+ import { spawn } from "child_process";
3
+ import { lstat, realpath, rename, rm, stat } from "fs/promises";
4
+ import { join, relative } from "path";
5
+ import { resolveDefaultBrowsePathAsync } from "./config.mjs";
6
+ import { deriveBrowserRootProjectId } from "./browser-root-project-index.mjs";
7
+
8
+ export const AGENT_STORE_DEMO_CLONE_TIMEOUT_MS = Math.max(
9
+ 1,
10
+ Number.parseInt(
11
+ process.env.OYSTERUN_AGENT_STORE_DEMO_CLONE_TIMEOUT_MS || "120000",
12
+ 10
13
+ ) || 120000
14
+ );
15
+
16
+ const MAX_GIT_OUTPUT_BYTES = 64 * 1024;
17
+ const GIT_KILL_GRACE_MS = 1500;
18
+ const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
19
+ const GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+$/;
20
+
21
+ function createAgentStoreError(message, status, code, detail = "") {
22
+ const error = new Error(message);
23
+ error.status = status;
24
+ error.code = code;
25
+ if (detail) error.detail = detail;
26
+ return error;
27
+ }
28
+
29
+ function normalizeString(value) {
30
+ return typeof value === "string" ? value.trim() : "";
31
+ }
32
+
33
+ function appendBoundedOutput(current, chunk) {
34
+ if (current.length >= MAX_GIT_OUTPUT_BYTES) return current;
35
+ return `${current}${String(chunk)}`.slice(0, MAX_GIT_OUTPUT_BYTES);
36
+ }
37
+
38
+ function summarizeGitError(stderr, stdout) {
39
+ const value = normalizeString(stderr) || normalizeString(stdout);
40
+ return value.slice(0, 2000);
41
+ }
42
+
43
+ async function pathExists(path) {
44
+ return Boolean(await lstat(path).catch(() => null));
45
+ }
46
+
47
+ function assertDirectChild(candidate, root) {
48
+ const relativePath = relative(root, candidate);
49
+ if (
50
+ !relativePath ||
51
+ relativePath.startsWith("..") ||
52
+ relativePath.startsWith("/") ||
53
+ relativePath.includes("/") ||
54
+ relativePath.includes("\\")
55
+ ) {
56
+ throw createAgentStoreError(
57
+ "Agent Store destination must be a direct child of Default Browse Root",
58
+ 400,
59
+ "agent_store_destination_invalid"
60
+ );
61
+ }
62
+ }
63
+
64
+ export function parsePublicGitHubRepositoryUrl(rawValue) {
65
+ const raw = normalizeString(rawValue);
66
+ if (!raw) {
67
+ throw createAgentStoreError(
68
+ "GitHub Repository URL is required",
69
+ 400,
70
+ "invalid_github_repository_url"
71
+ );
72
+ }
73
+
74
+ let parsed;
75
+ try {
76
+ parsed = new URL(raw);
77
+ } catch {
78
+ throw createAgentStoreError(
79
+ "Enter a valid public GitHub HTTPS repository URL",
80
+ 400,
81
+ "invalid_github_repository_url"
82
+ );
83
+ }
84
+
85
+ if (!/^https:\/\/github\.com\//i.test(raw)) {
86
+ throw createAgentStoreError(
87
+ "Only public https://github.com/<owner>/<repo> URLs are supported",
88
+ 400,
89
+ "invalid_github_repository_url"
90
+ );
91
+ }
92
+
93
+ if (
94
+ parsed.protocol !== "https:" ||
95
+ parsed.hostname.toLowerCase() !== "github.com" ||
96
+ parsed.username ||
97
+ parsed.password ||
98
+ parsed.port ||
99
+ parsed.search ||
100
+ parsed.hash
101
+ ) {
102
+ throw createAgentStoreError(
103
+ "Only public https://github.com/<owner>/<repo> URLs are supported",
104
+ 400,
105
+ "invalid_github_repository_url"
106
+ );
107
+ }
108
+
109
+ const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+)\/?$/);
110
+ if (!match) {
111
+ throw createAgentStoreError(
112
+ "GitHub URL must contain exactly one owner and repository",
113
+ 400,
114
+ "invalid_github_repository_url"
115
+ );
116
+ }
117
+
118
+ let owner;
119
+ let repositorySegment;
120
+ try {
121
+ owner = decodeURIComponent(match[1]);
122
+ repositorySegment = decodeURIComponent(match[2]);
123
+ } catch {
124
+ throw createAgentStoreError(
125
+ "GitHub URL contains invalid path encoding",
126
+ 400,
127
+ "invalid_github_repository_url"
128
+ );
129
+ }
130
+ const repositoryName = repositorySegment.endsWith(".git")
131
+ ? repositorySegment.slice(0, -4)
132
+ : repositorySegment;
133
+
134
+ if (
135
+ !GITHUB_OWNER_PATTERN.test(owner) ||
136
+ !GITHUB_REPOSITORY_PATTERN.test(repositoryName) ||
137
+ repositoryName === "." ||
138
+ repositoryName === ".."
139
+ ) {
140
+ throw createAgentStoreError(
141
+ "GitHub owner or repository name is invalid",
142
+ 400,
143
+ "invalid_github_repository_url"
144
+ );
145
+ }
146
+
147
+ return {
148
+ owner,
149
+ repositoryName,
150
+ repositoryUrl: `https://github.com/${owner}/${repositoryName}`,
151
+ cloneUrl: `https://github.com/${owner}/${repositoryName}.git`,
152
+ agentId: deriveBrowserRootProjectId(repositoryName),
153
+ };
154
+ }
155
+
156
+ function terminateGitProcess(child, signal) {
157
+ if (!child) return;
158
+ if (process.platform !== "win32" && Number.isInteger(child.pid)) {
159
+ try {
160
+ process.kill(-child.pid, signal);
161
+ return;
162
+ } catch {
163
+ // Fall through to the direct child when the process group is unavailable.
164
+ }
165
+ }
166
+ try {
167
+ child.kill(signal);
168
+ } catch {
169
+ // The process may already have exited.
170
+ }
171
+ }
172
+
173
+ export function runGitShallowClone({
174
+ cloneUrl,
175
+ destinationPath,
176
+ timeoutMs = AGENT_STORE_DEMO_CLONE_TIMEOUT_MS,
177
+ spawnImpl = spawn,
178
+ } = {}) {
179
+ return new Promise((resolvePromise, rejectPromise) => {
180
+ let child;
181
+ try {
182
+ child = spawnImpl(
183
+ "git",
184
+ ["clone", "--depth", "1", "--", cloneUrl, destinationPath],
185
+ {
186
+ detached: process.platform !== "win32",
187
+ env: process.env,
188
+ stdio: ["ignore", "pipe", "pipe"],
189
+ }
190
+ );
191
+ } catch (err) {
192
+ rejectPromise(
193
+ createAgentStoreError(
194
+ err?.code === "ENOENT"
195
+ ? "Git is not available on this Host"
196
+ : "Could not start Git clone",
197
+ err?.code === "ENOENT" ? 503 : 502,
198
+ err?.code === "ENOENT" ? "git_unavailable" : "agent_store_clone_failed"
199
+ )
200
+ );
201
+ return;
202
+ }
203
+
204
+ let stdout = "";
205
+ let stderr = "";
206
+ let timedOut = false;
207
+ let settled = false;
208
+ let forceKillTimer = null;
209
+
210
+ const timeout = setTimeout(() => {
211
+ timedOut = true;
212
+ terminateGitProcess(child, "SIGTERM");
213
+ forceKillTimer = setTimeout(() => {
214
+ terminateGitProcess(child, "SIGKILL");
215
+ }, GIT_KILL_GRACE_MS);
216
+ }, timeoutMs);
217
+
218
+ const cleanup = () => {
219
+ clearTimeout(timeout);
220
+ if (forceKillTimer) clearTimeout(forceKillTimer);
221
+ };
222
+
223
+ const rejectOnce = (error) => {
224
+ if (settled) return;
225
+ settled = true;
226
+ cleanup();
227
+ rejectPromise(error);
228
+ };
229
+
230
+ child.stdout?.on("data", (chunk) => {
231
+ stdout = appendBoundedOutput(stdout, chunk);
232
+ });
233
+ child.stderr?.on("data", (chunk) => {
234
+ stderr = appendBoundedOutput(stderr, chunk);
235
+ });
236
+ child.once("error", (err) => {
237
+ rejectOnce(
238
+ createAgentStoreError(
239
+ err?.code === "ENOENT"
240
+ ? "Git is not available on this Host"
241
+ : "Could not start Git clone",
242
+ err?.code === "ENOENT" ? 503 : 502,
243
+ err?.code === "ENOENT" ? "git_unavailable" : "agent_store_clone_failed"
244
+ )
245
+ );
246
+ });
247
+ child.once("close", (code, signal) => {
248
+ if (settled) return;
249
+ settled = true;
250
+ cleanup();
251
+ if (timedOut) {
252
+ rejectPromise(
253
+ createAgentStoreError(
254
+ "GitHub clone timed out",
255
+ 504,
256
+ "agent_store_clone_timeout"
257
+ )
258
+ );
259
+ return;
260
+ }
261
+ if (code !== 0) {
262
+ rejectPromise(
263
+ createAgentStoreError(
264
+ "Could not clone the GitHub repository",
265
+ 502,
266
+ "agent_store_clone_failed",
267
+ summarizeGitError(stderr, stdout) ||
268
+ `git exited with code ${code ?? "unknown"}${
269
+ signal ? ` (${signal})` : ""
270
+ }`
271
+ )
272
+ );
273
+ return;
274
+ }
275
+ resolvePromise({ stdout, stderr, exitCode: code });
276
+ });
277
+ });
278
+ }
279
+
280
+ export async function clonePublicGitHubAgent({
281
+ repositoryUrl,
282
+ browseRoot = null,
283
+ timeoutMs = AGENT_STORE_DEMO_CLONE_TIMEOUT_MS,
284
+ runClone = runGitShallowClone,
285
+ requestId = randomUUID(),
286
+ } = {}) {
287
+ const repository = parsePublicGitHubRepositoryUrl(repositoryUrl);
288
+ const requestedRoot = browseRoot || (await resolveDefaultBrowsePathAsync());
289
+ const rootPath = await realpath(requestedRoot);
290
+ const rootStats = await stat(rootPath);
291
+ if (!rootStats.isDirectory()) {
292
+ throw createAgentStoreError(
293
+ "Default Browse Root is not a directory",
294
+ 400,
295
+ "agent_store_browse_root_invalid"
296
+ );
297
+ }
298
+
299
+ const finalPath = join(rootPath, repository.repositoryName);
300
+ assertDirectChild(finalPath, rootPath);
301
+ if (await pathExists(finalPath)) {
302
+ throw createAgentStoreError(
303
+ "Agent folder already exists",
304
+ 409,
305
+ "agent_folder_already_exists"
306
+ );
307
+ }
308
+
309
+ const stagingName = `.oysterun-agent-store-${repository.agentId}-${requestId}.partial`;
310
+ const stagingPath = join(rootPath, stagingName);
311
+ assertDirectChild(stagingPath, rootPath);
312
+ if (await pathExists(stagingPath)) {
313
+ throw createAgentStoreError(
314
+ "Agent Store staging folder already exists",
315
+ 409,
316
+ "agent_store_staging_conflict"
317
+ );
318
+ }
319
+
320
+ let finalized = false;
321
+ try {
322
+ await runClone({
323
+ cloneUrl: repository.cloneUrl,
324
+ destinationPath: stagingPath,
325
+ timeoutMs,
326
+ });
327
+ const stagingStats = await stat(stagingPath).catch(() => null);
328
+ if (!stagingStats?.isDirectory()) {
329
+ throw createAgentStoreError(
330
+ "Git clone did not create an agent folder",
331
+ 502,
332
+ "agent_store_clone_failed"
333
+ );
334
+ }
335
+ if (await pathExists(finalPath)) {
336
+ throw createAgentStoreError(
337
+ "Agent folder already exists",
338
+ 409,
339
+ "agent_store_clone_finalize_conflict"
340
+ );
341
+ }
342
+ try {
343
+ await rename(stagingPath, finalPath);
344
+ } catch (err) {
345
+ if (["EEXIST", "ENOTEMPTY"].includes(err?.code)) {
346
+ throw createAgentStoreError(
347
+ "Agent folder already exists",
348
+ 409,
349
+ "agent_store_clone_finalize_conflict"
350
+ );
351
+ }
352
+ throw err;
353
+ }
354
+ finalized = true;
355
+ return {
356
+ status: "agent_store_demo_clone_complete",
357
+ repository_url: repository.repositoryUrl,
358
+ repository_owner: repository.owner,
359
+ repository_name: repository.repositoryName,
360
+ agent_id: repository.agentId,
361
+ agent_folder: await realpath(finalPath),
362
+ clone_depth: 1,
363
+ };
364
+ } finally {
365
+ if (!finalized) {
366
+ await rm(stagingPath, { recursive: true, force: true }).catch(() => {});
367
+ }
368
+ }
369
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oysterun-host-service",
3
- "version": "1.5.2",
3
+ "version": "1.5.3-beta.1",
4
4
  "type": "module",
5
5
  "description": "Oysterun host service — runs on Mac mini, manages Claude Code sessions",
6
6
  "scripts": {
@@ -117,6 +117,7 @@ import {
117
117
  listFolderPage,
118
118
  validateFolderPath,
119
119
  } from "./folder-browser.mjs";
120
+ import { clonePublicGitHubAgent } from "./agent-store-demo.mjs";
120
121
  import { attachRuntimeTurnId } from "./runtime-turn-events.mjs";
121
122
  import { TerminalSessionManager } from "./terminal-session-manager.mjs";
122
123
  import {
@@ -13454,6 +13455,44 @@ const server = createServer(async (req, res) => {
13454
13455
  }
13455
13456
  }
13456
13457
 
13458
+ if (req.method === "POST" && path === "/agent-store/demo/clone") {
13459
+ if (!requireDashboardMode(claims, res)) return;
13460
+ try {
13461
+ const body = await readBody(req);
13462
+ const payload = await clonePublicGitHubAgent({
13463
+ repositoryUrl: body.repository_url,
13464
+ });
13465
+ const snapshot = browserRootProjectIndex.refresh({
13466
+ reason: "agent_store_demo_clone",
13467
+ rebuildWatchers: true,
13468
+ });
13469
+ const discoveredProject = snapshot.projects.find(
13470
+ (project) => project.agent_folder === payload.agent_folder
13471
+ );
13472
+ return respond(res, 201, {
13473
+ ...payload,
13474
+ project_discovery: discoveredProject
13475
+ ? {
13476
+ status: "browser_root_project_discovered",
13477
+ project_id: discoveredProject.project_id,
13478
+ agent_folder: discoveredProject.agent_folder,
13479
+ }
13480
+ : {
13481
+ status: "browser_root_folder_unmarked",
13482
+ project_id: payload.agent_id,
13483
+ agent_folder: payload.agent_folder,
13484
+ },
13485
+ });
13486
+ } catch (err) {
13487
+ const response = {
13488
+ error: err.message || "Could not clone the GitHub repository",
13489
+ code: err.code || "agent_store_clone_failed",
13490
+ };
13491
+ if (err.detail) response.detail = err.detail;
13492
+ return respond(res, err.status || 500, response);
13493
+ }
13494
+ }
13495
+
13457
13496
  if (req.method === "POST" && path === "/dev/open-permissions-settings") {
13458
13497
  if (!requireDashboardMode(claims, res)) return;
13459
13498
  if (!supportsMacFolderAccessPermissions()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oysterun",
3
- "version": "1.5.2",
3
+ "version": "1.5.3-beta.1",
4
4
  "type": "module",
5
5
  "description": "Oysterun Host installer and local service",
6
6
  "bin": {