claude-remote-cli 3.10.0 → 3.11.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.
@@ -1,4 +1,4 @@
1
- .pin-gate.svelte-1qp96yb{display:flex;align-items:center;justify-content:center;height:100vh;background:var(--bg);padding:1rem}.pin-container.svelte-1qp96yb{display:flex;flex-direction:column;align-items:center;gap:1rem;width:100%;max-width:320px;text-align:center}.pin-container.svelte-1qp96yb h1:where(.svelte-1qp96yb){font-size:1.5rem;color:var(--text)}.pin-container.svelte-1qp96yb p:where(.svelte-1qp96yb){color:var(--text-muted);font-size:.95rem}input.svelte-1qp96yb{width:100%;padding:14px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:1.2rem;text-align:center;outline:none;-webkit-appearance:none}input.svelte-1qp96yb:focus{border-color:var(--accent)}button.svelte-1qp96yb{width:100%;padding:14px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;touch-action:manipulation}button.svelte-1qp96yb:active{opacity:.8}.error.svelte-1qp96yb{color:var(--accent);font-size:.9rem}.context-menu-trigger.svelte-1nmhce7{background:none;border:none;color:var(--text-muted);font-size:1rem;font-weight:700;cursor:pointer;padding:0 6px;border-radius:4px;touch-action:manipulation;flex-shrink:0;line-height:1;letter-spacing:1px;min-height:24px;display:inline-flex;align-items:center;transition:color .15s,background .15s}.context-menu-trigger.svelte-1nmhce7:hover{color:var(--text);background:var(--border)}.context-menu-backdrop.svelte-1nmhce7{position:fixed;top:0;right:0;bottom:0;left:0;z-index:999}.context-menu.svelte-1nmhce7{position:fixed;list-style:none;margin:0;padding:4px 0;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px #0006;z-index:1000;min-width:175px}.context-menu-item.svelte-1nmhce7{padding:9px 14px;font-size:.85rem;cursor:pointer;color:var(--text);white-space:nowrap}.context-menu-item.svelte-1nmhce7:hover{background:var(--border)}.context-menu-item.svelte-1nmhce7:focus{outline:2px solid var(--accent);outline-offset:-2px}.context-menu-item--danger.svelte-1nmhce7{color:#e74c3c}.context-menu-item--danger.svelte-1nmhce7:hover{background:#e74c3c1f}.context-menu-item--disabled.svelte-1nmhce7{opacity:.4;cursor:default}.context-menu-item--disabled.svelte-1nmhce7:hover{background:none}.workspace-item.svelte-168i8d5{display:flex;flex-direction:column}.workspace-header.svelte-168i8d5{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer;min-height:44px;transition:background .12s}.workspace-header.svelte-168i8d5:hover{background:var(--surface-hover)}.workspace-item.active.svelte-168i8d5 .workspace-header:where(.svelte-168i8d5){background:var(--surface-hover)}.workspace-left.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.workspace-header.reorder-mode.svelte-168i8d5{cursor:grab}.workspace-header.reorder-mode.svelte-168i8d5:active{cursor:grabbing}.collapse-chevron.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:.7rem;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:color .12s}.collapse-chevron.svelte-168i8d5:hover{color:var(--text)}.collapse-count.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);background:var(--border);border-radius:8px;padding:1px 6px;flex-shrink:0}.initial-block.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;font-size:var(--font-size-xs);font-weight:700;color:#000;font-family:var(--font-mono);flex-shrink:0;line-height:1}.workspace-name.svelte-168i8d5{font-size:var(--font-size-sm);font-weight:700;color:var(--text);font-family:var(--font-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.workspace-actions.svelte-168i8d5{display:flex;align-items:center;gap:2px;opacity:0;transition:opacity .12s;flex-shrink:0}.workspace-header.svelte-168i8d5:hover .workspace-actions:where(.svelte-168i8d5){opacity:1}.action-btn.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:3px;font-size:var(--font-size-xs);color:var(--text-muted);cursor:pointer;font-family:var(--font-mono);transition:background .1s,color .1s}.action-btn.svelte-168i8d5:hover{background:var(--border);color:var(--text)}.session-list.svelte-168i8d5{list-style:none}.session-row.svelte-168i8d5{position:relative;display:flex;flex-direction:column;gap:2px;padding:6px 10px 6px 36px;cursor:pointer;min-height:44px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);transition:background .1s;border-left:3px solid transparent;justify-content:center}.session-row.svelte-168i8d5:hover{background:var(--surface-hover);color:var(--text)}.session-row.selected.svelte-168i8d5{border-left-color:var(--accent);background:var(--surface-hover);color:var(--text)}.session-row.attention.svelte-168i8d5 .session-name:where(.svelte-168i8d5){font-weight:700;color:var(--text)}.session-row-primary.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0}.session-row-secondary.svelte-168i8d5{display:flex;align-items:center;gap:6px;padding-left:15px;font-size:.65rem;color:var(--text-muted);min-width:0}.secondary-branch.svelte-168i8d5{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.row-menu-overlay.svelte-168i8d5{position:absolute;right:8px;top:0;bottom:0;display:flex;align-items:center;opacity:0;transition:opacity .12s}.session-row.svelte-168i8d5:hover .row-menu-overlay:where(.svelte-168i8d5){opacity:1}.secondary-pr.svelte-168i8d5{white-space:nowrap;flex-shrink:0;color:var(--accent)}.secondary-time.svelte-168i8d5{white-space:nowrap;flex-shrink:0;opacity:.7}.session-count-badge.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;border-radius:8px;background:var(--border);color:var(--text-muted);font-size:.55rem;font-family:var(--font-mono);font-weight:600;padding:0 4px;flex-shrink:0}.diff-badge.svelte-168i8d5{display:inline-flex;align-items:center;gap:4px;font-size:.6rem;flex-shrink:0;margin-left:auto}.diff-add.svelte-168i8d5{color:var(--status-success)}.diff-del.svelte-168i8d5{color:var(--status-error)}.status-dot.svelte-168i8d5{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.status-dot--running.svelte-168i8d5{background:var(--status-success)}.status-dot--idle.svelte-168i8d5{background:var(--status-info)}.dot-inactive.svelte-168i8d5{width:7px;height:7px;border-radius:50%;background:#555;flex-shrink:0}.session-row.inactive.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--text-muted)}.session-row.inactive.svelte-168i8d5:hover .session-name:where(.svelte-168i8d5){color:var(--text)}.session-row.loading.svelte-168i8d5{pointer-events:none;opacity:.7}.session-row.loading.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--accent)}.status-dot--attention.svelte-168i8d5{background:var(--status-warning);box-shadow:0 0 5px 1px #fbbf2473;animation:svelte-168i8d5-attention-glow 2s ease-in-out infinite}.status-dot--permission-prompt.svelte-168i8d5{background:#eab308;box-shadow:0 0 5px 1px #eab30873;animation:svelte-168i8d5-attention-glow 1.5s ease-in-out infinite}@keyframes svelte-168i8d5-attention-glow{0%,to{box-shadow:0 0 3px 1px #fbbf244d}50%{box-shadow:0 0 7px 2px #fbbf2499}}.terminal-icon.svelte-168i8d5{font-size:.6rem;font-weight:700;color:var(--text-muted);flex-shrink:0;font-family:var(--font-mono);line-height:1}.session-name.svelte-168i8d5{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0}.session-name.bold.svelte-168i8d5{font-weight:700}.add-worktree-row.svelte-168i8d5{padding:4px 10px 6px 36px}.add-worktree-btn.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;cursor:pointer;transition:opacity .1s,color .1s}.add-worktree-btn.svelte-168i8d5:hover{opacity:1;color:var(--text)}.add-worktree-row.disabled.svelte-168i8d5{pointer-events:none}.add-worktree-row.disabled.svelte-168i8d5 .add-worktree-btn:where(.svelte-168i8d5){opacity:.7;color:var(--accent)}.workspace-divider.svelte-168i8d5{height:1px;background:var(--border);margin:0}@media(max-width:600px){.workspace-header.svelte-168i8d5,.session-row.svelte-168i8d5{min-height:48px}.workspace-actions.svelte-168i8d5{opacity:1}.row-menu-overlay.svelte-168i8d5{display:none}}.smart-search.svelte-itm1qs{position:relative;flex-shrink:0}.input-row.svelte-itm1qs{display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid var(--border);gap:6px}.prompt.svelte-itm1qs{font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--accent);flex-shrink:0;line-height:1;-webkit-user-select:none;user-select:none}.search-input.svelte-itm1qs{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-family:var(--font-mono);font-size:var(--font-size-sm);caret-color:var(--accent)}.search-input.svelte-itm1qs::placeholder{color:var(--text-muted);opacity:.5}.dropdown.svelte-itm1qs{position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-top:none;list-style:none;z-index:200;max-height:240px;overflow-y:auto}.dropdown-item.svelte-itm1qs{display:flex;align-items:baseline;gap:6px;padding:8px 10px;cursor:pointer;font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--text-muted);transition:background .1s;white-space:nowrap;overflow:hidden}.dropdown-item.svelte-itm1qs:hover,.dropdown-item.focused.svelte-itm1qs{background:var(--surface-hover);color:var(--text)}.dropdown-item.svelte-itm1qs strong:where(.svelte-itm1qs){color:var(--text);font-weight:700}.dropdown-path.svelte-itm1qs{font-size:var(--font-size-xs);color:var(--text-muted);opacity:.5;overflow:hidden;text-overflow:ellipsis;min-width:0;flex-shrink:1}@media(max-width:600px){.smart-search.svelte-itm1qs{width:100%}.input-row.svelte-itm1qs{width:100%;box-sizing:border-box}.search-input.svelte-itm1qs{width:100%;min-width:0}}.sidebar.svelte-owj5vn{position:relative;display:flex;flex-direction:column;background:var(--bg);border-right:1px solid var(--border);overflow:hidden;transition:transform .25s ease,width .2s ease,min-width .2s ease;z-index:100}.resize-handle.svelte-owj5vn{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;z-index:10;transition:background .15s}.resize-handle.svelte-owj5vn:hover{background:var(--accent)}.sidebar-header.svelte-owj5vn{display:flex;align-items:center;justify-content:space-between;padding:12px 10px;border-bottom:1px solid var(--border);flex-shrink:0}.sidebar-label.svelte-owj5vn{flex:1;font-size:var(--font-size-xs);font-weight:600;color:var(--text-muted);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em}.collapse-btn.svelte-owj5vn{background:none;border:none;color:var(--text-muted);font-size:1.1rem;cursor:pointer;padding:8px 10px;border-radius:4px;flex-shrink:0;line-height:1;font-family:var(--font-mono);min-width:36px;min-height:36px;display:flex;align-items:center;justify-content:center}.collapse-btn.svelte-owj5vn:hover{color:var(--text);background:var(--border)}.sidebar.collapsed.svelte-owj5vn .sidebar-header:where(.svelte-owj5vn){justify-content:center;padding:12px 4px}.icon-btn.svelte-owj5vn{background:none;border:none;color:var(--text);font-size:1.2rem;cursor:pointer;padding:4px 6px;border-radius:4px;touch-action:manipulation;display:none}.icon-btn.svelte-owj5vn:active{background:var(--border)}.home-btn.svelte-owj5vn{display:flex;align-items:center;gap:8px;padding:8px 10px;min-height:44px;cursor:pointer;border-left:3px solid transparent;transition:background .12s,border-color .12s;flex-shrink:0}.home-btn.svelte-owj5vn:hover{background:var(--surface-hover)}.home-btn--active.svelte-owj5vn{border-left-color:var(--accent);background:var(--surface-hover)}.home-icon.svelte-owj5vn{font-size:1rem;color:var(--text-muted);flex-shrink:0}.home-label.svelte-owj5vn{font-size:var(--font-size-sm);font-family:var(--font-mono);font-weight:600;color:var(--text)}.group-header.svelte-owj5vn{padding:10px 10px 4px;font-size:10px;font-family:var(--font-mono);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:1.5px}.workspace-list.svelte-owj5vn{flex:1;overflow-y:auto;overflow-x:hidden}.empty-state.svelte-owj5vn{padding:16px 10px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;text-align:center}.sidebar-footer-row.svelte-owj5vn{display:flex;gap:8px;margin:8px;align-items:stretch;flex-shrink:0}.add-workspace-btn.svelte-owj5vn{flex:1;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;transition:background .1s}.add-workspace-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.add-workspace-btn.svelte-owj5vn:active{background:var(--border)}.done-reorder-btn.svelte-owj5vn{margin:8px;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;flex-shrink:0;transition:background .1s}.done-reorder-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.done-reorder-btn.svelte-owj5vn:active{background:var(--border)}.settings-icon-btn.svelte-owj5vn{width:40px;min-height:40px;background:none;border:1px solid var(--border);border-radius:0;color:var(--text-muted);font-size:1rem;cursor:pointer;touch-action:manipulation;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .1s,color .1s}.settings-icon-btn.svelte-owj5vn:hover{background:var(--surface-hover);color:var(--text)}.settings-icon-btn.svelte-owj5vn:active{background:var(--border)}@media(max-width:600px){.sidebar.svelte-owj5vn{position:fixed;top:0;left:0;height:100%;transform:translate(-100%);box-shadow:2px 0 12px #00000080;transition:transform .25s ease}.sidebar.open.svelte-owj5vn{transform:translate(0)}.collapse-btn.svelte-owj5vn{display:none}.icon-btn.svelte-owj5vn{display:block;font-size:1.4rem;padding:4px 8px}.resize-handle.svelte-owj5vn{display:none}}/**
1
+ .pin-gate.svelte-1qp96yb{display:flex;align-items:center;justify-content:center;height:100vh;background:var(--bg);padding:1rem}.pin-container.svelte-1qp96yb{display:flex;flex-direction:column;align-items:center;gap:1rem;width:100%;max-width:320px;text-align:center}.pin-container.svelte-1qp96yb h1:where(.svelte-1qp96yb){font-size:1.5rem;color:var(--text)}.pin-container.svelte-1qp96yb p:where(.svelte-1qp96yb){color:var(--text-muted);font-size:.95rem}input.svelte-1qp96yb{width:100%;padding:14px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:1.2rem;text-align:center;outline:none;-webkit-appearance:none}input.svelte-1qp96yb:focus{border-color:var(--accent)}button.svelte-1qp96yb{width:100%;padding:14px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;touch-action:manipulation}button.svelte-1qp96yb:active{opacity:.8}.error.svelte-1qp96yb{color:var(--accent);font-size:.9rem}.context-menu-trigger.svelte-1nmhce7{background:none;border:none;color:var(--text-muted);font-size:1rem;font-weight:700;cursor:pointer;padding:0 6px;border-radius:4px;touch-action:manipulation;flex-shrink:0;line-height:1;letter-spacing:1px;min-height:24px;display:inline-flex;align-items:center;transition:color .15s,background .15s}.context-menu-trigger.svelte-1nmhce7:hover{color:var(--text);background:var(--border)}.context-menu-backdrop.svelte-1nmhce7{position:fixed;top:0;right:0;bottom:0;left:0;z-index:999}.context-menu.svelte-1nmhce7{position:fixed;list-style:none;margin:0;padding:4px 0;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px #0006;z-index:1000;min-width:175px}.context-menu-item.svelte-1nmhce7{padding:9px 14px;font-size:.85rem;cursor:pointer;color:var(--text);white-space:nowrap}.context-menu-item.svelte-1nmhce7:hover{background:var(--border)}.context-menu-item.svelte-1nmhce7:focus{outline:2px solid var(--accent);outline-offset:-2px}.context-menu-item--danger.svelte-1nmhce7{color:#e74c3c}.context-menu-item--danger.svelte-1nmhce7:hover{background:#e74c3c1f}.context-menu-item--disabled.svelte-1nmhce7{opacity:.4;cursor:default}.context-menu-item--disabled.svelte-1nmhce7:hover{background:none}.workspace-item.svelte-168i8d5{display:flex;flex-direction:column}.workspace-header.svelte-168i8d5{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer;min-height:44px;transition:background .12s}.workspace-header.svelte-168i8d5:hover{background:var(--surface-hover)}.workspace-item.active.svelte-168i8d5 .workspace-header:where(.svelte-168i8d5){background:var(--surface-hover)}.workspace-left.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.workspace-header.reorder-mode.svelte-168i8d5{cursor:grab}.workspace-header.reorder-mode.svelte-168i8d5:active{cursor:grabbing}.collapse-chevron.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:.7rem;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:color .12s}.collapse-chevron.svelte-168i8d5:hover{color:var(--text)}.collapse-count.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);background:var(--border);border-radius:8px;padding:1px 6px;flex-shrink:0}.initial-block.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;font-size:var(--font-size-xs);font-weight:700;color:#000;font-family:var(--font-mono);flex-shrink:0;line-height:1}.workspace-name.svelte-168i8d5{font-size:var(--font-size-sm);font-weight:700;color:var(--text);font-family:var(--font-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.workspace-actions.svelte-168i8d5{display:flex;align-items:center;gap:2px;opacity:0;transition:opacity .12s;flex-shrink:0}.workspace-header.svelte-168i8d5:hover .workspace-actions:where(.svelte-168i8d5){opacity:1}.action-btn.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:3px;font-size:var(--font-size-xs);color:var(--text-muted);cursor:pointer;font-family:var(--font-mono);transition:background .1s,color .1s}.action-btn.svelte-168i8d5:hover{background:var(--border);color:var(--text)}.session-list.svelte-168i8d5{list-style:none}.session-row.svelte-168i8d5{position:relative;display:flex;flex-direction:column;gap:2px;padding:6px 10px 6px 36px;cursor:pointer;min-height:44px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);transition:background .1s;border-left:3px solid transparent;justify-content:center}.session-row.svelte-168i8d5:hover{background:var(--surface-hover);color:var(--text)}.session-row.selected.svelte-168i8d5{border-left-color:var(--accent);background:var(--surface-hover);color:var(--text)}.session-row.attention.svelte-168i8d5 .session-name:where(.svelte-168i8d5){font-weight:700;color:var(--text)}.session-row-primary.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0}.session-row-secondary.svelte-168i8d5{display:flex;align-items:center;gap:6px;padding-left:15px;font-size:.65rem;color:var(--text-muted);min-width:0}.secondary-branch.svelte-168i8d5{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.row-menu-overlay.svelte-168i8d5{position:absolute;right:8px;top:0;bottom:0;display:flex;align-items:center;opacity:0;transition:opacity .12s}.session-row.svelte-168i8d5:hover .row-menu-overlay:where(.svelte-168i8d5){opacity:1}.secondary-pr.svelte-168i8d5{white-space:nowrap;flex-shrink:0;color:var(--accent)}.secondary-time.svelte-168i8d5{white-space:nowrap;flex-shrink:0;opacity:.7}.session-count-badge.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;border-radius:8px;background:var(--border);color:var(--text-muted);font-size:.55rem;font-family:var(--font-mono);font-weight:600;padding:0 4px;flex-shrink:0}.diff-badge.svelte-168i8d5{display:inline-flex;align-items:center;gap:4px;font-size:.6rem;flex-shrink:0;margin-left:auto}.diff-add.svelte-168i8d5{color:var(--status-success)}.diff-del.svelte-168i8d5{color:var(--status-error)}.status-dot.svelte-168i8d5{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.status-dot--running.svelte-168i8d5{background:var(--status-success)}.status-dot--idle.svelte-168i8d5{background:var(--status-info)}.dot-inactive.svelte-168i8d5{width:7px;height:7px;border-radius:50%;background:#555;flex-shrink:0}.session-row.inactive.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--text-muted)}.session-row.inactive.svelte-168i8d5:hover .session-name:where(.svelte-168i8d5){color:var(--text)}.session-row.loading.svelte-168i8d5{pointer-events:none;opacity:.7}.session-row.loading.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--accent)}.status-dot--attention.svelte-168i8d5{background:var(--status-warning);box-shadow:0 0 5px 1px #fbbf2473;animation:svelte-168i8d5-attention-glow 2s ease-in-out infinite}.status-dot--permission-prompt.svelte-168i8d5{background:#eab308;box-shadow:0 0 5px 1px #eab30873;animation:svelte-168i8d5-attention-glow 1.5s ease-in-out infinite}@keyframes svelte-168i8d5-attention-glow{0%,to{box-shadow:0 0 3px 1px #fbbf244d}50%{box-shadow:0 0 7px 2px #fbbf2499}}.session-name.svelte-168i8d5{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0}.session-name.bold.svelte-168i8d5{font-weight:700}.add-worktree-row.svelte-168i8d5{padding:4px 10px 6px 36px}.add-worktree-btn.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;cursor:pointer;transition:opacity .1s,color .1s}.add-worktree-btn.svelte-168i8d5:hover{opacity:1;color:var(--text)}.add-worktree-row.disabled.svelte-168i8d5{pointer-events:none}.add-worktree-row.disabled.svelte-168i8d5 .add-worktree-btn:where(.svelte-168i8d5){opacity:.7;color:var(--accent)}.workspace-divider.svelte-168i8d5{height:1px;background:var(--border);margin:0}@media(max-width:600px){.workspace-header.svelte-168i8d5,.session-row.svelte-168i8d5{min-height:48px}.workspace-actions.svelte-168i8d5{opacity:1}.row-menu-overlay.svelte-168i8d5{display:none}}.smart-search.svelte-itm1qs{position:relative;flex-shrink:0}.input-row.svelte-itm1qs{display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid var(--border);gap:6px}.prompt.svelte-itm1qs{font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--accent);flex-shrink:0;line-height:1;-webkit-user-select:none;user-select:none}.search-input.svelte-itm1qs{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-family:var(--font-mono);font-size:var(--font-size-sm);caret-color:var(--accent)}.search-input.svelte-itm1qs::placeholder{color:var(--text-muted);opacity:.5}.dropdown.svelte-itm1qs{position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-top:none;list-style:none;z-index:200;max-height:240px;overflow-y:auto}.dropdown-item.svelte-itm1qs{display:flex;align-items:baseline;gap:6px;padding:8px 10px;cursor:pointer;font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--text-muted);transition:background .1s;white-space:nowrap;overflow:hidden}.dropdown-item.svelte-itm1qs:hover,.dropdown-item.focused.svelte-itm1qs{background:var(--surface-hover);color:var(--text)}.dropdown-item.svelte-itm1qs strong:where(.svelte-itm1qs){color:var(--text);font-weight:700}.dropdown-path.svelte-itm1qs{font-size:var(--font-size-xs);color:var(--text-muted);opacity:.5;overflow:hidden;text-overflow:ellipsis;min-width:0;flex-shrink:1}@media(max-width:600px){.smart-search.svelte-itm1qs{width:100%}.input-row.svelte-itm1qs{width:100%;box-sizing:border-box}.search-input.svelte-itm1qs{width:100%;min-width:0}}.sidebar.svelte-owj5vn{position:relative;display:flex;flex-direction:column;background:var(--bg);border-right:1px solid var(--border);overflow:hidden;transition:transform .25s ease,width .2s ease,min-width .2s ease;z-index:100}.resize-handle.svelte-owj5vn{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;z-index:10;transition:background .15s}.resize-handle.svelte-owj5vn:hover{background:var(--accent)}.sidebar-header.svelte-owj5vn{display:flex;align-items:center;justify-content:space-between;padding:12px 10px;border-bottom:1px solid var(--border);flex-shrink:0}.sidebar-label.svelte-owj5vn{flex:1;font-size:var(--font-size-xs);font-weight:600;color:var(--text-muted);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em}.collapse-btn.svelte-owj5vn{background:none;border:none;color:var(--text-muted);font-size:1.1rem;cursor:pointer;padding:8px 10px;border-radius:4px;flex-shrink:0;line-height:1;font-family:var(--font-mono);min-width:36px;min-height:36px;display:flex;align-items:center;justify-content:center}.collapse-btn.svelte-owj5vn:hover{color:var(--text);background:var(--border)}.sidebar.collapsed.svelte-owj5vn .sidebar-header:where(.svelte-owj5vn){justify-content:center;padding:12px 4px}.icon-btn.svelte-owj5vn{background:none;border:none;color:var(--text);font-size:1.2rem;cursor:pointer;padding:4px 6px;border-radius:4px;touch-action:manipulation;display:none}.icon-btn.svelte-owj5vn:active{background:var(--border)}.home-btn.svelte-owj5vn{display:flex;align-items:center;gap:8px;padding:8px 10px;min-height:44px;cursor:pointer;border-left:3px solid transparent;transition:background .12s,border-color .12s;flex-shrink:0}.home-btn.svelte-owj5vn:hover{background:var(--surface-hover)}.home-btn--active.svelte-owj5vn{border-left-color:var(--accent);background:var(--surface-hover)}.home-icon.svelte-owj5vn{font-size:1rem;color:var(--text-muted);flex-shrink:0}.home-label.svelte-owj5vn{font-size:var(--font-size-sm);font-family:var(--font-mono);font-weight:600;color:var(--text)}.group-header.svelte-owj5vn{padding:10px 10px 4px;font-size:10px;font-family:var(--font-mono);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:1.5px}.workspace-list.svelte-owj5vn{flex:1;overflow-y:auto;overflow-x:hidden}.empty-state.svelte-owj5vn{padding:16px 10px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;text-align:center}.sidebar-footer-row.svelte-owj5vn{display:flex;gap:8px;margin:8px;align-items:stretch;flex-shrink:0}.add-workspace-btn.svelte-owj5vn{flex:1;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;transition:background .1s}.add-workspace-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.add-workspace-btn.svelte-owj5vn:active{background:var(--border)}.done-reorder-btn.svelte-owj5vn{margin:8px;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;flex-shrink:0;transition:background .1s}.done-reorder-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.done-reorder-btn.svelte-owj5vn:active{background:var(--border)}.settings-icon-btn.svelte-owj5vn{width:40px;min-height:40px;background:none;border:1px solid var(--border);border-radius:0;color:var(--text-muted);font-size:1rem;cursor:pointer;touch-action:manipulation;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .1s,color .1s}.settings-icon-btn.svelte-owj5vn:hover{background:var(--surface-hover);color:var(--text)}.settings-icon-btn.svelte-owj5vn:active{background:var(--border)}@media(max-width:600px){.sidebar.svelte-owj5vn{position:fixed;top:0;left:0;height:100%;transform:translate(-100%);box-shadow:2px 0 12px #00000080;transition:transform .25s ease}.sidebar.open.svelte-owj5vn{transform:translate(0)}.collapse-btn.svelte-owj5vn{display:none}.icon-btn.svelte-owj5vn{display:block;font-size:1.4rem;padding:4px 8px}.resize-handle.svelte-owj5vn{display:none}}/**
2
2
  * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
3
  * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
4
  * https://github.com/chjj/term.js
@@ -11,8 +11,8 @@
11
11
  <meta name="apple-mobile-web-app-capable" content="yes" />
12
12
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
13
13
  <meta name="theme-color" content="#1a1a1a" />
14
- <script type="module" crossorigin src="/assets/index-Dgf6cKGu.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-BTOnhJQN.css">
14
+ <script type="module" crossorigin src="/assets/index-BKUtECoE.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-C2q5VIv7.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
@@ -17,7 +17,7 @@ export function invalidateBranchLinkerCache() {
17
17
  */
18
18
  function extractTicketIds(branchName) {
19
19
  const ids = [];
20
- // Jira/Linear style: PROJECT-123 (2+ uppercase letters, dash, digits)
20
+ // Jira style: PROJECT-123 (2+ uppercase letters, dash, digits)
21
21
  // Skip "GH" prefix — that's our GitHub Issues namespace, handled separately below.
22
22
  const jiraRegex = /([A-Z]{2,}-\d+)/gi;
23
23
  let match;
@@ -83,12 +83,10 @@ export function createBranchLinkerRouter(deps) {
83
83
  if (ticketId.startsWith('GH-')) {
84
84
  source = 'github';
85
85
  }
86
- else if (process.env.JIRA_API_TOKEN) {
86
+ else {
87
+ // Non-GH ticket IDs (e.g., PROJ-123) assumed to be Jira
87
88
  source = 'jira';
88
89
  }
89
- else if (process.env.LINEAR_API_KEY) {
90
- source = 'linear';
91
- }
92
90
  links.push({
93
91
  ticketId,
94
92
  link: {
@@ -26,7 +26,6 @@ import { createBranchLinkerRouter, invalidateBranchLinkerCache } from './branch-
26
26
  import { createHooksRouter } from './hooks.js';
27
27
  import { createTicketTransitionsRouter } from './ticket-transitions.js';
28
28
  import { createIntegrationJiraRouter } from './integration-jira.js';
29
- import { createIntegrationLinearRouter } from './integration-linear.js';
30
29
  import { startPolling, stopPolling } from './review-poller.js';
31
30
  import { MOUNTAIN_NAMES } from './types.js';
32
31
  import { semverLessThan } from './utils.js';
@@ -282,9 +281,6 @@ async function main() {
282
281
  // Mount Jira integration router
283
282
  const integrationJiraRouter = createIntegrationJiraRouter({ configPath: CONFIG_PATH });
284
283
  app.use('/integration-jira', requireAuth, integrationJiraRouter);
285
- // Mount Linear integration router
286
- const integrationLinearRouter = createIntegrationLinearRouter({ configPath: CONFIG_PATH });
287
- app.use('/integration-linear', requireAuth, integrationLinearRouter);
288
284
  // Mount branch linker router
289
285
  const branchLinkerRouter = createBranchLinkerRouter({
290
286
  configPath: CONFIG_PATH,
@@ -753,8 +749,8 @@ async function main() {
753
749
  }
754
750
  if (ticketContext) {
755
751
  // Validate source is a known integration
756
- if (ticketContext.source !== 'github' && ticketContext.source !== 'jira' && ticketContext.source !== 'linear') {
757
- res.status(400).json({ error: "ticketContext.source must be 'github', 'jira', or 'linear'" });
752
+ if (ticketContext.source !== 'github' && ticketContext.source !== 'jira') {
753
+ res.status(400).json({ error: "ticketContext.source must be 'github' or 'jira'" });
758
754
  return;
759
755
  }
760
756
  // Validate repoPath is a configured workspace
@@ -763,25 +759,14 @@ async function main() {
763
759
  res.status(400).json({ error: 'ticketContext.repoPath is not a configured workspace' });
764
760
  return;
765
761
  }
766
- // Validate integration is configured for the claimed source
767
- if (ticketContext.source === 'jira') {
768
- if (!process.env['JIRA_API_TOKEN'] || !process.env['JIRA_EMAIL'] || !process.env['JIRA_BASE_URL']) {
769
- res.status(400).json({ error: 'Jira integration is not configured' });
770
- return;
771
- }
772
- }
773
- else if (ticketContext.source === 'linear') {
774
- if (!process.env['LINEAR_API_KEY']) {
775
- res.status(400).json({ error: 'Linear integration is not configured' });
776
- return;
777
- }
778
- }
762
+ // Jira integration is configured via acli CLI — no env var check needed.
763
+ // Auth validation happens when acli commands are actually called.
779
764
  // Validate ticket ID format per source
780
765
  if (ticketContext.source === 'github' && !/^GH-\d+$/.test(ticketContext.ticketId)) {
781
766
  res.status(400).json({ error: 'ticketContext.ticketId for github must match GH-<number>' });
782
767
  return;
783
768
  }
784
- if ((ticketContext.source === 'jira' || ticketContext.source === 'linear') && !/^[A-Z]+-\d+$/.test(ticketContext.ticketId)) {
769
+ if (ticketContext.source === 'jira' && !/^[A-Z][A-Z0-9]*-\d+$/.test(ticketContext.ticketId)) {
785
770
  res.status(400).json({ error: 'ticketContext.ticketId must match <PROJECT>-<number>' });
786
771
  return;
787
772
  }
@@ -1,4 +1,8 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
1
3
  import { Router } from 'express';
4
+ const execFileAsync = promisify(execFile);
5
+ const JIRA_TIMEOUT_MS = 10_000;
2
6
  const CACHE_TTL_MS = 60_000;
3
7
  const JIRA_ISSUES_CACHE_KEY = 'jira_issues';
4
8
  /**
@@ -7,47 +11,26 @@ const JIRA_ISSUES_CACHE_KEY = 'jira_issues';
7
11
  * Caller is responsible for mounting and applying auth middleware:
8
12
  * app.use('/integration-jira', requireAuth, createIntegrationJiraRouter({ configPath }));
9
13
  */
10
- export function createIntegrationJiraRouter(_deps) {
14
+ export function createIntegrationJiraRouter(deps) {
15
+ const exec = deps.execAsync ?? execFileAsync;
11
16
  const router = Router();
12
17
  // Single 60s in-memory cache (Jira is cross-workspace, not per-repo)
13
18
  const issuesCache = new Map();
14
- function getEnvVars() {
15
- const token = process.env.JIRA_API_TOKEN;
16
- const email = process.env.JIRA_EMAIL;
17
- const baseUrl = process.env.JIRA_BASE_URL;
18
- if (!token || !email || !baseUrl)
19
- return null;
20
- try {
21
- const parsed = new URL(baseUrl);
22
- const isHttps = parsed.protocol === 'https:';
23
- const isLocalHttp = parsed.protocol === 'http:' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1');
24
- if (!isHttps && !isLocalHttp) {
25
- console.warn('[integration-jira] JIRA_BASE_URL failed validation (must be https or http://localhost), treating as unconfigured');
26
- return null;
27
- }
19
+ // Cached site URL — resolved once per server lifetime
20
+ let cachedSiteUrl = null;
21
+ async function getSiteUrl() {
22
+ if (cachedSiteUrl !== null)
23
+ return cachedSiteUrl;
24
+ const { stdout } = await exec('acli', ['jira', 'auth', 'status'], { timeout: JIRA_TIMEOUT_MS });
25
+ const match = /Site:\s*([\w-]+\.atlassian\.net)/.exec(stdout);
26
+ if (!match || !match[1]) {
27
+ throw new Error('Could not parse site URL from acli jira auth status output');
28
28
  }
29
- catch {
30
- console.warn('[integration-jira] JIRA_BASE_URL is not a valid URL, treating as unconfigured');
31
- return null;
32
- }
33
- return { token, email, baseUrl };
34
- }
35
- function buildAuthHeader(email, token) {
36
- return `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
29
+ cachedSiteUrl = match[1];
30
+ return cachedSiteUrl;
37
31
  }
38
- // GET /integrations/jira/configured — returns whether env vars are set
39
- router.get('/configured', (_req, res) => {
40
- const env = getEnvVars();
41
- res.json({ configured: env !== null });
42
- });
43
32
  // GET /integrations/jira/issues — search issues assigned to currentUser
44
33
  router.get('/issues', async (_req, res) => {
45
- const env = getEnvVars();
46
- if (!env) {
47
- const response = { issues: [], error: 'jira_not_configured' };
48
- res.json(response);
49
- return;
50
- }
51
34
  const now = Date.now();
52
35
  // Return cached result if still fresh
53
36
  const cached = issuesCache.get(JIRA_ISSUES_CACHE_KEY);
@@ -56,119 +39,131 @@ export function createIntegrationJiraRouter(_deps) {
56
39
  res.json(response);
57
40
  return;
58
41
  }
59
- const jql = 'assignee=currentUser() AND status NOT IN (Done, Closed) ORDER BY updated DESC';
60
- const fields = 'summary,status,priority,customfield_10016,customfield_10020,assignee,updated';
61
- const url = `${env.baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&fields=${encodeURIComponent(fields)}&maxResults=50`;
62
- let data;
42
+ let siteUrl;
63
43
  try {
64
- const fetchResult = await Promise.allSettled([
65
- fetch(url, {
66
- headers: {
67
- Authorization: buildAuthHeader(env.email, env.token),
68
- Accept: 'application/json',
69
- },
70
- }),
71
- ]);
72
- const settled = fetchResult[0];
73
- if (settled.status === 'rejected') {
74
- const response = { issues: [], error: 'jira_fetch_failed' };
44
+ siteUrl = await getSiteUrl();
45
+ }
46
+ catch (err) {
47
+ const errCode = err.code;
48
+ if (errCode === 'ENOENT') {
49
+ const response = { issues: [], error: 'acli_not_in_path' };
75
50
  res.json(response);
76
51
  return;
77
52
  }
78
- const httpRes = settled.value;
79
- if (httpRes.status === 401 || httpRes.status === 403) {
80
- const response = { issues: [], error: 'jira_auth_failed' };
53
+ const stderr = err.stderr ?? '';
54
+ if (stderr.includes('not logged') || stderr.includes('auth') || stderr.includes('unauthorized')) {
55
+ const response = { issues: [], error: 'acli_not_authenticated' };
81
56
  res.json(response);
82
57
  return;
83
58
  }
84
- if (!httpRes.ok) {
85
- const response = { issues: [], error: 'jira_fetch_failed' };
59
+ const response = { issues: [], error: 'jira_fetch_failed' };
60
+ res.json(response);
61
+ return;
62
+ }
63
+ let stdout;
64
+ try {
65
+ ({ stdout } = await exec('acli', [
66
+ 'jira', 'workitem', 'search',
67
+ '--jql', 'assignee=currentUser() AND status NOT IN (Done, Closed) ORDER BY updated DESC',
68
+ '--json',
69
+ '--limit', '50',
70
+ ], { timeout: JIRA_TIMEOUT_MS }));
71
+ }
72
+ catch (err) {
73
+ const errCode = err.code;
74
+ if (errCode === 'ENOENT') {
75
+ const response = { issues: [], error: 'acli_not_in_path' };
86
76
  res.json(response);
87
77
  return;
88
78
  }
89
- data = (await httpRes.json());
79
+ const stderr = err.stderr ?? '';
80
+ if (stderr.includes('not logged') || stderr.includes('auth') || stderr.includes('unauthorized')) {
81
+ const response = { issues: [], error: 'acli_not_authenticated' };
82
+ res.json(response);
83
+ return;
84
+ }
85
+ const response = { issues: [], error: 'jira_fetch_failed' };
86
+ res.json(response);
87
+ return;
88
+ }
89
+ let items;
90
+ try {
91
+ items = JSON.parse(stdout);
90
92
  }
91
93
  catch {
92
94
  const response = { issues: [], error: 'jira_fetch_failed' };
93
95
  res.json(response);
94
96
  return;
95
97
  }
96
- const issues = data.issues.map((item) => {
97
- const projectKey = item.key.split('-')[0] ?? item.key;
98
- const sprint = item.fields.customfield_10020;
99
- const latestSprint = sprint && sprint.length > 0 ? sprint[sprint.length - 1]?.name ?? null : null;
100
- return {
101
- key: item.key,
102
- title: item.fields.summary,
103
- url: `${env.baseUrl}/browse/${item.key}`,
104
- status: item.fields.status.name,
105
- priority: item.fields.priority?.name ?? null,
106
- sprint: latestSprint,
107
- storyPoints: item.fields.customfield_10016,
108
- assignee: item.fields.assignee?.displayName ?? null,
109
- updatedAt: item.fields.updated,
110
- projectKey,
111
- };
112
- });
113
- // Sort by updatedAt descending
114
- issues.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
98
+ const issues = items.map((item) => ({
99
+ key: item.key,
100
+ title: item.fields.summary,
101
+ url: `https://${siteUrl}/browse/${item.key}`,
102
+ status: item.fields.status.name,
103
+ priority: item.fields.priority?.name ?? null,
104
+ assignee: item.fields.assignee?.displayName ?? null,
105
+ projectKey: item.key.split('-')[0] ?? item.key,
106
+ updatedAt: '',
107
+ sprint: null,
108
+ storyPoints: null,
109
+ }));
115
110
  // Update cache
116
111
  issuesCache.set(JIRA_ISSUES_CACHE_KEY, { issues, fetchedAt: now });
117
112
  const response = { issues };
118
113
  res.json(response);
119
114
  });
120
- // GET /integrations/jira/statuses?projectKey=X — fetch project statuses
115
+ // GET /integrations/jira/statuses?projectKey=X — fetch unique statuses for a project
121
116
  router.get('/statuses', async (req, res) => {
122
- const env = getEnvVars();
123
- if (!env) {
124
- res.json({ statuses: [], error: 'jira_not_configured' });
125
- return;
126
- }
127
117
  const projectKey = req.query['projectKey'];
128
118
  if (!projectKey || typeof projectKey !== 'string') {
129
119
  res.status(400).json({ statuses: [], error: 'missing_project_key' });
130
120
  return;
131
121
  }
132
- const url = `${env.baseUrl}/rest/api/3/project/${encodeURIComponent(projectKey)}/statuses`;
133
- let rawData;
122
+ // Sanitize: only allow [A-Z0-9]+ to prevent command injection
123
+ if (!/^[A-Z0-9]+$/.test(projectKey)) {
124
+ res.status(400).json({ statuses: [], error: 'invalid_project_key' });
125
+ return;
126
+ }
127
+ let stdout;
134
128
  try {
135
- const fetchResults = await Promise.allSettled([
136
- fetch(url, {
137
- headers: {
138
- Authorization: buildAuthHeader(env.email, env.token),
139
- Accept: 'application/json',
140
- },
141
- }),
142
- ]);
143
- const settled = fetchResults[0];
144
- if (settled.status === 'rejected') {
145
- res.json({ statuses: [], error: 'jira_fetch_failed' });
146
- return;
147
- }
148
- const httpRes = settled.value;
149
- if (httpRes.status === 401 || httpRes.status === 403) {
150
- res.json({ statuses: [], error: 'jira_auth_failed' });
129
+ ({ stdout } = await exec('acli', [
130
+ 'jira', 'workitem', 'search',
131
+ '--jql', `project = ${projectKey}`,
132
+ '--fields', 'status',
133
+ '--json',
134
+ '--limit', '50',
135
+ ], { timeout: JIRA_TIMEOUT_MS }));
136
+ }
137
+ catch (err) {
138
+ const errCode = err.code;
139
+ if (errCode === 'ENOENT') {
140
+ res.json({ statuses: [], error: 'acli_not_in_path' });
151
141
  return;
152
142
  }
153
- if (!httpRes.ok) {
154
- res.json({ statuses: [], error: 'jira_fetch_failed' });
143
+ const stderr = err.stderr ?? '';
144
+ if (stderr.includes('not logged') || stderr.includes('auth') || stderr.includes('unauthorized')) {
145
+ res.json({ statuses: [], error: 'acli_not_authenticated' });
155
146
  return;
156
147
  }
157
- rawData = (await httpRes.json());
148
+ res.json({ statuses: [], error: 'jira_fetch_failed' });
149
+ return;
150
+ }
151
+ let items;
152
+ try {
153
+ items = JSON.parse(stdout);
158
154
  }
159
155
  catch {
160
156
  res.json({ statuses: [], error: 'jira_fetch_failed' });
161
157
  return;
162
158
  }
163
- // Flatten statuses across all issue types and deduplicate by id
159
+ // Deduplicate statuses by id
164
160
  const seen = new Set();
165
161
  const statuses = [];
166
- for (const issueType of rawData) {
167
- for (const s of issueType.statuses) {
168
- if (!seen.has(s.id)) {
169
- seen.add(s.id);
170
- statuses.push({ id: s.id, name: s.name });
171
- }
162
+ for (const item of items) {
163
+ const { id, name } = item.fields.status;
164
+ if (!seen.has(id)) {
165
+ seen.add(id);
166
+ statuses.push({ id, name });
172
167
  }
173
168
  }
174
169
  res.json({ statuses });
@@ -32,44 +32,10 @@ async function removeLabel(exec, repoPath, issueNumber, label) {
32
32
  // Label may not exist — non-fatal
33
33
  }
34
34
  }
35
- /** Returns true if the URL is safe to use as a Jira base URL. */
36
- function isValidJiraUrl(url) {
35
+ /** Call a Jira transition by name via acli. Returns true on success, false on failure. */
36
+ async function jiraTransition(exec, ticketId, transitionName) {
37
37
  try {
38
- const parsed = new URL(url);
39
- if (parsed.protocol === 'https:')
40
- return true;
41
- if (parsed.protocol === 'http:' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'))
42
- return true;
43
- return false;
44
- }
45
- catch {
46
- return false;
47
- }
48
- }
49
- /** Call a Jira transition by ID. Returns true on success, false on failure. */
50
- async function jiraTransition(ticketId, transitionId) {
51
- const baseUrl = process.env.JIRA_BASE_URL;
52
- const email = process.env.JIRA_EMAIL;
53
- const token = process.env.JIRA_API_TOKEN;
54
- if (!baseUrl || !email || !token)
55
- return false;
56
- if (!isValidJiraUrl(baseUrl)) {
57
- console.warn(`[ticket-transitions] JIRA_BASE_URL failed validation, skipping transition for ${ticketId}`);
58
- return false;
59
- }
60
- try {
61
- const res = await fetch(`${baseUrl}/rest/api/3/issue/${encodeURIComponent(ticketId)}/transitions`, {
62
- method: 'POST',
63
- headers: {
64
- 'Authorization': `Basic ${Buffer.from(email + ':' + token).toString('base64')}`,
65
- 'Content-Type': 'application/json',
66
- },
67
- body: JSON.stringify({ transition: { id: transitionId } }),
68
- });
69
- if (!res.ok) {
70
- console.error(`[ticket-transitions] Jira transition returned ${res.status} for ${ticketId}`);
71
- return false;
72
- }
38
+ await exec('acli', ['jira', 'workitem', 'transition', '--key', ticketId, '--status', transitionName, '--yes'], { timeout: 10_000 });
73
39
  return true;
74
40
  }
75
41
  catch (err) {
@@ -77,54 +43,8 @@ async function jiraTransition(ticketId, transitionId) {
77
43
  return false;
78
44
  }
79
45
  }
80
- /** Update a Linear issue state. Returns true on success, false on failure. */
81
- async function linearStateUpdate(ticketIdentifier, stateId) {
82
- const apiKey = process.env.LINEAR_API_KEY;
83
- if (!apiKey)
84
- return false;
85
- // Linear mutations need the issue ID, but we only have the identifier (e.g. "TEAM-123").
86
- // Resolve the issue ID by identifier, then update state.
87
- try {
88
- const searchRes = await fetch('https://api.linear.app/graphql', {
89
- method: 'POST',
90
- headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
91
- body: JSON.stringify({
92
- query: `query($filter: IssueFilter) { issues(filter: $filter, first: 1) { nodes { id } } }`,
93
- variables: { filter: { identifier: { eq: ticketIdentifier } } },
94
- }),
95
- });
96
- if (!searchRes.ok) {
97
- console.error(`[ticket-transitions] Linear issue lookup returned ${searchRes.status} for ${ticketIdentifier}`);
98
- return false;
99
- }
100
- const searchData = (await searchRes.json());
101
- const issueId = searchData.data?.issues?.nodes?.[0]?.id;
102
- if (!issueId)
103
- return false;
104
- const updateRes = await fetch('https://api.linear.app/graphql', {
105
- method: 'POST',
106
- headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
107
- body: JSON.stringify({
108
- query: `mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }`,
109
- variables: { id: issueId, stateId },
110
- }),
111
- });
112
- if (!updateRes.ok) {
113
- console.error(`[ticket-transitions] Linear state update returned ${updateRes.status} for ${ticketIdentifier}`);
114
- return false;
115
- }
116
- return true;
117
- }
118
- catch (err) {
119
- console.error(`[ticket-transitions] Linear state update failed for ${ticketIdentifier}:`, err);
120
- return false;
121
- }
122
- }
123
46
  /**
124
47
  * Best-effort source detection from a ticket ID pattern.
125
- * Known limitation: when both Jira and Linear are configured, non-GH tickets
126
- * are matched by whichever env var is present. This is imperfect — a future
127
- * improvement would persist the source alongside branch links.
128
48
  */
129
49
  function detectTicketSource(ticketId, links) {
130
50
  // Use explicit source from branch link if available
@@ -135,14 +55,9 @@ function detectTicketSource(ticketId, links) {
135
55
  }
136
56
  if (ticketId.startsWith('GH-'))
137
57
  return 'github';
138
- // Prefer Jira for PROJECT-style keys (>= 3 uppercase letters before dash)
139
- // since Jira project keys are typically longer than Linear team keys (2-3 chars).
58
+ // Prefer Jira for PROJECT-style keys (>= 3 uppercase letters before dash).
140
59
  const prefix = ticketId.split('-')[0] ?? '';
141
- if (prefix.length >= 3 && process.env.JIRA_API_TOKEN)
142
- return 'jira';
143
- if (process.env.LINEAR_API_KEY)
144
- return 'linear';
145
- if (process.env.JIRA_API_TOKEN)
60
+ if (prefix.length >= 3)
146
61
  return 'jira';
147
62
  return 'github'; // fallback
148
63
  }
@@ -152,11 +67,9 @@ export function createTicketTransitionsRouter(deps) {
152
67
  const exec = deps.execAsync ?? execFileAsync;
153
68
  const { configPath } = deps;
154
69
  const router = Router();
155
- /** Get status mapping for a transition state from config */
156
- function getStatusMapping(config, source, state) {
157
- if (source === 'jira')
158
- return config.integrations?.jira?.statusMappings?.[state];
159
- return config.integrations?.linear?.statusMappings?.[state];
70
+ /** Get Jira status mapping for a transition state from config */
71
+ function getJiraStatusMapping(config, state) {
72
+ return config.integrations?.jira?.statusMappings?.[state];
160
73
  }
161
74
  async function transitionOnSessionCreate(ctx) {
162
75
  const current = transitionMap.get(ctx.ticketId);
@@ -172,18 +85,9 @@ export function createTicketTransitionsRouter(deps) {
172
85
  }
173
86
  else if (ctx.source === 'jira') {
174
87
  const config = loadConfig(configPath);
175
- const transitionId = getStatusMapping(config, 'jira', 'in-progress');
176
- if (transitionId) {
177
- const ok = await jiraTransition(ctx.ticketId, transitionId);
178
- if (ok)
179
- transitionMap.set(ctx.ticketId, 'in-progress');
180
- }
181
- }
182
- else if (ctx.source === 'linear') {
183
- const config = loadConfig(configPath);
184
- const stateId = getStatusMapping(config, 'linear', 'in-progress');
185
- if (stateId) {
186
- const ok = await linearStateUpdate(ctx.ticketId, stateId);
88
+ const transitionName = getJiraStatusMapping(config, 'in-progress');
89
+ if (transitionName) {
90
+ const ok = await jiraTransition(exec, ctx.ticketId, transitionName);
187
91
  if (ok)
188
92
  transitionMap.set(ctx.ticketId, 'in-progress');
189
93
  }
@@ -212,17 +116,9 @@ export function createTicketTransitionsRouter(deps) {
212
116
  transitionMap.set(ticketId, 'code-review');
213
117
  }
214
118
  else if (source === 'jira') {
215
- const transitionId = getStatusMapping(config, 'jira', 'code-review');
216
- if (transitionId) {
217
- const ok = await jiraTransition(ticketId, transitionId);
218
- if (ok)
219
- transitionMap.set(ticketId, 'code-review');
220
- }
221
- }
222
- else if (source === 'linear') {
223
- const stateId = getStatusMapping(config, 'linear', 'code-review');
224
- if (stateId) {
225
- const ok = await linearStateUpdate(ticketId, stateId);
119
+ const transitionName = getJiraStatusMapping(config, 'code-review');
120
+ if (transitionName) {
121
+ const ok = await jiraTransition(exec, ticketId, transitionName);
226
122
  if (ok)
227
123
  transitionMap.set(ticketId, 'code-review');
228
124
  }
@@ -242,17 +138,9 @@ export function createTicketTransitionsRouter(deps) {
242
138
  transitionMap.set(ticketId, 'ready-for-qa');
243
139
  }
244
140
  else if (source === 'jira') {
245
- const transitionId = getStatusMapping(config, 'jira', 'ready-for-qa');
246
- if (transitionId) {
247
- const ok = await jiraTransition(ticketId, transitionId);
248
- if (ok)
249
- transitionMap.set(ticketId, 'ready-for-qa');
250
- }
251
- }
252
- else if (source === 'linear') {
253
- const stateId = getStatusMapping(config, 'linear', 'ready-for-qa');
254
- if (stateId) {
255
- const ok = await linearStateUpdate(ticketId, stateId);
141
+ const transitionName = getJiraStatusMapping(config, 'ready-for-qa');
142
+ if (transitionName) {
143
+ const ok = await jiraTransition(exec, ticketId, transitionName);
256
144
  if (ok)
257
145
  transitionMap.set(ticketId, 'ready-for-qa');
258
146
  }