pipeline-moe 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipeline-moe",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Local-first multi-agent chat room — N stateful pi agent sessions over a shared workspace, with terminal and web clients.",
6
6
  "license": "MIT",
package/src/server.ts CHANGED
@@ -717,6 +717,15 @@ async function main(): Promise<void> {
717
717
  }
718
718
  })
719
719
 
720
+ // Pending manual inputs for in-flight OAuth flows (pasted redirect URL or
721
+ // authorization code), keyed by provider. Resolved by the /login/input route,
722
+ // rejected by DELETE /login (cancel) — rejecting is what makes pi's flow
723
+ // abort and release the localhost callback port instead of waiting forever.
724
+ const pendingOAuthInputs = new Map<
725
+ string,
726
+ { resolve: (value: string) => void; reject: (err: Error) => void }
727
+ >()
728
+
720
729
  // Start OAuth login flow for a provider (device-code or auth URL).
721
730
  app.post("/api/providers/:name/login", async (req, res) => {
722
731
  const name = req.params.name
@@ -731,9 +740,22 @@ async function main(): Promise<void> {
731
740
  return
732
741
  }
733
742
 
743
+ // Retrying while a previous flow for this provider is still waiting would
744
+ // collide on pi's fixed callback port (EADDRINUSE) — abort the old flow
745
+ // first so a retry always gets a fresh start.
746
+ const stale = pendingOAuthInputs.get(name)
747
+ if (stale) {
748
+ pendingOAuthInputs.delete(name)
749
+ stale.reject(new Error("Login restarted"))
750
+ await new Promise((r) => setTimeout(r, 100)) // let the old flow release the port
751
+ }
752
+
734
753
  // Start the login flow in the background — communicate progress via SSE.
735
754
  // Return 202 immediately so the client can wait for SSE events.
736
755
  const providerName = name // capture for closure
756
+ // Identity of THIS attempt's pending entry: the finally below must only
757
+ // clear the map when the entry is still ours, never a newer attempt's.
758
+ let myEntry: { resolve: (value: string) => void; reject: (err: Error) => void } | undefined
737
759
  setImmediate(async () => {
738
760
  try {
739
761
  await resolved.authStorage.login(name, {
@@ -760,14 +782,28 @@ async function main(): Promise<void> {
760
782
  message,
761
783
  })
762
784
  },
785
+ // Manual completion path: providers like Anthropic race a localhost
786
+ // callback server against a pasted redirect URL. Without this
787
+ // callback the flow waits on the callback forever when the browser
788
+ // is on another machine. The promise resolves when a client POSTs
789
+ // /login/input; cleanup happens in the outer finally.
790
+ onManualCodeInput: () =>
791
+ new Promise<string>((resolve, reject) => {
792
+ myEntry = { resolve, reject }
793
+ pendingOAuthInputs.set(providerName, myEntry)
794
+ }),
763
795
  onPrompt: async (prompt) => {
764
- // For headless: reject prompts that require user input
796
+ // Ask the clients for input over SSE and wait for /login/input.
765
797
  hub.broadcast("oauth_progress", {
766
798
  provider: providerName,
767
- type: "error",
768
- message: `OAuth requires interactive prompt: "${prompt.message}" — use the pi CLI for this provider.`,
799
+ type: "prompt",
800
+ message: prompt.message,
801
+ placeholder: (prompt as { placeholder?: string }).placeholder,
802
+ })
803
+ return new Promise<string>((resolve, reject) => {
804
+ myEntry = { resolve, reject }
805
+ pendingOAuthInputs.set(providerName, myEntry)
769
806
  })
770
- throw new Error("interactive prompt not supported in headless mode")
771
807
  },
772
808
  onSelect: async () => {
773
809
  hub.broadcast("oauth_progress", {
@@ -797,12 +833,49 @@ async function main(): Promise<void> {
797
833
  type: "error",
798
834
  message: msg,
799
835
  })
836
+ } finally {
837
+ if (myEntry && pendingOAuthInputs.get(providerName) === myEntry) {
838
+ pendingOAuthInputs.delete(providerName)
839
+ }
800
840
  }
801
841
  })
802
842
 
803
843
  res.status(202).json({ accepted: true, provider: name })
804
844
  })
805
845
 
846
+ // Feed manual input (pasted redirect URL / authorization code) into an
847
+ // in-flight OAuth login for this provider.
848
+ app.post("/api/providers/:name/login/input", (req, res) => {
849
+ const name = req.params.name
850
+ const resolveInput = pendingOAuthInputs.get(name)
851
+ if (!resolveInput) {
852
+ res.status(404).json({ error: `no OAuth flow waiting for input for "${name}"` })
853
+ return
854
+ }
855
+ const value = String((req.body as { value?: unknown } | undefined)?.value ?? "").trim()
856
+ if (!value) {
857
+ res.status(400).json({ error: "value is required" })
858
+ return
859
+ }
860
+ pendingOAuthInputs.delete(name)
861
+ resolveInput.resolve(value)
862
+ res.json({ ok: true })
863
+ })
864
+
865
+ // Cancel an in-flight OAuth login: rejecting the pending input makes pi's
866
+ // flow abort and release its localhost callback port.
867
+ app.delete("/api/providers/:name/login", (req, res) => {
868
+ const name = req.params.name
869
+ const pending = pendingOAuthInputs.get(name)
870
+ if (!pending) {
871
+ res.status(404).json({ error: `no OAuth flow in flight for "${name}"` })
872
+ return
873
+ }
874
+ pendingOAuthInputs.delete(name)
875
+ pending.reject(new Error("Login cancelled"))
876
+ res.json({ ok: true })
877
+ })
878
+
806
879
  // Remove credentials for a provider.
807
880
  app.delete("/api/providers/:name", async (req, res) => {
808
881
  const name = req.params.name