@indigoai-us/hq-cli 5.56.0 → 5.57.0

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.
@@ -0,0 +1,175 @@
1
+ export type SandboxRunnerState = "queued" | "running" | "succeeded" | "failed";
2
+
3
+ export interface SandboxRunnerStartRequest {
4
+ skillId: string;
5
+ companyUid: string;
6
+ args?: Record<string, unknown>;
7
+ companySlug?: string;
8
+ only?: string[];
9
+ usage?: unknown;
10
+ }
11
+
12
+ export interface SandboxRunnerStartResponse {
13
+ jobId: string;
14
+ status: SandboxRunnerState;
15
+ }
16
+
17
+ export interface SandboxRunnerJob {
18
+ jobId: string;
19
+ status: SandboxRunnerState;
20
+ stdout?: string;
21
+ stderr?: string;
22
+ logsTail?: string;
23
+ exitCode?: number;
24
+ error?: string;
25
+ }
26
+
27
+ export interface SandboxRunnerClientOptions {
28
+ baseUrl?: string;
29
+ fetchImpl?: typeof fetch;
30
+ }
31
+
32
+ export interface SandboxRunnerPollOptions {
33
+ intervalMs?: number;
34
+ maxPolls?: number;
35
+ }
36
+
37
+ const DEFAULT_SANDBOX_RUNNER_URL = "https://hqapi.getindigo.ai/sandbox";
38
+
39
+ function normalizeBaseUrl(baseUrl: string): string {
40
+ return baseUrl.replace(/\/+$/, "");
41
+ }
42
+
43
+ function getSandboxRunnerBaseUrl(): string {
44
+ return normalizeBaseUrl(
45
+ process.env.HQ_SANDBOX_RUNNER_URL ?? DEFAULT_SANDBOX_RUNNER_URL,
46
+ );
47
+ }
48
+
49
+ function isSandboxRunnerState(value: unknown): value is SandboxRunnerState {
50
+ return value === "queued" || value === "running" || value === "succeeded" || value === "failed";
51
+ }
52
+
53
+ async function parseJsonResponse(res: Response): Promise<Record<string, unknown>> {
54
+ return (await res.json().catch(() => ({}))) as Record<string, unknown>;
55
+ }
56
+
57
+ function requireString(body: Record<string, unknown>, key: string): string {
58
+ const value = body[key];
59
+ if (typeof value !== "string" || value.length === 0) {
60
+ throw new Error(`Sandbox Runner returned an invalid '${key}'.`);
61
+ }
62
+ return value;
63
+ }
64
+
65
+ function normalizeJob(
66
+ body: Record<string, unknown>,
67
+ jobIdFallback?: string,
68
+ ): SandboxRunnerJob {
69
+ const status = body.status;
70
+ if (!isSandboxRunnerState(status)) {
71
+ throw new Error("Sandbox Runner returned an invalid job status.");
72
+ }
73
+ const output =
74
+ typeof body.stdout === "string"
75
+ ? body.stdout
76
+ : typeof body.output === "string"
77
+ ? body.output
78
+ : undefined;
79
+ return {
80
+ jobId:
81
+ typeof body.jobId === "string" && body.jobId.length > 0
82
+ ? body.jobId
83
+ : jobIdFallback ?? requireString(body, "jobId"),
84
+ status,
85
+ stdout: output,
86
+ stderr: typeof body.stderr === "string" ? body.stderr : undefined,
87
+ logsTail: typeof body.logsTail === "string" ? body.logsTail : undefined,
88
+ exitCode: typeof body.exitCode === "number" ? body.exitCode : undefined,
89
+ error: typeof body.error === "string" ? body.error : undefined,
90
+ };
91
+ }
92
+
93
+ function delay(ms: number): Promise<void> {
94
+ if (ms <= 0) return Promise.resolve();
95
+ return new Promise((resolve) => setTimeout(resolve, ms));
96
+ }
97
+
98
+ export class SandboxRunnerClient {
99
+ private readonly baseUrl: string;
100
+ private readonly fetchImpl: typeof fetch;
101
+
102
+ constructor(options: SandboxRunnerClientOptions = {}) {
103
+ this.baseUrl = normalizeBaseUrl(options.baseUrl ?? getSandboxRunnerBaseUrl());
104
+ this.fetchImpl = options.fetchImpl ?? fetch;
105
+ }
106
+
107
+ async startJob(
108
+ token: string,
109
+ request: SandboxRunnerStartRequest,
110
+ ): Promise<SandboxRunnerStartResponse> {
111
+ const res = await this.fetchImpl(`${this.baseUrl}/jobs`, {
112
+ method: "POST",
113
+ headers: {
114
+ Authorization: `Bearer ${token}`,
115
+ "Content-Type": "application/json",
116
+ },
117
+ body: JSON.stringify(request),
118
+ });
119
+ const body = await parseJsonResponse(res);
120
+ if (!res.ok) {
121
+ const message =
122
+ typeof body.message === "string"
123
+ ? body.message
124
+ : typeof body.error === "string"
125
+ ? body.error
126
+ : res.statusText;
127
+ throw new Error(`Sandbox Runner rejected job: ${message}`);
128
+ }
129
+ const status = body.status;
130
+ if (!isSandboxRunnerState(status)) {
131
+ throw new Error("Sandbox Runner returned an invalid start status.");
132
+ }
133
+ return {
134
+ jobId: requireString(body, "jobId"),
135
+ status,
136
+ };
137
+ }
138
+
139
+ async getJob(token: string, jobId: string): Promise<SandboxRunnerJob> {
140
+ const res = await this.fetchImpl(
141
+ `${this.baseUrl}/jobs/${encodeURIComponent(jobId)}`,
142
+ {
143
+ headers: { Authorization: `Bearer ${token}` },
144
+ },
145
+ );
146
+ const body = await parseJsonResponse(res);
147
+ if (!res.ok) {
148
+ const message =
149
+ typeof body.message === "string"
150
+ ? body.message
151
+ : typeof body.error === "string"
152
+ ? body.error
153
+ : res.statusText;
154
+ throw new Error(`Sandbox Runner job lookup failed: ${message}`);
155
+ }
156
+ return normalizeJob(body, jobId);
157
+ }
158
+
159
+ async pollJob(
160
+ token: string,
161
+ jobId: string,
162
+ options: SandboxRunnerPollOptions = {},
163
+ ): Promise<SandboxRunnerJob> {
164
+ const intervalMs = options.intervalMs ?? 1000;
165
+ const maxPolls = options.maxPolls ?? 300;
166
+ for (let attempt = 0; attempt < maxPolls; attempt += 1) {
167
+ const job = await this.getJob(token, jobId);
168
+ if (job.status === "succeeded" || job.status === "failed") {
169
+ return job;
170
+ }
171
+ await delay(intervalMs);
172
+ }
173
+ throw new Error(`Sandbox Runner job '${jobId}' did not finish before the poll limit.`);
174
+ }
175
+ }