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 bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@ellipsis-dev/sdk": "^0.6.0",
"@ellipsis-dev/sdk": "^0.8.1",
"chalk": "^5.6.2",
"cli-table3": "^0.6.5",
"commander": "^12.1.0",
Expand Down
8 changes: 4 additions & 4 deletions src/commands/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Command } from 'commander'
import { InvalidArgumentError } from 'commander'
import { ApiClient } from '../lib/api'
import { api } from '../lib/api'
import { collect, parseWhen, toInt } from '../lib/args'
import { alsoKnownAs, apiRoutes } from '../lib/help'
import { printJson, printTable, runAction } from '../lib/output'
Expand Down Expand Up @@ -109,7 +109,7 @@ export function registerAnalytics(program: Command): void {
json?: boolean
}) => {
await runAction(async () => {
const res = await new ApiClient().getAnalyticsMetrics({
const res = await api().analytics.metrics({
...windowQuery(opts),
repo: opts.repo.length > 0 ? opts.repo : undefined,
account_type: opts.accountType,
Expand Down Expand Up @@ -179,7 +179,7 @@ export function registerAnalytics(program: Command): void {
: opts.accountType === 'bot'
? ['Bot']
: undefined
const res = await new ApiClient().getAnalyticsPullRequests({
const res = await api().analytics.pullRequests({
...windowQuery(opts),
account_type: accountTypes,
status: opts.status.length > 0 ? opts.status : undefined,
Expand Down Expand Up @@ -249,7 +249,7 @@ export function registerAnalytics(program: Command): void {
json?: boolean
}) => {
await runAction(async () => {
const res = await new ApiClient().getAnalyticsReviews({
const res = await api().analytics.reviews({
...windowQuery(opts),
repo: opts.repo.length > 0 ? opts.repo : undefined,
author: opts.author.length > 0 ? opts.author : undefined,
Expand Down
28 changes: 16 additions & 12 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { type Command } from 'commander'
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { basename, dirname, extname } from 'node:path'
import { ApiClient } from '../lib/api'
import { api } from '../lib/api'
import { resolveAppBase } from '../lib/config'
import { alsoKnownAs, apiRoutes } from '../lib/help'
import { repoFromCwd } from '../lib/laptop'
import { formatTs, printJson, printTable, printYaml, runAction } from '../lib/output'
import { configUrl } from '../lib/urls'
import { readConfigFile } from './session'
import type {
AgentConfig,
AgentDefaultView,
CreateAgentConfigRequest,
SavedAgentConfig,
Expand All @@ -34,7 +35,7 @@ export function registerConfig(program: Command): void {
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
await runAction(async () => {
const configs = await new ApiClient().listAgentConfigs()
const { configs } = await api().agents.configs.list()
if (opts.json) {
printJson(configs)
return
Expand Down Expand Up @@ -64,15 +65,18 @@ export function registerConfig(program: Command): void {
.option('--json', 'output raw JSON')
.action(async (configId: string, opts: { json?: boolean }) => {
await runAction(async () => {
const api = new ApiClient()
const client = api()
// --json is the machine-readable mode: emit only the raw config.
if (opts.json) {
printJson(await api.getAgentConfig(configId))
printJson((await client.agents.configs.get(configId)).config)
return
}
// Fetch the config and the login (for the link) together. The link goes
// to stderr so the YAML on stdout stays clean for piping/redirecting.
const [c, me] = await Promise.all([api.getAgentConfig(configId), api.whoami()])
const [{ config: c }, me] = await Promise.all([
client.agents.configs.get(configId),
client.me(),
])
printYaml(c)
console.error(`\nview: ${configUrl(resolveAppBase(), me.customer_login, configId)}`)
})
Expand Down Expand Up @@ -119,9 +123,9 @@ export function registerConfig(program: Command): void {
repository: opts.repo,
path: opts.path,
}
if (opts.file) req.config = readConfigFile(opts.file)
if (opts.file) req.config = readConfigFile(opts.file) as AgentConfig
if (opts.template) req.template_id = opts.template
const created = await new ApiClient().createAgentConfig(req)
const created = await api().agents.configs.create(req)
if (opts.json) {
printJson(created)
return
Expand Down Expand Up @@ -156,7 +160,7 @@ export function registerConfig(program: Command): void {
// (the same ladder session start resolves server-side).
.action(async (opts: { json?: boolean }) => {
await runAction(async () => {
const rungs = await new ApiClient().listAgentDefaults()
const { defaults: rungs } = await api().agents.defaults.list()
const repo = repoFromCwd(process.cwd())
const repoRung = repo
? rungs.find((d) => d.repository?.toLowerCase() === repo.toLowerCase())
Expand Down Expand Up @@ -197,7 +201,7 @@ export function registerConfig(program: Command): void {
// merged view, not just this command's own opts.
.action(async (_opts: { json?: boolean }, cmd: Command) => {
await runAction(async () => {
const rungs = await new ApiClient().listAgentDefaults()
const { defaults: rungs } = await api().agents.defaults.list()
if (cmd.optsWithGlobals().json) {
printJson(rungs)
return
Expand Down Expand Up @@ -234,7 +238,7 @@ export function registerConfig(program: Command): void {
async (configId: string, opts: { repo?: string | boolean; json?: boolean }, cmd: Command) => {
await runAction(async () => {
const repository = resolveRepoFlag(opts.repo)
const set = await new ApiClient().putAgentDefault({
const { default: set } = await api().agents.defaults.set({
config_id: configId,
...(repository ? { repository } : {}),
})
Expand Down Expand Up @@ -265,7 +269,7 @@ export function registerConfig(program: Command): void {
.action(async (opts: { repo?: string | boolean }) => {
await runAction(async () => {
const repository = resolveRepoFlag(opts.repo)
await new ApiClient().deleteAgentDefault(repository)
await api().agents.defaults.delete({ repository })
console.log(
`✓ cleared ${repository ? `default for ${repository}` : 'account default'}`,
)
Expand Down Expand Up @@ -309,7 +313,7 @@ export function registerConfig(program: Command): void {
return
}
await runAction(async () => {
const created = await new ApiClient().createAgentConfig({
const created = await api().agents.configs.create({
template_id: opts.template,
repository: opts.repo!,
path: opts.path,
Expand Down
15 changes: 9 additions & 6 deletions src/commands/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react'
import { render } from 'ink'
import { SessionTranscriptStore } from '@ellipsis-dev/sdk/store'
import { SESSION_STREAM_PROTOCOL_VERSION } from '@ellipsis-dev/sdk/stream'
import { ApiClient } from '../lib/api'
import { api } from '../lib/api'
import { requireToken, resolveApiBase, resolveAppBase } from '../lib/config'
import { runAction } from '../lib/output'
import { sessionUrl } from '../lib/urls'
Expand Down Expand Up @@ -92,11 +92,14 @@ export async function runConnect(
// from the fetched session. Shown in the footer meta line.
configName?: string,
): Promise<void> {
const api = new ApiClient()
const client = api()
const token = requireToken()
const openSocket = makeOpenSocket(token, resolveWsBase(resolveApiBase()))

const [session, me] = await Promise.all([api.getAgentSession(sessionId), api.whoami()])
const [{ session }, me] = await Promise.all([
client.sessions.get(sessionId),
client.me(),
])
const c = connectability(session)
// --no-input forces watch-only even when the session would accept messages.
const canSend = readOnly ? false : c.canSend
Expand All @@ -106,7 +109,7 @@ export async function runConnect(
const url = sessionUrl(resolveAppBase(), me.customer_login, sessionId)
// The config identity for the footer meta line: the caller's resolved name
// first, then whatever the session itself carries.
const config = configName ?? session.resolved_config_name ?? session.agent_config_id ?? null
const config = configName ?? session.config_id ?? null

// No scrollback preamble: the app owns the whole surface, Claude Code-style.
// The footer carries the session identity/status; a watch-only reason
Expand All @@ -118,7 +121,7 @@ export async function runConnect(
// cursor instead of replaying history. --no-records skips *rendering* the
// seeded history (minRenderFeedSeq), not re-streaming it.
const store = new SessionTranscriptStore()
const page = await api.getAgentSessionRecordsPage(sessionId)
const page = (await client.sessions.records(sessionId)).response
const ordered = [...page.records].sort((a, b) => a.feed_seq - b.feed_seq)
// Seed the session + open inbox as a synthetic snapshot frame (protocol v3:
// the store folds the inbox from the snapshot projection and the message_*
Expand Down Expand Up @@ -146,7 +149,7 @@ export async function runConnect(
}
const app = render(
React.createElement(ConnectApp, {
api,
api: client,
sessionId,
store,
openSocket,
Expand Down
25 changes: 12 additions & 13 deletions src/commands/file.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Command } from 'commander'
import { readFileSync, writeFileSync } from 'node:fs'
import { basename } from 'node:path'
import { ApiClient, ApiError } from '../lib/api'
import { api, APIError } from '../lib/api'
import { alsoKnownAs, apiRoutes } from '../lib/help'
import { formatTs, printJson, printTable, runAction } from '../lib/output'
import type { CreateFileRequest, FileView, GetFileResponse } from '../lib/types'
Expand Down Expand Up @@ -94,7 +94,7 @@ export function registerFile(program: Command): void {
.action(async (path: string, opts: { json?: boolean }) => {
await runAction(async () => {
const req = buildUploadRequest(path, readFileSync(path))
const res = await new ApiClient().uploadFile(req)
const res = await api().files.create(req)
// The URL is the whole point — keep it the bare primary output so an
// agent (or $(...) in a script) can capture it directly.
if (opts.json) printJson(res)
Expand All @@ -111,10 +111,9 @@ export function registerFile(program: Command): void {
.option('--json', 'output raw JSON')
.action(async (opts: { session?: string; limit?: number; json?: boolean }) => {
await runAction(async () => {
const files = await new ApiClient().listFiles({
agent_session_id: opts.session,
limit: opts.limit,
})
const files = (
await api().files.list({ session_id: opts.session, limit: opts.limit })
).items
if (opts.json) {
printJson(files)
return
Expand All @@ -130,7 +129,7 @@ export function registerFile(program: Command): void {
f.filename,
formatSize(f.size_bytes),
formatTs(f.created_at),
f.agent_session_id ?? '-',
f.session_id ?? '-',
]),
)
})
Expand All @@ -147,7 +146,7 @@ export function registerFile(program: Command): void {
.option('--json', 'output raw JSON (includes the short-lived download_url)')
.action(async (fileId: string, opts: { output?: string; json?: boolean }) => {
await runAction(async () => {
const res = await new ApiClient().getFile(fileId)
const res = await api().files.get(fileId)
if (opts.output) {
// download_url is a ~60s presigned S3 GET — fetch it immediately,
// while it's fresh. The JSON API never carries the bytes itself.
Expand All @@ -172,18 +171,18 @@ export function registerFile(program: Command): void {
.action(async (fileId: string, opts: { json?: boolean }) => {
await runAction(async () => {
try {
await new ApiClient().deleteFile(fileId)
await api().files.delete(fileId)
} catch (err) {
// A 404 covers "never existed", "someone else's", and "already
// deleted" — all the same "there's nothing here to delete" to the
// caller, so give one clear message instead of the raw HTTP error.
if (err instanceof ApiError && err.status === 404) {
if (err instanceof APIError && err.status === 404) {
throw new Error(`file not found: ${fileId}`)
}
// A 403 is a real policy decision (e.g. sandbox tokens can't delete);
// surface the server's own explanation rather than masking it.
if (err instanceof ApiError && err.status === 403) {
throw new Error(err.detail)
if (err instanceof APIError && err.status === 403) {
throw new Error(err.message)
}
throw err
}
Expand All @@ -201,7 +200,7 @@ function renderFile(res: GetFileResponse): void {
console.log(`type: ${f.content_type}`)
console.log(`size: ${formatSize(f.size_bytes)}`)
console.log(`created: ${formatTs(f.created_at)}`)
if (f.agent_session_id) console.log(`session: ${f.agent_session_id}`)
if (f.session_id) console.log(`session: ${f.session_id}`)
console.log(`url: ${res.url}`)
console.log(`\ndownload the file with: agent file get ${f.id} -o ${f.filename}`)
}
Expand Down
6 changes: 3 additions & 3 deletions src/commands/github.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Command } from 'commander'
import { ApiClient } from '../lib/api'
import { api } from '../lib/api'
import { alsoKnownAs, apiRoutes } from '../lib/help'
import { printJson, printTable, runAction } from '../lib/output'

Expand All @@ -23,7 +23,7 @@ export function registerGithub(program: Command): void {
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
await runAction(async () => {
const res = await new ApiClient().listGithubRepositories()
const res = await api().integrations.github.repos()
if (opts.json) {
printJson(res)
return
Expand Down Expand Up @@ -58,7 +58,7 @@ export function registerGithub(program: Command): void {
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
await runAction(async () => {
const res = await new ApiClient().listGithubMembers()
const res = await api().integrations.github.members()
if (opts.json) {
printJson(res)
return
Expand Down
7 changes: 3 additions & 4 deletions src/commands/help.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command } from 'commander'
import { ApiClient, ApiError } from '../lib/api'
import { api, APIError } from '../lib/api'
import { apiRoutes } from '../lib/help'
import { runAction } from '../lib/output'
import { repoFromCwd } from '../lib/laptop'
Expand Down Expand Up @@ -73,12 +73,11 @@ async function startHelperSession(): Promise<void> {
// `agent`, rather than running a workflow against a fabricated kickoff.
req.idle_start = true

const api = new ApiClient()
try {
const session = await api.startAgentSession(req)
const { session } = await api().sessions.start(req)
await startConnect(session, 'Ellipsis help agent')
} catch (err) {
if (err instanceof ApiError && err.status === 404) {
if (err instanceof APIError && err.status === 404) {
throw new Error(
`the help agent is not available on this host yet (template "${HELPER_TEMPLATE_SLUG}" not found). Run \`agent template list\` to see what you can start.`,
)
Expand Down
4 changes: 2 additions & 2 deletions src/commands/integrations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Command } from 'commander'
import { ApiClient } from '../lib/api'
import { api } from '../lib/api'
import { alsoKnownAs, apiRoutes } from '../lib/help'
import { printJson, printTable, runAction } from '../lib/output'
import type { GetIntegrationsResponse } from '../lib/types'
Expand All @@ -17,7 +17,7 @@ export function registerIntegration(program: Command): void {
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
await runAction(async () => {
const integrations = await new ApiClient().getIntegrations()
const integrations = await api().integrations.list()
if (opts.json) {
printJson(integrations)
return
Expand Down
4 changes: 2 additions & 2 deletions src/commands/linear.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Command } from 'commander'
import { ApiClient, requireConnected } from '../lib/api'
import { api, requireConnected } from '../lib/api'
import { alsoKnownAs, apiRoutes } from '../lib/help'
import { printJson, printTable, runAction } from '../lib/output'

Expand All @@ -20,7 +20,7 @@ export function registerLinear(program: Command): void {
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
await runAction(async () => {
const res = await requireConnected('Linear', new ApiClient().listLinearTeams())
const res = await requireConnected('Linear', api().integrations.linear.teams())
if (opts.json) {
printJson(res)
return
Expand Down
5 changes: 2 additions & 3 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command } from 'commander'
import { ApiClient } from '../lib/api'
import { api } from '../lib/api'
import {
activeHostName,
clearActiveHostToken,
Expand All @@ -15,9 +15,8 @@ export function registerLogin(program: Command): void {
.description('Authenticate against the active host via the device-code flow')
.option('--no-browser', 'do not auto-open the verification URL (for headless or SSH)')
.action(async (opts: { browser?: boolean }) => {
const api = new ApiClient()
try {
const { token } = await deviceLogin(api, {
const { token } = await deviceLogin(api(), {
onPrompt: (start) => {
// Build the approval URL from the app base of the host the CLI is
// pointed at, NOT the server's verification_uri_complete — the
Expand Down
Loading