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
37 changes: 27 additions & 10 deletions src/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,53 @@ import type {
CreateAgentResponse,
EnsureAgentResponse,
} from "./types.js";
import type { RequestOptions } from "../request-options.js";

export class AgentsClient {
private baseUrl: string;
private apiKey: string;
private timeout: number;
private defaultProjectId: string | null;

constructor(baseUrl: string, apiKey: string, timeout: number) {
constructor(
baseUrl: string,
apiKey: string,
timeout: number,
defaultProjectId: string | null,
) {
this.baseUrl = baseUrl.replace(/\/+$/, "");
this.apiKey = apiKey;
this.timeout = timeout;
this.defaultProjectId = defaultProjectId;
}

private buildHeaders(options?: RequestOptions): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`;
}
const projectId = options?.projectId ?? this.defaultProjectId;
if (projectId) {
headers["X-Project-Id"] = projectId;
}
return headers;
}

/**
* Create a new agent.
*/
createAgent = async (
input: CreateAgentInput,
options?: RequestOptions,
): Promise<CreateAgentResponse> => {
const url = `${this.baseUrl}/api/agents`;

const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`;
}

try {
const res = await fetch(url, {
method: "POST",
headers,
headers: this.buildHeaders(options),
body: JSON.stringify(input),
signal: AbortSignal.timeout(this.timeout),
});
Expand Down Expand Up @@ -68,9 +84,10 @@ export class AgentsClient {
*/
ensureAgent = async (
input: CreateAgentInput,
options?: RequestOptions,
): Promise<EnsureAgentResponse> => {
try {
await this.createAgent(input);
await this.createAgent(input, options);
return { name: input.name, identifier: input.identifier, created: true };
} catch (error) {
if (error instanceof OneCLIRequestError && error.statusCode === 409) {
Expand Down
58 changes: 44 additions & 14 deletions src/approvals/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OneCLIRequestError } from "../errors.js";
import type { ApprovalRequest, ManualApprovalCallback } from "./types.js";
import type { RequestOptions } from "../request-options.js";

/** Internal response shape from the gateway long-poll endpoint. */
interface PollResponse {
Expand All @@ -11,6 +12,7 @@ export class ApprovalClient {
private baseUrl: string;
private apiKey: string;
private gatewayUrl: string | null;
private defaultProjectId: string | null;
private running = false;
private abortController: AbortController | null = null;

Expand All @@ -21,22 +23,41 @@ export class ApprovalClient {
*/
private inFlight = new Set<string>();

constructor(baseUrl: string, apiKey: string, gatewayUrl: string | null) {
constructor(
baseUrl: string,
apiKey: string,
gatewayUrl: string | null,
defaultProjectId: string | null,
) {
this.baseUrl = baseUrl.replace(/\/+$/, "");
this.apiKey = apiKey;
this.gatewayUrl = gatewayUrl;
this.defaultProjectId = defaultProjectId;
}

private buildAuthHeaders(projectId?: string | null): Record<string, string> {
const headers: Record<string, string> = {
Authorization: `Bearer ${this.apiKey}`,
};
const resolved = projectId ?? this.defaultProjectId;
if (resolved) {
headers["X-Project-Id"] = resolved;
}
return headers;
}

/**
* Resolve the gateway URL from the web app.
* Called once on first poll, then cached.
*/
private async resolveGatewayUrl(): Promise<string> {
private async resolveGatewayUrl(
projectId?: string | null,
): Promise<string> {
if (this.gatewayUrl) return this.gatewayUrl;

const url = `${this.baseUrl}/api/gateway-url`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${this.apiKey}` },
headers: this.buildAuthHeaders(projectId),
signal: AbortSignal.timeout(5000),
});

Expand All @@ -62,19 +83,23 @@ export class ApprovalClient {
* submission fails), the ID is removed from `inFlight` and the
* approval will be retried on the next poll cycle.
*/
async start(callback: ManualApprovalCallback): Promise<void> {
async start(
callback: ManualApprovalCallback,
options?: RequestOptions,
): Promise<void> {
this.running = true;
const gatewayUrl = await this.resolveGatewayUrl();
const projectId = options?.projectId ?? null;
const gatewayUrl = await this.resolveGatewayUrl(projectId);

while (this.running) {
try {
const poll = await this.poll(gatewayUrl);
const poll = await this.poll(gatewayUrl, projectId);

for (const request of poll.requests) {
this.inFlight.add(request.id);
request.timeoutSeconds = poll.timeoutSeconds;

this.handleRequest(gatewayUrl, request, callback);
this.handleRequest(gatewayUrl, request, callback, projectId);
}
} catch {
if (!this.running) return;
Expand All @@ -92,11 +117,12 @@ export class ApprovalClient {
gatewayUrl: string,
request: ApprovalRequest,
callback: ManualApprovalCallback,
projectId?: string | null,
): void {
(async () => {
try {
const decision = await callback(request);
await this.submitDecision(gatewayUrl, request.id, decision);
await this.submitDecision(gatewayUrl, request.id, decision, projectId);
} finally {
this.inFlight.delete(request.id);
}
Expand All @@ -115,7 +141,10 @@ export class ApprovalClient {
* Long-poll the gateway for pending approvals.
* Server holds up to 30s; we set a 35s client timeout.
*/
private async poll(gatewayUrl: string): Promise<PollResponse> {
private async poll(
gatewayUrl: string,
projectId?: string | null,
): Promise<PollResponse> {
this.abortController = new AbortController();

let url = `${gatewayUrl}/api/approvals/pending`;
Expand All @@ -124,7 +153,7 @@ export class ApprovalClient {
url += `?exclude=${encodeURIComponent(exclude)}`;
}
const res = await fetch(url, {
headers: { Authorization: `Bearer ${this.apiKey}` },
headers: this.buildAuthHeaders(projectId),
signal: AbortSignal.any([
this.abortController.signal,
AbortSignal.timeout(35_000),
Expand All @@ -146,15 +175,16 @@ export class ApprovalClient {
gatewayUrl: string,
id: string,
decision: string,
projectId?: string | null,
): Promise<void> {
const url = `${gatewayUrl}/api/approvals/${encodeURIComponent(id)}/decision`;

const headers = this.buildAuthHeaders(projectId);
headers["Content-Type"] = "application/json";

const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
headers,
body: JSON.stringify({ decision }),
signal: AbortSignal.timeout(5000),
});
Expand Down
69 changes: 49 additions & 20 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { AgentsClient } from "./agents/index.js";
import { ApprovalClient } from "./approvals/index.js";
import { ProvisionClient } from "./provisions/index.js";
import type { OneCLIOptions } from "./types.js";
import type { RequestOptions } from "./request-options.js";
import type {
ApplyContainerConfigOptions,
ContainerConfig,
GetContainerConfigOptions,
} from "./container/types.js";
import type {
CreateAgentInput,
Expand All @@ -17,8 +19,8 @@ import type {
ManualApprovalHandle,
} from "./approvals/types.js";
import type {
ProvisionUserInput,
ProvisionUserResponse,
ProvisionProjectInput,
ProvisionProjectResponse,
} from "./provisions/types.js";

const DEFAULT_URL = "https://app.onecli.sh";
Expand All @@ -36,25 +38,44 @@ export class OneCLI {
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
const gatewayUrl =
options.gatewayUrl ?? process.env.ONECLI_GATEWAY_URL ?? null;
const projectId =
options.projectId ?? process.env.ONECLI_PROJECT_ID ?? null;

this.containerClient = new ContainerClient(url, apiKey, timeout);
this.agentsClient = new AgentsClient(url, apiKey, timeout);
this.approvalClient = new ApprovalClient(url, apiKey, gatewayUrl);
this.provisionClient = new ProvisionClient(url, apiKey, timeout);
this.containerClient = new ContainerClient(
url,
apiKey,
timeout,
projectId,
);
this.agentsClient = new AgentsClient(url, apiKey, timeout, projectId);
this.approvalClient = new ApprovalClient(
url,
apiKey,
gatewayUrl,
projectId,
);
this.provisionClient = new ProvisionClient(
url,
apiKey,
timeout,
projectId,
);
}

/**
* Fetch the gateway skill markdown from OneCLI.
*/
getGatewaySkill = (): Promise<string> => {
return this.containerClient.getGatewaySkill();
getGatewaySkill = (options?: RequestOptions): Promise<string> => {
return this.containerClient.getGatewaySkill(options);
};

/**
* Fetch the raw container configuration from OneCLI.
*/
getContainerConfig = (agent?: string): Promise<ContainerConfig> => {
return this.containerClient.getContainerConfig(agent);
getContainerConfig = (
options?: GetContainerConfigOptions,
): Promise<ContainerConfig> => {
return this.containerClient.getContainerConfig(options);
};

/**
Expand All @@ -71,26 +92,33 @@ export class OneCLI {
/**
* Create a new agent.
*/
createAgent = (input: CreateAgentInput): Promise<CreateAgentResponse> => {
return this.agentsClient.createAgent(input);
createAgent = (
input: CreateAgentInput,
options?: RequestOptions,
): Promise<CreateAgentResponse> => {
return this.agentsClient.createAgent(input, options);
};

/**
* Ensure an agent exists. Creates it if missing, returns normally if it already exists.
*/
ensureAgent = (input: CreateAgentInput): Promise<EnsureAgentResponse> => {
return this.agentsClient.ensureAgent(input);
ensureAgent = (
input: CreateAgentInput,
options?: RequestOptions,
): Promise<EnsureAgentResponse> => {
return this.agentsClient.ensureAgent(input, options);
};

/**
* Provision a new user in your organization.
* Provision a new project in your organization.
* Pre-creates a user account, project, and API key.
* Returns a claim URL and API key. Requires admin/owner role.
*/
provisionUser = (
input?: ProvisionUserInput,
): Promise<ProvisionUserResponse> => {
return this.provisionClient.provisionUser(input);
provisionProject = (
input?: ProvisionProjectInput,
options?: RequestOptions,
): Promise<ProvisionProjectResponse> => {
return this.provisionClient.provisionProject(input, options);
};

/**
Expand All @@ -101,8 +129,9 @@ export class OneCLI {
*/
configureManualApproval = (
callback: ManualApprovalCallback,
options?: RequestOptions,
): ManualApprovalHandle => {
this.approvalClient.start(callback).catch(() => {
this.approvalClient.start(callback, options).catch(() => {
// Errors handled internally with backoff
});
return { stop: () => this.approvalClient.stop() };
Expand Down
Loading
Loading