Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/cli/skills/research/initialize-atlas-graph/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Run this skill when:
atlas doctor --format=json
```
If it reports unavailable/unauthenticated, tell the user to run
`openscience connect login` — the graph cannot be created without a session.
`openscience login` — the graph cannot be created without a session.

2. **Create or link the graph** (idempotent — safe to re-run; returns the
existing graph if one already exists):
Expand All @@ -41,7 +41,7 @@ Run this skill when:
`status`, `message` when known). Relay the fix that matches the kind — do
NOT guess or tell the user to re-login for a network problem:
- `"unauthenticated"` — no session, or the backend rejected the saved key.
Tell the user to run `openscience connect login`.
Tell the user to run `openscience login`.
- `"unreachable"` — the Atlas backend at the printed `host` could not be
reached (network/DNS error or 5xx). The user IS logged in; suggest checking
connectivity and any `OPENSCIENCE_API_BASE`/`SYNSC_API_BASE` override, then
Expand Down
2 changes: 1 addition & 1 deletion backend/cli/src/agent/prompt/research.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ At the START of the task, before Stage 1, set up the graph:
even if `atlas` is unavailable. On success it prints `{"project_id":"<id>"}` and writes
`.openscience/project.json`; note the project_id. ONLY skip Atlas if it prints
`project_id: null` with an `error` kind — relay the matching fix (`unauthenticated` →
`openscience connect login`; `plan` → app.syntheticsciences.ai/cli; `unreachable`/`backend`
`openscience login`; `plan` → app.syntheticsciences.ai/cli; `unreachable`/`backend`
→ show the message) and continue the work. (Or load the `initialize-atlas-graph` skill,
which wraps this exact command.)
2. **Load prior graph state — optional, best-effort.** Run `atlas doctor --format=json`. If it
Expand Down
81 changes: 81 additions & 0 deletions backend/cli/src/cli/cmd/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,87 @@ export const AuthCodexCommand = cmd({
},
})

/** Best-effort server-side disconnect of the Codex credential, mirroring
* pushTokensToBackend's POST. Runs BEFORE the local removal so the thk_ key can
* still authenticate the call. Never throws — the local Auth.remove is what
* actually signs the CLI out. */
async function disconnectCodexBackend(): Promise<void> {
const session = await OpenScience.getSession?.()
const thkToken = session?.api_key
if (!thkToken) return
try {
await fetch(`${managedApiBase()}/api/keys/openai-codex`, {
method: "DELETE",
headers: { Authorization: `Bearer ${thkToken}` },
})
} catch {
/* best-effort — local removal below is the source of truth */
}
}

/** `openscience connect [codex]` — sign in with a ChatGPT (Codex) subscription.
* The connect/disconnect verb pair is Codex's; Atlas uses login/logout. */
export const ConnectCommand = cmd({
command: "connect [service]",
describe: "connect a ChatGPT subscription (Codex) — sign in with ChatGPT",
builder: (yargs) =>
yargs.positional("service", {
type: "string",
choices: ["codex"] as const,
default: "codex",
describe: "service to connect (only `codex` today)",
}),
async handler() {
await Instance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
prompts.intro("Connect ChatGPT (Codex)")
const handled = await runCodexAuthFlow()
if (!handled) prompts.outro("Done")
},
})
},
})

/** `openscience disconnect [codex]` — sign out of ChatGPT (Codex): clears the
* local OAuth credential and best-effort revokes it server-side. */
export const DisconnectCommand = cmd({
command: "disconnect [service]",
describe: "disconnect your ChatGPT subscription (Codex)",
builder: (yargs) =>
yargs.positional("service", {
type: "string",
choices: ["codex"] as const,
default: "codex",
describe: "service to disconnect (only `codex` today)",
}),
async handler() {
await Instance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
prompts.intro("Disconnect ChatGPT (Codex)")

const existing = await Auth.get("openai-codex")
const backend = await backendHasCodex()
if (!existing && backend !== true) {
prompts.log.warn("ChatGPT (Codex) isn't connected.")
prompts.outro("Done")
return
}

// Revoke server-side while the thk_ key can still authenticate, then
// drop the local credential (the part that actually signs the CLI out).
await disconnectCodexBackend()
await Auth.remove("openai-codex")
prompts.log.success("Disconnected ChatGPT (Codex)")
prompts.outro("Done")
},
})
},
})

export const AuthLogoutCommand = cmd({
command: ["remove", "rm", "logout"],
describe: "remove a saved provider key",
Expand Down
20 changes: 5 additions & 15 deletions backend/cli/src/cli/cmd/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,18 +303,8 @@ export const DevicesCommand = cmd({
},
})

/** Back-compat umbrella: `openscience connect <login|logout|status|sync|devices>`
* still works and forwards to the promoted top-level commands. */
export const ConnectCommand = cmd({
command: "connect",
describe: "Atlas account (alias for `login` / `logout` / `status` / `sync` / `devices`)",
builder: (yargs) =>
yargs
.command(LoginCommand)
.command(LogoutCommand)
.command(StatusCommand)
.command(SyncCommand)
.command(DevicesCommand)
.demandCommand(),
async handler() {},
})
// Atlas is `login` / `logout` (+ `status` / `sync` / `devices`), all top-level.
// The `connect` / `disconnect` verbs now belong to Codex (see cli/cmd/auth.ts):
// `connect` signs in with ChatGPT, `disconnect` signs out. Keeping the two
// account types on distinct verb pairs removes the old ambiguity where
// `connect login` and `login` both meant the Atlas account.
2 changes: 1 addition & 1 deletion backend/cli/src/cli/cmd/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ const SkillListCommand = cmd({
UI.println(
"Only a small bundled skill set is visible; this usually means the full OpenScience skill index was not fetched.",
)
UI.println("Run `openscience connect sync` or try again once online/authenticated.")
UI.println("Run `openscience sync` or try again once online/authenticated.")
UI.println("")
} else if (!showAll) {
UI.println("Use `openscience skill list --all` to show bundled OpenScience skills.")
Expand Down
12 changes: 3 additions & 9 deletions backend/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,10 @@ import { EOL } from "os"
import { WebCommand } from "./cli/cmd/web"
import { PrCommand } from "./cli/cmd/pr"
import { SessionCommand } from "./cli/cmd/session"
import {
ConnectCommand,
LoginCommand,
LogoutCommand,
StatusCommand,
SyncCommand,
DevicesCommand,
} from "./cli/cmd/connect"
import { LoginCommand, LogoutCommand, StatusCommand, SyncCommand, DevicesCommand } from "./cli/cmd/connect"
import { ProjectCommand } from "./cli/cmd/project"
import { WalletCommand } from "./cli/cmd/billing"
import { KeysCommand } from "./cli/cmd/auth"
import { KeysCommand, ConnectCommand, DisconnectCommand } from "./cli/cmd/auth"
import { InitCommand, DoctorCommand } from "./cli/onboard"
import { OpenScience } from "./openscience"

Expand Down Expand Up @@ -145,6 +138,7 @@ const cli = yargs(hideBin(process.argv))
.command(DoctorCommand)
.command(ProjectCommand)
.command(ConnectCommand)
.command(DisconnectCommand)
.fail((msg, err) => {
if (
msg?.startsWith("Unknown argument") ||
Expand Down
4 changes: 2 additions & 2 deletions backend/cli/src/openscience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ if (API_BASE !== DEFAULT_API_BASE) {
}
}

// User-facing URL the CLI prints during `openscience connect login`. Defaults
// User-facing URL the CLI prints during `openscience login`. Defaults
// to the unified Atlas frontend's /cli route — Plan tab, key management,
// and billing all live there. SYNSC_AUTH_URL overrides (e.g. point at a
// staging frontend or the old auth.syntheticsciences.ai surface).
Expand Down Expand Up @@ -514,7 +514,7 @@ export namespace OpenScience {
} catch {}
// Union of what this process synced (in-memory map) and what the last
// sync persisted (disk snapshot, replayed by preload-env.ts at boot) —
// a fresh `connect logout` process has only the latter.
// a fresh `logout` process has only the latter.
const synced = await readSyncedSnapshot()
for (const [key, value] of syncedSecretValues.entries()) synced.set(key, value)
for (const name of ["synced-env.json", "openscience-synced.json"]) {
Expand Down
2 changes: 1 addition & 1 deletion backend/cli/src/openscience/preload-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Runs BEFORE any provider SDK construction (Anthropic, OpenAI,
* @ai-sdk/google) so those SDKs see the correct ANTHROPIC_BASE_URL /
* ANTHROPIC_API_KEY / etc. on their own startup, without waiting for
* the asynchronous `openscience connect sync` call later in CLI boot.
* the asynchronous `openscience sync` call later in CLI boot.
*
* Without this, the first invocation after a fresh terminal session
* would race: sync sets process.env in-process, but the SDK had
Expand Down
2 changes: 1 addition & 1 deletion backend/cli/src/plugin/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
})
// Re-sync after backend now knows about the new codex
// credential, so `openai-codex` shows up in the local
// provider list without a separate `openscience connect sync`.
// provider list without a separate `openscience sync`.
await OpenScience.syncServices?.().catch((e: unknown) => {
log.warn("post-codex-login sync failed", { error: String(e) })
})
Expand Down
2 changes: 1 addition & 1 deletion backend/cli/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export namespace Provider {
if (isAtlasProxyBaseURL(options["baseURL"])) return
throw new Error(
`${provider.id} is using a managed Atlas key without an Atlas proxy URL. ` +
"Run `openscience connect sync` and try again.",
"Run `openscience sync` and try again.",
)
}

Expand Down
2 changes: 1 addition & 1 deletion backend/cli/src/server/routes/atlas-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ export async function initProjectDetailed(directory: string): Promise<InitProjec
if (!directory)
return { projectId: null, failure: { kind: "backend", message: "no directory provided", host: API_BASE } }
// Fail fast offline: no managed session means no request can succeed —
// don't turn a missing `openscience connect login` into a network error.
// don't turn a missing `openscience login` into a network error.
if (!(await token())) return { projectId: null, failure: { kind: "unauthenticated", host: API_BASE } }
const existing = await resolveProjectId(directory)
if (existing) return { projectId: existing }
Expand Down
4 changes: 2 additions & 2 deletions backend/cli/src/skill/system/initialize-atlas-graph.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Run this skill when:
atlas doctor --format=json
```
If it reports unavailable/unauthenticated, tell the user to run
`openscience connect login` — the graph cannot be created without a session.
`openscience login` — the graph cannot be created without a session.

2. **Create or link the graph** (idempotent — safe to re-run; returns the
existing graph if one already exists):
Expand All @@ -41,7 +41,7 @@ Run this skill when:
`status`, `message` when known). Relay the fix that matches the kind — do
NOT guess or tell the user to re-login for a network problem:
- `"unauthenticated"` — no session, or the backend rejected the saved key.
Tell the user to run `openscience connect login`.
Tell the user to run `openscience login`.
- `"unreachable"` — the Atlas backend at the printed `host` could not be
reached (network/DNS error or 5xx). The user IS logged in; suggest checking
connectivity and any `OPENSCIENCE_API_BASE`/`SYNSC_API_BASE` override, then
Expand Down
2 changes: 1 addition & 1 deletion frontend/workspace/vite-thesis.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function sessionToken() {

async function atlasFetch(method, path, body) {
const token = sessionToken()
if (!token) throw new Error("not logged in — run `openscience connect login`")
if (!token) throw new Error("not logged in — run `openscience login`")
const res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
Expand Down
Loading