@pickaxe/harnesslayer 0.1.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.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/index.d.ts +578 -0
- package/dist/index.js +1501 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1501 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var HarnesslayerError = class extends Error {
|
|
3
|
+
name = "HarnesslayerError";
|
|
4
|
+
};
|
|
5
|
+
var HarnesslayerHTTPError = class extends HarnesslayerError {
|
|
6
|
+
name = "HarnesslayerHTTPError";
|
|
7
|
+
};
|
|
8
|
+
var HarnesslayerResponseError = class extends HarnesslayerError {
|
|
9
|
+
name = "HarnesslayerResponseError";
|
|
10
|
+
};
|
|
11
|
+
var HarnesslayerAPIError = class extends HarnesslayerError {
|
|
12
|
+
name = "HarnesslayerAPIError";
|
|
13
|
+
};
|
|
14
|
+
var DriftstoneError = class extends Error {
|
|
15
|
+
name = "DriftstoneError";
|
|
16
|
+
};
|
|
17
|
+
var DriftstoneHTTPError = class extends DriftstoneError {
|
|
18
|
+
name = "DriftstoneHTTPError";
|
|
19
|
+
};
|
|
20
|
+
var DriftstoneResponseError = class extends DriftstoneError {
|
|
21
|
+
name = "DriftstoneResponseError";
|
|
22
|
+
};
|
|
23
|
+
var DriftstoneAPIError = class extends DriftstoneError {
|
|
24
|
+
name = "DriftstoneAPIError";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/internal/constants.ts
|
|
28
|
+
var DEFAULT_API_BASE_URL = "https://pickaxeproject--harnesslayer-api-v2-api.modal.run";
|
|
29
|
+
var DEFAULT_DRIFTSTONE_API_BASE_URL = "https://api.driftstone.ai";
|
|
30
|
+
|
|
31
|
+
// src/internal/driftstone.ts
|
|
32
|
+
import { readFile } from "fs/promises";
|
|
33
|
+
import path from "path";
|
|
34
|
+
var DriftstoneClient = class {
|
|
35
|
+
repos;
|
|
36
|
+
baseUrl;
|
|
37
|
+
headers;
|
|
38
|
+
timeoutMs;
|
|
39
|
+
fetchImpl;
|
|
40
|
+
constructor(options) {
|
|
41
|
+
const apiKey = options.apiKey.trim();
|
|
42
|
+
if (!apiKey) throw new DriftstoneError("apiKey must be provided");
|
|
43
|
+
if (!apiKey.startsWith("dk-")) throw new DriftstoneError("Invalid Driftstone API key");
|
|
44
|
+
this.baseUrl = `${(options.baseUrl ?? DEFAULT_DRIFTSTONE_API_BASE_URL).replace(/\/+$/, "")}/${options.version}`;
|
|
45
|
+
this.timeoutMs = options.timeoutMs;
|
|
46
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
47
|
+
this.headers = {
|
|
48
|
+
Authorization: `Bearer ${apiKey}`,
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
Accept: "application/json",
|
|
51
|
+
"User-Agent": "Harnesslayer-typescript/0.1.0"
|
|
52
|
+
};
|
|
53
|
+
this.repos = new DriftstoneRepositories(this);
|
|
54
|
+
}
|
|
55
|
+
async request(method, requestPath, payload = {}) {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(requestPath.replace(/^\/+/, ""), `${this.baseUrl}/`);
|
|
60
|
+
const init = {
|
|
61
|
+
method,
|
|
62
|
+
headers: this.headers,
|
|
63
|
+
signal: controller.signal
|
|
64
|
+
};
|
|
65
|
+
if (method === "GET") {
|
|
66
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
67
|
+
if (value != null) url.searchParams.set(key, String(value));
|
|
68
|
+
}
|
|
69
|
+
} else if (Object.keys(payload).length) {
|
|
70
|
+
init.body = JSON.stringify(payload);
|
|
71
|
+
}
|
|
72
|
+
const response = await this.fetchImpl(url, init);
|
|
73
|
+
const data = await this.parseResponse(response);
|
|
74
|
+
if (response.status >= 400) {
|
|
75
|
+
throw new DriftstoneAPIError(
|
|
76
|
+
`Driftstone API returned error status ${response.status}: ${JSON.stringify(data)}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (data.success === false) {
|
|
80
|
+
throw new DriftstoneAPIError(
|
|
81
|
+
`Driftstone API error: ${String(data.error ?? data.message ?? "Request failed")}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof DriftstoneAPIError || error instanceof DriftstoneResponseError) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
throw new DriftstoneHTTPError(
|
|
90
|
+
`HTTP request failed: ${error instanceof Error ? error.message : String(error)}`
|
|
91
|
+
);
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async uploadFile(url, filePath) {
|
|
97
|
+
const trimmedUrl = url.trim();
|
|
98
|
+
if (!trimmedUrl) throw new DriftstoneError("url must be a non-empty string");
|
|
99
|
+
const response = await this.fetchImpl(trimmedUrl, {
|
|
100
|
+
method: "PUT",
|
|
101
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
102
|
+
body: await readFile(filePath)
|
|
103
|
+
});
|
|
104
|
+
if (response.status >= 400) {
|
|
105
|
+
throw new DriftstoneHTTPError(`File upload failed with status ${response.status}`);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
async uploadFiles(upload, files, root) {
|
|
110
|
+
const uploadUrls = new Map(upload.uploadUrls.map((item) => [item.name, item.url]));
|
|
111
|
+
const rootPath = path.resolve(root);
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const url = uploadUrls.get(file.name);
|
|
114
|
+
if (!url) throw new DriftstoneError(`Missing upload URL for file '${file.name}'`);
|
|
115
|
+
const filePath = path.resolve(rootPath, file.name);
|
|
116
|
+
if (filePath !== rootPath && !filePath.startsWith(`${rootPath}${path.sep}`)) {
|
|
117
|
+
throw new DriftstoneError(`file path must stay within root: ${file.name}`);
|
|
118
|
+
}
|
|
119
|
+
await this.uploadFile(url, filePath);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
async parseResponse(response) {
|
|
124
|
+
let decoded;
|
|
125
|
+
try {
|
|
126
|
+
decoded = await response.json();
|
|
127
|
+
} catch {
|
|
128
|
+
throw new DriftstoneResponseError(
|
|
129
|
+
`Driftstone API returned non-JSON response (status=${response.status})`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
133
|
+
throw new DriftstoneResponseError("Driftstone API returned an unexpected response payload");
|
|
134
|
+
}
|
|
135
|
+
return decoded;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var DriftstoneRepositories = class {
|
|
139
|
+
constructor(client) {
|
|
140
|
+
this.client = client;
|
|
141
|
+
}
|
|
142
|
+
client;
|
|
143
|
+
async create(name) {
|
|
144
|
+
const response = await this.client.request("POST", "/repos", { name: requireDriftstoneString(name, "name") });
|
|
145
|
+
return repositoryFromApi(response.data);
|
|
146
|
+
}
|
|
147
|
+
async get(repo) {
|
|
148
|
+
const response = await this.client.request("GET", `/repos/${encodeRepo(repo)}`);
|
|
149
|
+
return repositoryFromApi(response.data);
|
|
150
|
+
}
|
|
151
|
+
async createBranch(repo, name, options) {
|
|
152
|
+
const payload = {
|
|
153
|
+
name: requireRouteSegment(name, "name"),
|
|
154
|
+
storageDir: requireRouteSegment(options.storageDir, "storageDir"),
|
|
155
|
+
archive: options.archive ?? true
|
|
156
|
+
};
|
|
157
|
+
if (options.message != null) payload.message = requireDriftstoneString(options.message, "message");
|
|
158
|
+
if (options.transforms != null) payload.transforms = options.transforms;
|
|
159
|
+
const response = await this.client.request("POST", `/repos/${encodeRepo(repo)}/branches`, payload);
|
|
160
|
+
return branchFromApi(response.data);
|
|
161
|
+
}
|
|
162
|
+
async createUpload(repo, options) {
|
|
163
|
+
const response = await this.client.request("POST", `/repos/${encodeRepo(repo)}/uploads`, {
|
|
164
|
+
branch: requireRouteSegment(options.branch ?? "main", "branch"),
|
|
165
|
+
storageDir: requireRouteSegment(options.storageDir, "storageDir"),
|
|
166
|
+
files: options.files
|
|
167
|
+
});
|
|
168
|
+
return uploadFromApi(response.data);
|
|
169
|
+
}
|
|
170
|
+
async completeUpload(repo, uploadId) {
|
|
171
|
+
await this.client.request(
|
|
172
|
+
"POST",
|
|
173
|
+
`/repos/${encodeRepo(repo)}/uploads/${requireRouteSegment(uploadId, "uploadId")}/complete`
|
|
174
|
+
);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
async copyDirectory(repo, options) {
|
|
178
|
+
const response = await this.client.request("POST", `/repos/${encodeRepo(repo)}/copies`, {
|
|
179
|
+
sourceDir: requireRouteSegment(options.sourceDir, "sourceDir"),
|
|
180
|
+
destDir: requireRouteSegment(options.destDir, "destDir"),
|
|
181
|
+
sourceBranch: requireRouteSegment(options.sourceBranch ?? "main", "sourceBranch"),
|
|
182
|
+
destBranch: requireRouteSegment(options.destBranch ?? "main", "destBranch"),
|
|
183
|
+
transforms: options.transforms ?? {},
|
|
184
|
+
archive: options.archive ?? true
|
|
185
|
+
});
|
|
186
|
+
return copyFromApi(response.data);
|
|
187
|
+
}
|
|
188
|
+
async getCopyStatus(repo, runId) {
|
|
189
|
+
const response = await this.client.request(
|
|
190
|
+
"GET",
|
|
191
|
+
`/repos/${encodeRepo(repo)}/copies/${requireRouteSegment(runId, "runId")}`
|
|
192
|
+
);
|
|
193
|
+
return copyStatusFromApi(response.data);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function requireDriftstoneString(value, name) {
|
|
197
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
198
|
+
throw new DriftstoneError(`${name} must be a non-empty string`);
|
|
199
|
+
}
|
|
200
|
+
return value.trim();
|
|
201
|
+
}
|
|
202
|
+
function requireRouteSegment(value, name) {
|
|
203
|
+
const normalized = requireDriftstoneString(value, name);
|
|
204
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
205
|
+
throw new DriftstoneError(`${name} cannot contain / or \\`);
|
|
206
|
+
}
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
function encodeRepo(repo) {
|
|
210
|
+
return encodeURIComponent(requireDriftstoneString(repo, "repo").replace(/^\/+|\/+$/g, ""));
|
|
211
|
+
}
|
|
212
|
+
function repositoryFromApi(payload) {
|
|
213
|
+
return {
|
|
214
|
+
id: String(payload.id),
|
|
215
|
+
orgId: String(payload.orgId),
|
|
216
|
+
name: String(payload.name),
|
|
217
|
+
slug: String(payload.slug),
|
|
218
|
+
branch: String(payload.branch)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function branchFromApi(payload) {
|
|
222
|
+
return {
|
|
223
|
+
id: String(payload.id),
|
|
224
|
+
repoId: String(payload.repoId),
|
|
225
|
+
name: String(payload.name),
|
|
226
|
+
storagePath: payload.storagePath ?? null,
|
|
227
|
+
runId: payload.runId ?? null,
|
|
228
|
+
status: payload.status ?? null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function copyFromApi(payload) {
|
|
232
|
+
return {
|
|
233
|
+
repoId: String(payload.repoId),
|
|
234
|
+
runId: String(payload.runId),
|
|
235
|
+
status: String(payload.status),
|
|
236
|
+
sourceDir: payload.sourceDir ?? null,
|
|
237
|
+
destDir: payload.destDir ?? null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function copyStatusFromApi(payload) {
|
|
241
|
+
return {
|
|
242
|
+
repoId: String(payload.repoId),
|
|
243
|
+
runId: String(payload.runId),
|
|
244
|
+
status: String(payload.status),
|
|
245
|
+
result: payload.result && typeof payload.result === "object" && !Array.isArray(payload.result) ? payload.result : null
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function uploadFromApi(payload) {
|
|
249
|
+
const urls = Array.isArray(payload.uploadUrls) ? payload.uploadUrls : [];
|
|
250
|
+
return {
|
|
251
|
+
repoId: String(payload.repoId),
|
|
252
|
+
uploadId: String(payload.uploadId),
|
|
253
|
+
uploadUrls: urls.filter((item) => Boolean(item) && typeof item === "object").map((item) => ({ name: String(item.name), url: String(item.url) }))
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/internal/transport.ts
|
|
258
|
+
var Transport = class {
|
|
259
|
+
headers;
|
|
260
|
+
timeoutMs;
|
|
261
|
+
baseUrl;
|
|
262
|
+
fetchImpl;
|
|
263
|
+
constructor(options) {
|
|
264
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
265
|
+
this.timeoutMs = options.timeoutMs;
|
|
266
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
267
|
+
this.headers = {
|
|
268
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
269
|
+
"Content-Type": "application/json",
|
|
270
|
+
Accept: "application/json",
|
|
271
|
+
"User-Agent": options.userAgent
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async request(method, path4, payload = {}) {
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
277
|
+
try {
|
|
278
|
+
const url = new URL(path4.replace(/^\/+/, ""), `${this.baseUrl}/`);
|
|
279
|
+
const init = {
|
|
280
|
+
method,
|
|
281
|
+
headers: this.headers,
|
|
282
|
+
signal: controller.signal
|
|
283
|
+
};
|
|
284
|
+
if (method === "GET") {
|
|
285
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
286
|
+
if (value != null) url.searchParams.set(key, String(value));
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
init.body = JSON.stringify(payload);
|
|
290
|
+
}
|
|
291
|
+
const response = await this.fetchImpl(url, init);
|
|
292
|
+
const data = await this.parseResponse(response);
|
|
293
|
+
if (response.status >= 400) {
|
|
294
|
+
throw new HarnesslayerAPIError(
|
|
295
|
+
`Harnesslayer API returned error status ${response.status}: ${JSON.stringify(data)}`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (data.success === false) {
|
|
299
|
+
const message = String(data.error ?? data.message ?? "Request failed");
|
|
300
|
+
throw new HarnesslayerAPIError(`Harnesslayer API error: ${message}`);
|
|
301
|
+
}
|
|
302
|
+
return data;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof HarnesslayerAPIError || error instanceof HarnesslayerResponseError) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
throw new HarnesslayerHTTPError(`HTTP request failed: ${message}`);
|
|
309
|
+
} finally {
|
|
310
|
+
clearTimeout(timeout);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async parseResponse(response) {
|
|
314
|
+
let decoded;
|
|
315
|
+
try {
|
|
316
|
+
decoded = await response.json();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
throw new HarnesslayerResponseError(
|
|
319
|
+
`Harnesslayer API returned non-JSON response (status=${response.status})`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
323
|
+
throw new HarnesslayerResponseError(
|
|
324
|
+
"Harnesslayer API returned an unexpected response payload"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return decoded;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/internal/config.ts
|
|
332
|
+
import { createHash, randomUUID } from "crypto";
|
|
333
|
+
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
334
|
+
import path2 from "path";
|
|
335
|
+
|
|
336
|
+
// src/internal/utils.ts
|
|
337
|
+
function safeParseJson(value) {
|
|
338
|
+
if (typeof value !== "string") return value;
|
|
339
|
+
try {
|
|
340
|
+
return JSON.parse(value);
|
|
341
|
+
} catch {
|
|
342
|
+
return void 0;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function parseDate(value, field) {
|
|
346
|
+
if (value instanceof Date) return value;
|
|
347
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
348
|
+
const date = new Date(value);
|
|
349
|
+
if (!Number.isNaN(date.getTime())) return date;
|
|
350
|
+
}
|
|
351
|
+
throw new TypeError(`Expected ${field} to be a date-compatible value`);
|
|
352
|
+
}
|
|
353
|
+
function parseOptionalDate(value, field) {
|
|
354
|
+
if (value == null) return null;
|
|
355
|
+
return parseDate(value, field);
|
|
356
|
+
}
|
|
357
|
+
function sleep(ms) {
|
|
358
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/internal/config.ts
|
|
362
|
+
var IGNORED_PARTS = /* @__PURE__ */ new Set(["__MACOSX"]);
|
|
363
|
+
function newVersionId() {
|
|
364
|
+
return `ver-${randomUUID()}`;
|
|
365
|
+
}
|
|
366
|
+
function newStateId() {
|
|
367
|
+
return `state-${randomUUID()}`;
|
|
368
|
+
}
|
|
369
|
+
function validateCloneTransform(transform) {
|
|
370
|
+
if (transform == null) return {};
|
|
371
|
+
if (!transform || typeof transform !== "object" || Array.isArray(transform)) {
|
|
372
|
+
throw new HarnesslayerError("transform must be an object");
|
|
373
|
+
}
|
|
374
|
+
const normalized = {};
|
|
375
|
+
for (const [filePath, patch] of Object.entries(transform)) {
|
|
376
|
+
if (!filePath.trim()) {
|
|
377
|
+
throw new HarnesslayerError("transform keys must be non-empty strings");
|
|
378
|
+
}
|
|
379
|
+
const normalizedPath = filePath.trim().replace(/\\/g, "/");
|
|
380
|
+
if (normalizedPath.startsWith("/") || normalizedPath.split("/").includes("..")) {
|
|
381
|
+
throw new HarnesslayerError(`Invalid transform file path: ${filePath}`);
|
|
382
|
+
}
|
|
383
|
+
const lower = normalizedPath.toLowerCase();
|
|
384
|
+
if (lower.endsWith(".json")) {
|
|
385
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
386
|
+
throw new HarnesslayerError(`transform['${filePath}'] must be a JSON object patch`);
|
|
387
|
+
}
|
|
388
|
+
} else if (lower.endsWith(".md")) {
|
|
389
|
+
if (typeof patch !== "string") {
|
|
390
|
+
throw new HarnesslayerError(`transform['${filePath}'] must be Markdown string content`);
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
throw new HarnesslayerError(`transform target must be a .json or .md file: ${filePath}`);
|
|
394
|
+
}
|
|
395
|
+
normalized[normalizedPath] = patch;
|
|
396
|
+
}
|
|
397
|
+
return normalized;
|
|
398
|
+
}
|
|
399
|
+
async function validateConfigPath(configPath) {
|
|
400
|
+
const root = path2.resolve(configPath);
|
|
401
|
+
const rootStat = await stat(root).catch(() => null);
|
|
402
|
+
if (!rootStat) throw new HarnesslayerError(`Config path does not exist: ${root}`);
|
|
403
|
+
if (!rootStat.isDirectory()) throw new HarnesslayerError(`Config path must be a directory: ${root}`);
|
|
404
|
+
const basename = path2.basename(configPath);
|
|
405
|
+
if (basename.endsWith(".claude")) {
|
|
406
|
+
await validateClaudeConfigPath(root);
|
|
407
|
+
return { type: "claude", path: root };
|
|
408
|
+
}
|
|
409
|
+
if (basename.endsWith(".openclaw")) {
|
|
410
|
+
await validateOpenClawConfigPath(root);
|
|
411
|
+
return { type: "openclaw", path: root };
|
|
412
|
+
}
|
|
413
|
+
throw new HarnesslayerError(`Config path must end with .claude or .openclaw: ${configPath}`);
|
|
414
|
+
}
|
|
415
|
+
async function createUploadFilesPayload(root) {
|
|
416
|
+
const files = [];
|
|
417
|
+
const rootPath = path2.resolve(root);
|
|
418
|
+
async function walk(directory) {
|
|
419
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
420
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
421
|
+
for (const entry of entries) {
|
|
422
|
+
if (IGNORED_PARTS.has(entry.name)) continue;
|
|
423
|
+
const absolute = path2.join(directory, entry.name);
|
|
424
|
+
const relative = path2.relative(rootPath, absolute).split(path2.sep).join("/");
|
|
425
|
+
if (relative.split("/").some((part) => IGNORED_PARTS.has(part))) continue;
|
|
426
|
+
if (entry.isDirectory()) {
|
|
427
|
+
await walk(absolute);
|
|
428
|
+
} else if (entry.isFile()) {
|
|
429
|
+
const bytes = await readFile2(absolute);
|
|
430
|
+
files.push({
|
|
431
|
+
name: relative,
|
|
432
|
+
hash: createHash("sha256").update(bytes).digest("hex")
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
await walk(rootPath);
|
|
438
|
+
if (!files.length) {
|
|
439
|
+
throw new HarnesslayerError(`Config path contains no files to upload: ${rootPath}`);
|
|
440
|
+
}
|
|
441
|
+
return files;
|
|
442
|
+
}
|
|
443
|
+
async function validateClaudeConfigPath(root) {
|
|
444
|
+
await requireFile(path2.join(root, "agent.json"), `Invalid Agent config path: missing agent.json in '${root}'`);
|
|
445
|
+
await requireFile(
|
|
446
|
+
path2.join(root, "environment.json"),
|
|
447
|
+
`Invalid Agent config path: missing environment.json in '${root}'`
|
|
448
|
+
);
|
|
449
|
+
const agentJson = safeParseJson(await readFile2(path2.join(root, "agent.json"), "utf8"));
|
|
450
|
+
if (!agentJson || typeof agentJson !== "object" || Array.isArray(agentJson)) {
|
|
451
|
+
throw new HarnesslayerError("agent.json is not a valid JSON object");
|
|
452
|
+
}
|
|
453
|
+
const skills = agentJson.skills;
|
|
454
|
+
if (Array.isArray(skills)) {
|
|
455
|
+
for (const skill of skills) {
|
|
456
|
+
if (!skill || typeof skill !== "object" || !("type" in skill)) {
|
|
457
|
+
throw new HarnesslayerError("Each skill in agent.json must be an object with a 'name' field");
|
|
458
|
+
}
|
|
459
|
+
if (skill.type !== "anthropic") {
|
|
460
|
+
throw new HarnesslayerError(
|
|
461
|
+
"Only skills of type 'anthropic' are supported in agent.json, if you want custom skills you must add them as a folder under 'skills' directory"
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
467
|
+
for (const entry of entries) {
|
|
468
|
+
if (IGNORED_PARTS.has(entry.name)) continue;
|
|
469
|
+
if (entry.isDirectory() && entry.name !== "skills") {
|
|
470
|
+
throw new HarnesslayerError(`Invalid Agent config path: unexpected subdirectory '${entry.name}' in '${root}'`);
|
|
471
|
+
}
|
|
472
|
+
if (entry.isFile() && !["agent.json", "environment.json"].includes(entry.name)) {
|
|
473
|
+
throw new HarnesslayerError(`Invalid Agent config path: unexpected file '${entry.name}' in '${root}'`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const skillsDir = path2.join(root, "skills");
|
|
477
|
+
const skillsStat = await stat(skillsDir).catch(() => null);
|
|
478
|
+
if (!skillsStat?.isDirectory()) return;
|
|
479
|
+
const skillEntries = await readdir(skillsDir, { withFileTypes: true });
|
|
480
|
+
for (const skillEntry of skillEntries) {
|
|
481
|
+
if (IGNORED_PARTS.has(skillEntry.name)) continue;
|
|
482
|
+
if (!skillEntry.isDirectory()) {
|
|
483
|
+
throw new HarnesslayerError(`Invalid Agent config path: skill '${skillEntry.name}' is not a directory in '${root}'`);
|
|
484
|
+
}
|
|
485
|
+
await requireFile(
|
|
486
|
+
path2.join(skillsDir, skillEntry.name, "SKILL.md"),
|
|
487
|
+
`Invalid Agent config path: missing SKILL.md in skill '${skillEntry.name}' in '${root}'`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function validateOpenClawConfigPath(root) {
|
|
492
|
+
await requireFile(
|
|
493
|
+
path2.join(root, "openclaw.json"),
|
|
494
|
+
`Invalid OpenClaw config path: missing openclaw.json in '${root}'`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
async function requireFile(filePath, message) {
|
|
498
|
+
const fileStat = await stat(filePath).catch(() => null);
|
|
499
|
+
if (!fileStat?.isFile()) throw new HarnesslayerError(message);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/models.ts
|
|
503
|
+
function accessGroupFromApi(payload) {
|
|
504
|
+
return {
|
|
505
|
+
id: String(payload.id),
|
|
506
|
+
appId: String(payload.appId),
|
|
507
|
+
name: String(payload.name),
|
|
508
|
+
limit: Number(payload.limit),
|
|
509
|
+
limitReset: payload.limitReset,
|
|
510
|
+
limitMessage: String(payload.limitMessage),
|
|
511
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
512
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt")
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function appFromApi(payload, resources) {
|
|
516
|
+
return {
|
|
517
|
+
id: String(payload.id),
|
|
518
|
+
orgId: String(payload.orgId),
|
|
519
|
+
type: payload.type,
|
|
520
|
+
name: String(payload.name),
|
|
521
|
+
slug: String(payload.slug),
|
|
522
|
+
vaultId: payload.vaultId ?? null,
|
|
523
|
+
defaultAccessGroupId: payload.defaultAccessGroupId ?? null,
|
|
524
|
+
headVersionId: payload.headVersionId ?? null,
|
|
525
|
+
metadata: payload.metadata ?? null,
|
|
526
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
527
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt"),
|
|
528
|
+
...resources,
|
|
529
|
+
access_group: resources.accessGroup
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function channelFromApi(payload, run) {
|
|
533
|
+
const channelId = String(payload.id);
|
|
534
|
+
return {
|
|
535
|
+
id: channelId,
|
|
536
|
+
appId: String(payload.appId),
|
|
537
|
+
type: payload.type,
|
|
538
|
+
slug: String(payload.slug),
|
|
539
|
+
name: String(payload.name),
|
|
540
|
+
webhook: payload.webhook ?? null,
|
|
541
|
+
data: payload.data ?? null,
|
|
542
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
543
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt"),
|
|
544
|
+
run: (options) => run(channelId, options)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function organizationFromApi(payload) {
|
|
548
|
+
return {
|
|
549
|
+
id: String(payload.id),
|
|
550
|
+
name: String(payload.name),
|
|
551
|
+
slug: String(payload.slug),
|
|
552
|
+
createdBy: String(payload.createdBy),
|
|
553
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
554
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt")
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function profileFromApi(payload, state) {
|
|
558
|
+
return {
|
|
559
|
+
id: String(payload.id),
|
|
560
|
+
appId: String(payload.appId),
|
|
561
|
+
identifier: String(payload.identifier),
|
|
562
|
+
currentSpending: Number(payload.currentSpending),
|
|
563
|
+
totalSpending: Number(payload.totalSpending),
|
|
564
|
+
metadata: payload.metadata ?? null,
|
|
565
|
+
headStateId: payload.headStateId ?? null,
|
|
566
|
+
initialChannelId: payload.initialChannelId ?? null,
|
|
567
|
+
accessGroupId: payload.accessGroupId ?? null,
|
|
568
|
+
lastResetAt: parseOptionalDate(payload.lastResetAt, "lastResetAt"),
|
|
569
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
570
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt"),
|
|
571
|
+
state
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function sessionFromApi(payload) {
|
|
575
|
+
return {
|
|
576
|
+
id: String(payload.id),
|
|
577
|
+
appId: String(payload.appId),
|
|
578
|
+
type: payload.type,
|
|
579
|
+
userIds: Array.isArray(payload.userIds) ? payload.userIds.map(String) : [],
|
|
580
|
+
profileIds: Array.isArray(payload.profileIds) ? payload.profileIds.map(String) : [],
|
|
581
|
+
metadata: payload.metadata ?? null,
|
|
582
|
+
externalId: payload.externalId ?? null,
|
|
583
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
584
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt")
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function instanceFromApi(payload) {
|
|
588
|
+
return {
|
|
589
|
+
id: String(payload.id),
|
|
590
|
+
appId: String(payload.appId),
|
|
591
|
+
versionId: String(payload.versionId),
|
|
592
|
+
channelId: String(payload.channelId),
|
|
593
|
+
sessionId: String(payload.sessionId),
|
|
594
|
+
profileId: String(payload.profileId),
|
|
595
|
+
accessGroupId: String(payload.accessGroupId),
|
|
596
|
+
stateId: payload.stateId ?? null,
|
|
597
|
+
identifier: String(payload.identifier),
|
|
598
|
+
timeout: Number(payload.timeout),
|
|
599
|
+
totalCost: Number(payload.totalCost),
|
|
600
|
+
costBreakdown: payload.costBreakdown ?? {},
|
|
601
|
+
tokenBreakdown: payload.tokenBreakdown ?? {},
|
|
602
|
+
metadata: payload.metadata ?? null,
|
|
603
|
+
status: payload.status,
|
|
604
|
+
error: payload.error ?? null,
|
|
605
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
606
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt")
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function conflictFromApi(payload) {
|
|
610
|
+
return {
|
|
611
|
+
id: String(payload.id),
|
|
612
|
+
appId: String(payload.appId),
|
|
613
|
+
syncId: String(payload.syncId),
|
|
614
|
+
profileId: String(payload.profileId),
|
|
615
|
+
versionId: String(payload.versionId),
|
|
616
|
+
stateId: String(payload.stateId),
|
|
617
|
+
stateBranch: String(payload.stateBranch),
|
|
618
|
+
reason: payload.reason,
|
|
619
|
+
error: payload.error ?? null,
|
|
620
|
+
createdAt: parseDate(payload.createdAt, "createdAt"),
|
|
621
|
+
updatedAt: parseDate(payload.updatedAt, "updatedAt")
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/internal/validation.ts
|
|
626
|
+
function requireNonEmptyString(value, name) {
|
|
627
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
628
|
+
throw new HarnesslayerError(`${name} must be a non-empty string`);
|
|
629
|
+
}
|
|
630
|
+
return value.trim();
|
|
631
|
+
}
|
|
632
|
+
function validateSlug(slug) {
|
|
633
|
+
const trimmedSlug = requireNonEmptyString(slug, "slug");
|
|
634
|
+
if (![...trimmedSlug].every((char) => /[A-Za-z0-9_-]/.test(char))) {
|
|
635
|
+
throw new HarnesslayerError(
|
|
636
|
+
"slug must be URL-safe (only contain letters, numbers, hyphens, and underscores)"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (/^[-_]|[-_]$/.test(trimmedSlug)) {
|
|
640
|
+
throw new HarnesslayerError("slug cannot start or end with a hyphen or underscore");
|
|
641
|
+
}
|
|
642
|
+
return trimmedSlug;
|
|
643
|
+
}
|
|
644
|
+
function validatePositiveInteger(value, name) {
|
|
645
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
646
|
+
throw new HarnesslayerError(`${name} must be a positive integer`);
|
|
647
|
+
}
|
|
648
|
+
return value;
|
|
649
|
+
}
|
|
650
|
+
function ensureObject(value, name) {
|
|
651
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
652
|
+
throw new HarnesslayerError(`${name} must be an object`);
|
|
653
|
+
}
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
function ensureStringMap(value, name) {
|
|
657
|
+
const object = ensureObject(value, name);
|
|
658
|
+
for (const [key, item] of Object.entries(object)) {
|
|
659
|
+
if (typeof key !== "string" || typeof item !== "string") {
|
|
660
|
+
throw new HarnesslayerError(`${name} must be an object of string keys and values`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return object;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/resources/base.ts
|
|
667
|
+
var ResourceBase = class {
|
|
668
|
+
client;
|
|
669
|
+
driftstone;
|
|
670
|
+
constructor(client) {
|
|
671
|
+
this.client = client;
|
|
672
|
+
this.driftstone = client.driftstone;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
var AppBase = class extends ResourceBase {
|
|
676
|
+
appId;
|
|
677
|
+
constructor(client, appId) {
|
|
678
|
+
super(client);
|
|
679
|
+
this.appId = appId;
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
var ProfileBase = class extends AppBase {
|
|
683
|
+
profileId;
|
|
684
|
+
constructor(client, appId, profileId) {
|
|
685
|
+
super(client, appId);
|
|
686
|
+
this.profileId = profileId;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/resources/access-group.ts
|
|
691
|
+
var HarnesslayerAccessGroup = class extends AppBase {
|
|
692
|
+
async create(name, limit, options = {}) {
|
|
693
|
+
const limitReset = options.limitReset;
|
|
694
|
+
if (!["daily", "monthly", "yearly"].includes(limitReset)) {
|
|
695
|
+
throw new HarnesslayerError("limitReset must be one of 'daily', 'monthly', or 'yearly'");
|
|
696
|
+
}
|
|
697
|
+
if (options.limitMessage != null && typeof options.limitMessage !== "string") {
|
|
698
|
+
throw new HarnesslayerError("limitMessage must be a string");
|
|
699
|
+
}
|
|
700
|
+
const payload = {
|
|
701
|
+
appId: this.appId,
|
|
702
|
+
name: requireNonEmptyString(name, "name"),
|
|
703
|
+
limit: validatePositiveInteger(limit, "limit"),
|
|
704
|
+
limitReset
|
|
705
|
+
};
|
|
706
|
+
if (options.limitMessage != null) payload.limitMessage = options.limitMessage;
|
|
707
|
+
const response = await this.client.request("POST", "/access_group/create", payload);
|
|
708
|
+
return accessGroupFromApi(response.data);
|
|
709
|
+
}
|
|
710
|
+
async retrieve(accessGroupId) {
|
|
711
|
+
try {
|
|
712
|
+
const id = requireNonEmptyString(accessGroupId, "accessGroupId");
|
|
713
|
+
const response = await this.client.request("GET", `/access_group/${id}`);
|
|
714
|
+
return accessGroupFromApi(response.data);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (error instanceof HarnesslayerAPIError || error instanceof HarnesslayerHTTPError || error instanceof HarnesslayerResponseError) {
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
if (error instanceof HarnesslayerError) return null;
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async update(accessGroupId, options) {
|
|
724
|
+
const id = requireNonEmptyString(accessGroupId, "accessGroupId");
|
|
725
|
+
const payload = {};
|
|
726
|
+
if (options.name != null) payload.name = requireNonEmptyString(options.name, "name");
|
|
727
|
+
if (options.limit != null) payload.limit = validatePositiveInteger(options.limit, "limit");
|
|
728
|
+
if (options.limitReset != null) {
|
|
729
|
+
if (!["daily", "monthly", "yearly"].includes(options.limitReset)) {
|
|
730
|
+
throw new HarnesslayerError("limitReset must be one of 'daily', 'monthly', or 'yearly'");
|
|
731
|
+
}
|
|
732
|
+
payload.limitReset = options.limitReset;
|
|
733
|
+
}
|
|
734
|
+
if (options.limitMessage != null) {
|
|
735
|
+
if (typeof options.limitMessage !== "string") throw new HarnesslayerError("limitMessage must be a string");
|
|
736
|
+
payload.limitMessage = options.limitMessage;
|
|
737
|
+
}
|
|
738
|
+
if (!Object.keys(payload).length) throw new HarnesslayerError("At least one field must be provided to update");
|
|
739
|
+
const response = await this.client.request("PATCH", `/access_group/${id}`, payload);
|
|
740
|
+
return accessGroupFromApi(response.data);
|
|
741
|
+
}
|
|
742
|
+
async list(options = {}) {
|
|
743
|
+
const response = await this.client.request("GET", "/access_group/list", {
|
|
744
|
+
appId: this.appId,
|
|
745
|
+
page: validatePositiveInteger(options.page ?? 1, "page"),
|
|
746
|
+
pageSize: validatePositiveInteger(options.pageSize ?? 10, "pageSize")
|
|
747
|
+
});
|
|
748
|
+
if (!Array.isArray(response.data)) {
|
|
749
|
+
throw new HarnesslayerError("Expected 'data' to be a list in access group list response");
|
|
750
|
+
}
|
|
751
|
+
return response.data.map((item) => accessGroupFromApi(item));
|
|
752
|
+
}
|
|
753
|
+
async delete(accessGroupId) {
|
|
754
|
+
const id = requireNonEmptyString(accessGroupId, "accessGroupId");
|
|
755
|
+
await this.client.request("DELETE", `/access_group/${id}`);
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// src/resources/channel.ts
|
|
761
|
+
var HarnesslayerChannel = class extends AppBase {
|
|
762
|
+
async init(slug, options) {
|
|
763
|
+
const payload = {
|
|
764
|
+
appId: this.appId,
|
|
765
|
+
type: options.type,
|
|
766
|
+
slug: validateSlug(slug),
|
|
767
|
+
returnIfExist: true
|
|
768
|
+
};
|
|
769
|
+
if (!["api", "imessage"].includes(options.type)) {
|
|
770
|
+
throw new HarnesslayerError("type must be either 'api' or 'imessage'");
|
|
771
|
+
}
|
|
772
|
+
if (options.name != null) payload.name = requireNonEmptyString(options.name, "name");
|
|
773
|
+
if (options.type === "api") {
|
|
774
|
+
if (options.webhook != null) {
|
|
775
|
+
this.validateWebhook(options.webhook);
|
|
776
|
+
payload.webhook = options.webhook;
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
this.validateIMessageData(options.data);
|
|
780
|
+
payload.data = options.data;
|
|
781
|
+
}
|
|
782
|
+
const response = await this.client.request("POST", "/channel/create", payload);
|
|
783
|
+
return channelFromApi(
|
|
784
|
+
response.data,
|
|
785
|
+
(channelId, runOptions) => this.run(channelId, runOptions)
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
async *run(channelId, options) {
|
|
789
|
+
const id = requireNonEmptyString(channelId, "channelId");
|
|
790
|
+
const userId = requireNonEmptyString(options.userId, "userId");
|
|
791
|
+
const input = options.input ?? "";
|
|
792
|
+
if (typeof input !== "string") throw new HarnesslayerError("input must be a string");
|
|
793
|
+
if (options.images != null && !Array.isArray(options.images)) throw new HarnesslayerError("images must be a list");
|
|
794
|
+
const channel = await this.client.request("GET", `/channel/${id}`);
|
|
795
|
+
const data = channel.data;
|
|
796
|
+
if (data.type === "imessage") {
|
|
797
|
+
throw new HarnesslayerError("You must text the iMessage number directly to trigger the channel");
|
|
798
|
+
}
|
|
799
|
+
const payload = {
|
|
800
|
+
appId: this.appId,
|
|
801
|
+
channelId: id,
|
|
802
|
+
sessionId: options.sessionId ?? null,
|
|
803
|
+
userId,
|
|
804
|
+
input
|
|
805
|
+
};
|
|
806
|
+
if (options.images?.length) payload.images = options.images;
|
|
807
|
+
const runResponse = await this.client.request("POST", "/response/run", payload);
|
|
808
|
+
const instanceId = String(runResponse.data.instanceId);
|
|
809
|
+
let nextIndex = 0;
|
|
810
|
+
let backoff = 150;
|
|
811
|
+
while (true) {
|
|
812
|
+
const poll = await this.client.request("GET", `/response/poll/${instanceId}`, {
|
|
813
|
+
after: nextIndex,
|
|
814
|
+
timeoutMs: 25e3
|
|
815
|
+
});
|
|
816
|
+
const pollData = poll.data;
|
|
817
|
+
const status = pollData.status ?? "running";
|
|
818
|
+
const events = Array.isArray(pollData.events) ? pollData.events : [];
|
|
819
|
+
for (const event of events) {
|
|
820
|
+
const index = Number(event.index);
|
|
821
|
+
if (index < nextIndex) continue;
|
|
822
|
+
nextIndex = index + 1;
|
|
823
|
+
yield {
|
|
824
|
+
instanceId,
|
|
825
|
+
index,
|
|
826
|
+
data: safeParseJson(event.data) ?? { type: "unknown" },
|
|
827
|
+
done: status !== "running",
|
|
828
|
+
error: event.error ?? null
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
if (status !== "running") return;
|
|
832
|
+
if (events.length) {
|
|
833
|
+
backoff = 150;
|
|
834
|
+
} else {
|
|
835
|
+
await sleep(backoff);
|
|
836
|
+
backoff = Math.min(backoff * 1.5, 1500);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
validateWebhook(webhook) {
|
|
841
|
+
if (!webhook || typeof webhook !== "object" || Array.isArray(webhook)) {
|
|
842
|
+
throw new HarnesslayerError("webhook must be an object with 'name', 'url', and optional 'headers'");
|
|
843
|
+
}
|
|
844
|
+
requireNonEmptyString(webhook.name, "webhook.name");
|
|
845
|
+
requireNonEmptyString(webhook.url, "webhook.url");
|
|
846
|
+
if (webhook.headers != null) ensureStringMap(webhook.headers, "webhook.headers");
|
|
847
|
+
}
|
|
848
|
+
validateIMessageData(data) {
|
|
849
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
850
|
+
throw new HarnesslayerError("data must be an object with iMessage credentials");
|
|
851
|
+
}
|
|
852
|
+
for (const field of ["sendBlueAPIKey", "sendBlueSecretKey", "fromNumber"]) {
|
|
853
|
+
requireNonEmptyString(data[field], `data.${field}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// src/resources/conflict.ts
|
|
859
|
+
var HarnesslayerConflict = class extends AppBase {
|
|
860
|
+
async retrieve(conflictId) {
|
|
861
|
+
try {
|
|
862
|
+
const response = await this.client.request("GET", `/conflict/${requireNonEmptyString(conflictId, "conflictId")}`);
|
|
863
|
+
return conflictFromApi(response.data);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
if (error instanceof HarnesslayerAPIError || error instanceof HarnesslayerHTTPError || error instanceof HarnesslayerResponseError) {
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
if (error instanceof HarnesslayerError) return null;
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
async list(options = {}) {
|
|
873
|
+
const response = await this.client.request("GET", "/conflict/list", {
|
|
874
|
+
appId: this.appId,
|
|
875
|
+
page: validatePositiveInteger(options.page ?? 1, "page"),
|
|
876
|
+
pageSize: validatePositiveInteger(options.pageSize ?? 10, "pageSize")
|
|
877
|
+
});
|
|
878
|
+
if (!Array.isArray(response.data)) throw new HarnesslayerError("Expected 'data' to be a list in conflict list response");
|
|
879
|
+
return response.data.map((item) => conflictFromApi(item));
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// src/resources/instance.ts
|
|
884
|
+
var HarnesslayerInstance = class extends AppBase {
|
|
885
|
+
async retrieve(instanceId) {
|
|
886
|
+
try {
|
|
887
|
+
const response = await this.client.request("GET", `/instance/${requireNonEmptyString(instanceId, "instanceId")}`);
|
|
888
|
+
return instanceFromApi(response.data);
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if (error instanceof HarnesslayerAPIError || error instanceof HarnesslayerHTTPError || error instanceof HarnesslayerResponseError) {
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
if (error instanceof HarnesslayerError) return null;
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async list(options = {}) {
|
|
898
|
+
const response = await this.client.request("GET", "/instance/list", {
|
|
899
|
+
appId: this.appId,
|
|
900
|
+
page: validatePositiveInteger(options.page ?? 1, "page"),
|
|
901
|
+
pageSize: validatePositiveInteger(options.pageSize ?? 10, "pageSize")
|
|
902
|
+
});
|
|
903
|
+
if (!Array.isArray(response.data)) throw new HarnesslayerError("Expected 'data' to be a list in instance list response");
|
|
904
|
+
return response.data.map((item) => instanceFromApi(item));
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// src/internal/zip.ts
|
|
909
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
910
|
+
import path3 from "path";
|
|
911
|
+
import { unzipSync } from "fflate";
|
|
912
|
+
async function extractZip(bytes, outPath) {
|
|
913
|
+
const outDir = path3.resolve(outPath);
|
|
914
|
+
const entries = unzipSync(bytes);
|
|
915
|
+
for (const [entryName, data] of Object.entries(entries)) {
|
|
916
|
+
const target = path3.resolve(outDir, entryName);
|
|
917
|
+
if (target !== outDir && !target.startsWith(`${outDir}${path3.sep}`)) {
|
|
918
|
+
throw new HarnesslayerError(`Archive entry escapes output directory: ${entryName}`);
|
|
919
|
+
}
|
|
920
|
+
if (entryName.endsWith("/")) {
|
|
921
|
+
await mkdir(target, { recursive: true });
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
await mkdir(path3.dirname(target), { recursive: true });
|
|
925
|
+
await writeFile(target, data);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/resources/state.ts
|
|
930
|
+
var HarnesslayerState = class extends ProfileBase {
|
|
931
|
+
async create(options) {
|
|
932
|
+
if (!options.stateId && !options.statePath) {
|
|
933
|
+
throw new HarnesslayerError("Either stateId or statePath must be provided");
|
|
934
|
+
}
|
|
935
|
+
const payload = { profileId: this.profileId };
|
|
936
|
+
if (options.stateId) {
|
|
937
|
+
if (!options.stateId.startsWith("state-")) throw new HarnesslayerError("stateId is invalid");
|
|
938
|
+
payload.stateId = options.stateId;
|
|
939
|
+
} else if (options.statePath) {
|
|
940
|
+
const stateId = newStateId();
|
|
941
|
+
const pathData = await validateConfigPath(options.statePath);
|
|
942
|
+
const files = await createUploadFilesPayload(pathData.path);
|
|
943
|
+
const repo = await this.driftstone.repos.get(this.appId);
|
|
944
|
+
const upload = await this.driftstone.repos.createUpload(repo.id, {
|
|
945
|
+
branch: this.profileId,
|
|
946
|
+
storageDir: stateId,
|
|
947
|
+
files
|
|
948
|
+
});
|
|
949
|
+
await this.driftstone.uploadFiles(upload, files, pathData.path);
|
|
950
|
+
await this.driftstone.repos.completeUpload(repo.id, upload.uploadId);
|
|
951
|
+
payload.stateId = stateId;
|
|
952
|
+
}
|
|
953
|
+
await this.client.request("POST", "/state/create", payload);
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
async download(outPath, stateId) {
|
|
957
|
+
const targetStateId = await this.resolveStateId(stateId);
|
|
958
|
+
const response = await this.client.request("POST", `/state/download/${targetStateId}`);
|
|
959
|
+
const url = response.data.url;
|
|
960
|
+
if (typeof url !== "string" || !url.trim()) throw new HarnesslayerResponseError("Missing download URL for state");
|
|
961
|
+
const archive = await fetch(url, { redirect: "follow" });
|
|
962
|
+
if (archive.status >= 400) throw new HarnesslayerAPIError("Failed to download state archive");
|
|
963
|
+
await extractZip(new Uint8Array(await archive.arrayBuffer()), outPath);
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
async clone(options = {}) {
|
|
967
|
+
try {
|
|
968
|
+
const transforms = validateCloneTransform(options.transform);
|
|
969
|
+
const newId = newStateId();
|
|
970
|
+
const targetStateId = await this.tryResolveStateId(options.stateId);
|
|
971
|
+
if (targetStateId) {
|
|
972
|
+
if (!targetStateId.startsWith("state-")) throw new HarnesslayerError("stateId is invalid");
|
|
973
|
+
const copyJob = await this.driftstone.repos.copyDirectory(this.appId, {
|
|
974
|
+
sourceDir: targetStateId,
|
|
975
|
+
destDir: newId,
|
|
976
|
+
sourceBranch: this.profileId,
|
|
977
|
+
destBranch: this.profileId,
|
|
978
|
+
transforms,
|
|
979
|
+
archive: true
|
|
980
|
+
});
|
|
981
|
+
const status = await this.waitForCopyRun(copyJob.runId, "state clone");
|
|
982
|
+
if (status?.result?.noChanges === true) return false;
|
|
983
|
+
} else {
|
|
984
|
+
const app = await this.client.request("GET", `/app/${this.appId}`);
|
|
985
|
+
const headVersionId = app.data.headVersionId;
|
|
986
|
+
if (typeof headVersionId !== "string" || !headVersionId.trim()) {
|
|
987
|
+
throw new HarnesslayerResponseError("Missing headVersionId for app");
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
const branch = await this.driftstone.repos.createBranch(this.appId, this.profileId, {
|
|
991
|
+
storageDir: newId,
|
|
992
|
+
transforms,
|
|
993
|
+
archive: true
|
|
994
|
+
});
|
|
995
|
+
await this.waitForCopyRun(branch.runId ?? null, "state clone");
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (!(error instanceof Error) || !error.message.toLowerCase().includes("same name already exists")) {
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
const copyJob = await this.driftstone.repos.copyDirectory(this.appId, {
|
|
1001
|
+
sourceDir: headVersionId.trim(),
|
|
1002
|
+
destDir: newId,
|
|
1003
|
+
sourceBranch: "main",
|
|
1004
|
+
destBranch: this.profileId,
|
|
1005
|
+
transforms,
|
|
1006
|
+
archive: true
|
|
1007
|
+
});
|
|
1008
|
+
const status = await this.waitForCopyRun(copyJob.runId, "state clone");
|
|
1009
|
+
if (status?.result?.noChanges === true) return false;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
await this.client.request("POST", "/state/create", { profileId: this.profileId, stateId: newId });
|
|
1013
|
+
return true;
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
if (error instanceof HarnesslayerError) return false;
|
|
1016
|
+
throw error;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async waitForCopyRun(runId, operation) {
|
|
1020
|
+
if (!runId) return null;
|
|
1021
|
+
const deadline = Date.now() + Math.max(this.client.timeoutMs, 1e3);
|
|
1022
|
+
while (true) {
|
|
1023
|
+
const status = await this.driftstone.repos.getCopyStatus(this.appId, runId);
|
|
1024
|
+
if (status.status === "completed") return status;
|
|
1025
|
+
if (status.status === "failed") {
|
|
1026
|
+
throw new HarnesslayerAPIError(String(status.result?.error ?? `${operation} failed`));
|
|
1027
|
+
}
|
|
1028
|
+
if (status.status !== "running") {
|
|
1029
|
+
throw new HarnesslayerResponseError(`Unexpected ${operation} status: ${status.status}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (Date.now() >= deadline) {
|
|
1032
|
+
throw new HarnesslayerAPIError(`Timed out waiting for ${operation} run ${runId}`);
|
|
1033
|
+
}
|
|
1034
|
+
await sleep(1e3);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
async resolveStateId(stateId) {
|
|
1038
|
+
const target = await this.tryResolveStateId(stateId);
|
|
1039
|
+
if (!target) throw new HarnesslayerError("stateId must be provided or profile must have a head state");
|
|
1040
|
+
if (!target.startsWith("state-")) throw new HarnesslayerError("stateId is invalid");
|
|
1041
|
+
return target;
|
|
1042
|
+
}
|
|
1043
|
+
async tryResolveStateId(stateId) {
|
|
1044
|
+
let target = typeof stateId === "string" ? stateId.trim() : "";
|
|
1045
|
+
if (!target) {
|
|
1046
|
+
const profile = await this.client.request("GET", `/profile/${this.profileId}`);
|
|
1047
|
+
const headStateId = profile.data.headStateId;
|
|
1048
|
+
if (typeof headStateId === "string" && headStateId.trim()) target = headStateId.trim();
|
|
1049
|
+
}
|
|
1050
|
+
return target || null;
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// src/resources/profile.ts
|
|
1055
|
+
var HarnesslayerProfile = class extends AppBase {
|
|
1056
|
+
async init(identifier, options = {}) {
|
|
1057
|
+
const payload = {
|
|
1058
|
+
appId: this.appId,
|
|
1059
|
+
identifier: requireNonEmptyString(identifier, "identifier"),
|
|
1060
|
+
returnIfExist: true
|
|
1061
|
+
};
|
|
1062
|
+
if (options.accessGroupId != null) payload.accessGroupId = requireNonEmptyString(options.accessGroupId, "accessGroupId");
|
|
1063
|
+
if (options.metadata != null) {
|
|
1064
|
+
if (typeof options.metadata !== "object" || Array.isArray(options.metadata)) {
|
|
1065
|
+
throw new HarnesslayerError("metadata must be an object");
|
|
1066
|
+
}
|
|
1067
|
+
payload.metadata = options.metadata;
|
|
1068
|
+
}
|
|
1069
|
+
let pathData = null;
|
|
1070
|
+
if (options.statePath != null) {
|
|
1071
|
+
pathData = await validateConfigPath(requireNonEmptyString(options.statePath, "statePath"));
|
|
1072
|
+
const app = await this.client.request("GET", `/app/${this.appId}`);
|
|
1073
|
+
const appType = app.data.type;
|
|
1074
|
+
if (pathData.type !== appType) {
|
|
1075
|
+
throw new HarnesslayerError(`app type '${String(appType)}' does not match statePath type '${pathData.type}'`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const response = await this.client.request("POST", "/profile/create", payload);
|
|
1079
|
+
const data = response.data;
|
|
1080
|
+
const state = new HarnesslayerState(this.client, this.appId, String(data.id));
|
|
1081
|
+
const profile = profileFromApi(data, state);
|
|
1082
|
+
if (!response.exists) {
|
|
1083
|
+
const stateId = newStateId();
|
|
1084
|
+
if (!pathData) {
|
|
1085
|
+
const branch = await this.driftstone.repos.createBranch(this.appId, String(data.id), {
|
|
1086
|
+
storageDir: stateId,
|
|
1087
|
+
archive: true
|
|
1088
|
+
});
|
|
1089
|
+
await state.waitForCopyRun(branch.runId ?? null, "profile init");
|
|
1090
|
+
await state.create({ stateId });
|
|
1091
|
+
} else {
|
|
1092
|
+
await this.driftstone.repos.createBranch(this.appId, String(data.id), {
|
|
1093
|
+
storageDir: newStateId()
|
|
1094
|
+
});
|
|
1095
|
+
const files = await createUploadFilesPayload(pathData.path);
|
|
1096
|
+
const repo = await this.driftstone.repos.get(this.appId);
|
|
1097
|
+
const upload = await this.driftstone.repos.createUpload(repo.id, {
|
|
1098
|
+
branch: String(data.id),
|
|
1099
|
+
storageDir: stateId,
|
|
1100
|
+
files
|
|
1101
|
+
});
|
|
1102
|
+
await this.driftstone.uploadFiles(upload, files, pathData.path);
|
|
1103
|
+
await this.driftstone.repos.completeUpload(repo.id, upload.uploadId);
|
|
1104
|
+
await state.create({ stateId });
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return profile;
|
|
1108
|
+
}
|
|
1109
|
+
async retrieve(profileId) {
|
|
1110
|
+
try {
|
|
1111
|
+
const id = requireNonEmptyString(profileId, "profileId");
|
|
1112
|
+
const response = await this.client.request("GET", `/profile/${id}`);
|
|
1113
|
+
const state = new HarnesslayerState(this.client, this.appId, id);
|
|
1114
|
+
return profileFromApi(response.data, state);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
if (error instanceof HarnesslayerError) return null;
|
|
1117
|
+
throw error;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async update(profileId, options) {
|
|
1121
|
+
const id = requireNonEmptyString(profileId, "profileId");
|
|
1122
|
+
const payload = {};
|
|
1123
|
+
if (options.identifier != null) payload.identifier = requireNonEmptyString(options.identifier, "identifier");
|
|
1124
|
+
if (options.accessGroupId != null) payload.accessGroupId = requireNonEmptyString(options.accessGroupId, "accessGroupId");
|
|
1125
|
+
if (options.metadata != null) {
|
|
1126
|
+
if (typeof options.metadata !== "object" || Array.isArray(options.metadata)) throw new HarnesslayerError("metadata must be an object");
|
|
1127
|
+
payload.metadata = options.metadata;
|
|
1128
|
+
}
|
|
1129
|
+
if (!Object.keys(payload).length) throw new HarnesslayerError("At least one field must be provided to update");
|
|
1130
|
+
const response = await this.client.request("PATCH", `/profile/${id}`, payload);
|
|
1131
|
+
const state = new HarnesslayerState(this.client, this.appId, String(response.data.id));
|
|
1132
|
+
return profileFromApi(response.data, state);
|
|
1133
|
+
}
|
|
1134
|
+
state(profileId) {
|
|
1135
|
+
return new HarnesslayerState(this.client, this.appId, requireNonEmptyString(profileId, "profileId"));
|
|
1136
|
+
}
|
|
1137
|
+
async delete(profileId) {
|
|
1138
|
+
await this.client.request("DELETE", `/profile/${requireNonEmptyString(profileId, "profileId")}`);
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
// src/resources/session.ts
|
|
1144
|
+
var HarnesslayerSession = class extends AppBase {
|
|
1145
|
+
async create(type) {
|
|
1146
|
+
if (!["api", "imessage"].includes(type)) throw new HarnesslayerError("type must be either 'api' or 'imessage'");
|
|
1147
|
+
const response = await this.client.request("POST", "/session/create", { appId: this.appId, type });
|
|
1148
|
+
return sessionFromApi(response.data);
|
|
1149
|
+
}
|
|
1150
|
+
async retrieve(sessionId) {
|
|
1151
|
+
try {
|
|
1152
|
+
const response = await this.client.request("GET", `/session/${requireNonEmptyString(sessionId, "sessionId")}`);
|
|
1153
|
+
return sessionFromApi(response.data);
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
if (error instanceof HarnesslayerAPIError || error instanceof HarnesslayerHTTPError || error instanceof HarnesslayerResponseError) {
|
|
1156
|
+
throw error;
|
|
1157
|
+
}
|
|
1158
|
+
if (error instanceof HarnesslayerError) return null;
|
|
1159
|
+
throw error;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async update(sessionId, options) {
|
|
1163
|
+
const payload = {};
|
|
1164
|
+
if (options.type != null) {
|
|
1165
|
+
if (!["single", "multiple"].includes(options.type)) throw new HarnesslayerError("type must be either 'single' or 'multiple'");
|
|
1166
|
+
payload.type = options.type;
|
|
1167
|
+
}
|
|
1168
|
+
if (options.userIds != null) {
|
|
1169
|
+
if (!Array.isArray(options.userIds) || !options.userIds.every((id) => typeof id === "string" && id.trim())) {
|
|
1170
|
+
throw new HarnesslayerError("userIds must be a list of non-empty strings");
|
|
1171
|
+
}
|
|
1172
|
+
payload.userIds = options.userIds;
|
|
1173
|
+
}
|
|
1174
|
+
if (!Object.keys(payload).length) throw new HarnesslayerError("At least one of 'type' or 'userIds' must be provided");
|
|
1175
|
+
const response = await this.client.request("PATCH", `/session/${requireNonEmptyString(sessionId, "sessionId")}`, payload);
|
|
1176
|
+
return sessionFromApi(response.data);
|
|
1177
|
+
}
|
|
1178
|
+
async list(options = {}) {
|
|
1179
|
+
const response = await this.client.request("GET", "/session/list", {
|
|
1180
|
+
appId: this.appId,
|
|
1181
|
+
page: validatePositiveInteger(options.page ?? 1, "page"),
|
|
1182
|
+
pageSize: validatePositiveInteger(options.pageSize ?? 10, "pageSize")
|
|
1183
|
+
});
|
|
1184
|
+
if (!Array.isArray(response.data)) throw new HarnesslayerError("Expected 'data' to be a list in session list response");
|
|
1185
|
+
return response.data.map((item) => sessionFromApi(item));
|
|
1186
|
+
}
|
|
1187
|
+
async delete(sessionId) {
|
|
1188
|
+
await this.client.request("DELETE", `/session/${requireNonEmptyString(sessionId, "sessionId")}`);
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
// src/resources/version.ts
|
|
1194
|
+
var HarnesslayerVersion = class extends AppBase {
|
|
1195
|
+
async create(options) {
|
|
1196
|
+
if (!options.versionId && !options.versionPath) {
|
|
1197
|
+
throw new HarnesslayerError("Either versionId or versionPath must be provided");
|
|
1198
|
+
}
|
|
1199
|
+
const payload = { appId: this.appId };
|
|
1200
|
+
if (options.versionId) {
|
|
1201
|
+
if (!options.versionId.startsWith("ver-")) throw new HarnesslayerError("versionId is invalid");
|
|
1202
|
+
payload.versionId = options.versionId;
|
|
1203
|
+
} else if (options.versionPath) {
|
|
1204
|
+
const versionId = newVersionId();
|
|
1205
|
+
const pathData = await validateConfigPath(options.versionPath);
|
|
1206
|
+
const files = await createUploadFilesPayload(pathData.path);
|
|
1207
|
+
const repo = await this.driftstone.repos.get(this.appId);
|
|
1208
|
+
const upload = await this.driftstone.repos.createUpload(repo.id, {
|
|
1209
|
+
branch: "main",
|
|
1210
|
+
storageDir: versionId,
|
|
1211
|
+
files
|
|
1212
|
+
});
|
|
1213
|
+
await this.driftstone.uploadFiles(upload, files, pathData.path);
|
|
1214
|
+
await this.driftstone.repos.completeUpload(repo.id, upload.uploadId);
|
|
1215
|
+
payload.versionId = versionId;
|
|
1216
|
+
}
|
|
1217
|
+
await this.client.request("POST", "/version/create", payload);
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
async download(outPath, versionId) {
|
|
1221
|
+
const targetVersionId = await this.resolveVersionId(versionId);
|
|
1222
|
+
const response = await this.client.request("GET", `/version/download/${targetVersionId}`);
|
|
1223
|
+
const url = response.data.url;
|
|
1224
|
+
if (typeof url !== "string" || !url.trim()) {
|
|
1225
|
+
throw new HarnesslayerResponseError("Missing download URL for version");
|
|
1226
|
+
}
|
|
1227
|
+
const archive = await fetch(url, { redirect: "follow" });
|
|
1228
|
+
if (archive.status >= 400) throw new HarnesslayerAPIError("Failed to download version archive");
|
|
1229
|
+
await extractZip(new Uint8Array(await archive.arrayBuffer()), outPath);
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
async clone(options = {}) {
|
|
1233
|
+
try {
|
|
1234
|
+
const targetVersionId = await this.resolveVersionId(options.versionId);
|
|
1235
|
+
const newId = newVersionId();
|
|
1236
|
+
const copyJob = await this.driftstone.repos.copyDirectory(this.appId, {
|
|
1237
|
+
sourceDir: targetVersionId,
|
|
1238
|
+
destDir: newId,
|
|
1239
|
+
transforms: validateCloneTransform(options.transform),
|
|
1240
|
+
archive: true
|
|
1241
|
+
});
|
|
1242
|
+
const deadline = Date.now() + Math.max(this.client.timeoutMs, 1e3);
|
|
1243
|
+
while (true) {
|
|
1244
|
+
const status = await this.driftstone.repos.getCopyStatus(this.appId, copyJob.runId);
|
|
1245
|
+
if (status.status === "completed") {
|
|
1246
|
+
if (status.result?.noChanges === true) return false;
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
if (status.status === "failed") {
|
|
1250
|
+
throw new HarnesslayerAPIError(String(status.result?.error ?? "Version clone failed"));
|
|
1251
|
+
}
|
|
1252
|
+
if (status.status !== "running") {
|
|
1253
|
+
throw new HarnesslayerResponseError(`Unexpected version clone status: ${status.status}`);
|
|
1254
|
+
}
|
|
1255
|
+
if (Date.now() >= deadline) {
|
|
1256
|
+
throw new HarnesslayerAPIError(`Timed out waiting for version clone run ${copyJob.runId}`);
|
|
1257
|
+
}
|
|
1258
|
+
await sleep(1e3);
|
|
1259
|
+
}
|
|
1260
|
+
await this.client.request("POST", "/version/create", { appId: this.appId, versionId: newId });
|
|
1261
|
+
return true;
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
if (error instanceof HarnesslayerError) return false;
|
|
1264
|
+
throw error;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async sync(strategy = "manual") {
|
|
1268
|
+
if (!["manual", "current", "incoming", "smart"].includes(strategy)) {
|
|
1269
|
+
throw new HarnesslayerError("Invalid sync strategy");
|
|
1270
|
+
}
|
|
1271
|
+
const response = await this.client.request("POST", "/version/sync", {
|
|
1272
|
+
appId: this.appId,
|
|
1273
|
+
strategy
|
|
1274
|
+
});
|
|
1275
|
+
if (response.error) throw new HarnesslayerAPIError(`Failed to sync version: ${String(response.error)}`);
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
async resolveVersionId(versionId) {
|
|
1279
|
+
let target = typeof versionId === "string" ? versionId.trim() : "";
|
|
1280
|
+
if (!target) {
|
|
1281
|
+
const app = await this.client.request("GET", `/app/${this.appId}`);
|
|
1282
|
+
const headVersionId = app.data.headVersionId;
|
|
1283
|
+
if (typeof headVersionId !== "string" || !headVersionId.trim()) {
|
|
1284
|
+
throw new HarnesslayerResponseError("Missing headVersionId for app");
|
|
1285
|
+
}
|
|
1286
|
+
target = headVersionId.trim();
|
|
1287
|
+
}
|
|
1288
|
+
if (!target.startsWith("ver-")) throw new HarnesslayerError("versionId is invalid");
|
|
1289
|
+
return target;
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
// src/resources/app.ts
|
|
1294
|
+
var HarnesslayerApp = class extends ResourceBase {
|
|
1295
|
+
async init(slug, options) {
|
|
1296
|
+
if (!["claude", "openclaw"].includes(options.type)) {
|
|
1297
|
+
throw new HarnesslayerError("type must be one of 'claude', 'openclaw'");
|
|
1298
|
+
}
|
|
1299
|
+
const payload = {
|
|
1300
|
+
slug: validateSlug(slug),
|
|
1301
|
+
type: options.type,
|
|
1302
|
+
returnIfExist: true
|
|
1303
|
+
};
|
|
1304
|
+
if (options.name != null) payload.name = requireNonEmptyString(options.name, "name");
|
|
1305
|
+
if (options.limit != null) {
|
|
1306
|
+
if (typeof options.limit !== "object" || Array.isArray(options.limit) || options.limit.amount == null || options.limit.interval == null) {
|
|
1307
|
+
throw new HarnesslayerError("limit must be an object with 'amount' and 'interval' keys");
|
|
1308
|
+
}
|
|
1309
|
+
payload.limit = options.limit;
|
|
1310
|
+
}
|
|
1311
|
+
if (options.credentials != null) {
|
|
1312
|
+
if (!Array.isArray(options.credentials) || !options.credentials.every((item) => item && typeof item === "object")) {
|
|
1313
|
+
throw new HarnesslayerError("credentials must be a list of objects");
|
|
1314
|
+
}
|
|
1315
|
+
payload.credentials = options.credentials;
|
|
1316
|
+
}
|
|
1317
|
+
const createResponse = await this.client.request("POST", "/app/create", payload);
|
|
1318
|
+
const app = this.buildAppModel(createResponse.data);
|
|
1319
|
+
if (!createResponse.exists) {
|
|
1320
|
+
const pathData = await validateConfigPath(requireNonEmptyString(options.versionPath, "versionPath"));
|
|
1321
|
+
if (pathData.type !== options.type) {
|
|
1322
|
+
throw new HarnesslayerError(`type '${options.type}' does not match versionPath type '${pathData.type}'`);
|
|
1323
|
+
}
|
|
1324
|
+
const versionId = newVersionId();
|
|
1325
|
+
const files = await createUploadFilesPayload(pathData.path);
|
|
1326
|
+
const repo = await this.driftstone.repos.create(String(createResponse.data.id));
|
|
1327
|
+
const upload = await this.driftstone.repos.createUpload(repo.id, {
|
|
1328
|
+
branch: "main",
|
|
1329
|
+
storageDir: versionId,
|
|
1330
|
+
files
|
|
1331
|
+
});
|
|
1332
|
+
await this.driftstone.uploadFiles(upload, files, pathData.path);
|
|
1333
|
+
await this.driftstone.repos.completeUpload(repo.id, upload.uploadId);
|
|
1334
|
+
await app.version.create({ versionId });
|
|
1335
|
+
}
|
|
1336
|
+
return app;
|
|
1337
|
+
}
|
|
1338
|
+
async retrieve(options) {
|
|
1339
|
+
try {
|
|
1340
|
+
const appId = options.appId?.trim();
|
|
1341
|
+
const slug = options.slug?.trim();
|
|
1342
|
+
if (!appId && !slug) throw new HarnesslayerError("Either appId or slug must be provided");
|
|
1343
|
+
if (appId && slug) throw new HarnesslayerError("Only one of appId or slug can be provided, not both");
|
|
1344
|
+
const response = await this.client.request("GET", `/app/${appId ?? slug}`);
|
|
1345
|
+
return this.buildAppModel(response.data);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (error instanceof HarnesslayerError) return null;
|
|
1348
|
+
throw error;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
async delete(appId) {
|
|
1352
|
+
await this.client.request("DELETE", `/app/${requireNonEmptyString(appId, "appId")}`);
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
channel(appId) {
|
|
1356
|
+
return new HarnesslayerChannel(this.client, requireNonEmptyString(appId, "appId"));
|
|
1357
|
+
}
|
|
1358
|
+
instance(appId) {
|
|
1359
|
+
return new HarnesslayerInstance(this.client, requireNonEmptyString(appId, "appId"));
|
|
1360
|
+
}
|
|
1361
|
+
accessGroup(appId) {
|
|
1362
|
+
return new HarnesslayerAccessGroup(this.client, requireNonEmptyString(appId, "appId"));
|
|
1363
|
+
}
|
|
1364
|
+
profile(appId) {
|
|
1365
|
+
return new HarnesslayerProfile(this.client, requireNonEmptyString(appId, "appId"));
|
|
1366
|
+
}
|
|
1367
|
+
conflict(appId) {
|
|
1368
|
+
return new HarnesslayerConflict(this.client, requireNonEmptyString(appId, "appId"));
|
|
1369
|
+
}
|
|
1370
|
+
buildAppModel(appData) {
|
|
1371
|
+
const appId = String(appData.id);
|
|
1372
|
+
return appFromApi(appData, {
|
|
1373
|
+
version: new HarnesslayerVersion(this.client, appId),
|
|
1374
|
+
channel: new HarnesslayerChannel(this.client, appId),
|
|
1375
|
+
session: new HarnesslayerSession(this.client, appId),
|
|
1376
|
+
accessGroup: new HarnesslayerAccessGroup(this.client, appId),
|
|
1377
|
+
profile: new HarnesslayerProfile(this.client, appId),
|
|
1378
|
+
instance: new HarnesslayerInstance(this.client, appId),
|
|
1379
|
+
conflict: new HarnesslayerConflict(this.client, appId)
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
// src/resources/organization.ts
|
|
1385
|
+
var HarnesslayerOrganization = class extends ResourceBase {
|
|
1386
|
+
async create(name, options) {
|
|
1387
|
+
const response = await this.client.request("POST", "/organization/create", {
|
|
1388
|
+
name: requireNonEmptyString(name, "name"),
|
|
1389
|
+
slug: validateSlug(options.slug)
|
|
1390
|
+
});
|
|
1391
|
+
return organizationFromApi(response.data);
|
|
1392
|
+
}
|
|
1393
|
+
async init(name, options) {
|
|
1394
|
+
if (options.returnIfExist != null && options.return_if_exist != null) {
|
|
1395
|
+
throw new HarnesslayerError("Only one of returnIfExist or return_if_exist may be provided");
|
|
1396
|
+
}
|
|
1397
|
+
const response = await this.client.request("POST", "/organization/create", {
|
|
1398
|
+
name: requireNonEmptyString(name, "name"),
|
|
1399
|
+
slug: validateSlug(options.slug),
|
|
1400
|
+
returnIfExist: options.returnIfExist ?? options.return_if_exist ?? false
|
|
1401
|
+
});
|
|
1402
|
+
return organizationFromApi(response.data);
|
|
1403
|
+
}
|
|
1404
|
+
async update(organizationId, options) {
|
|
1405
|
+
const payload = {};
|
|
1406
|
+
if (options.name != null) payload.name = requireNonEmptyString(options.name, "name");
|
|
1407
|
+
if (options.slug != null) payload.slug = validateSlug(options.slug);
|
|
1408
|
+
if (!Object.keys(payload).length) throw new HarnesslayerError("At least one field must be provided to update");
|
|
1409
|
+
const response = await this.client.request(
|
|
1410
|
+
"PATCH",
|
|
1411
|
+
`/organization/${requireNonEmptyString(organizationId, "organizationId")}`,
|
|
1412
|
+
payload
|
|
1413
|
+
);
|
|
1414
|
+
return organizationFromApi(response.data);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// src/resources/response.ts
|
|
1419
|
+
var HarnesslayerResponse = class extends ResourceBase {
|
|
1420
|
+
async stop(instanceId) {
|
|
1421
|
+
const response = await this.client.request("POST", "/response/stop", {
|
|
1422
|
+
instanceId: requireNonEmptyString(instanceId, "instanceId")
|
|
1423
|
+
});
|
|
1424
|
+
if (!response.data || typeof response.data !== "object" || Array.isArray(response.data)) {
|
|
1425
|
+
throw new HarnesslayerError("Expected 'data' to be an object in response stop response");
|
|
1426
|
+
}
|
|
1427
|
+
return response.data;
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
// src/client.ts
|
|
1432
|
+
var Harnesslayer = class {
|
|
1433
|
+
apiKey;
|
|
1434
|
+
apiVersion;
|
|
1435
|
+
timeoutMs;
|
|
1436
|
+
baseUrl;
|
|
1437
|
+
driftstone;
|
|
1438
|
+
transport;
|
|
1439
|
+
constructor(apiKeyOrOptions, maybeOptions = {}) {
|
|
1440
|
+
const options = typeof apiKeyOrOptions === "string" ? { ...maybeOptions, apiKey: apiKeyOrOptions } : apiKeyOrOptions;
|
|
1441
|
+
const apiKey = options.apiKey.trim();
|
|
1442
|
+
if (!apiKey) throw new HarnesslayerError("apiKey must be provided");
|
|
1443
|
+
if (!apiKey.startsWith("dk-")) throw new HarnesslayerError("Invalid Harnesslayer API key");
|
|
1444
|
+
this.apiKey = apiKey;
|
|
1445
|
+
this.apiVersion = options.version ?? "v1";
|
|
1446
|
+
this.timeoutMs = options.timeoutMs ?? (options.timeout != null ? options.timeout * 1e3 : 6e5);
|
|
1447
|
+
this.baseUrl = `${(options.baseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "")}/${this.apiVersion}`;
|
|
1448
|
+
const transportOptions = {
|
|
1449
|
+
baseUrl: this.baseUrl,
|
|
1450
|
+
apiKey: this.apiKey,
|
|
1451
|
+
timeoutMs: this.timeoutMs,
|
|
1452
|
+
userAgent: "Harnesslayer-typescript/0.1.0",
|
|
1453
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
1454
|
+
};
|
|
1455
|
+
this.transport = new Transport(transportOptions);
|
|
1456
|
+
const driftstoneOptions = {
|
|
1457
|
+
apiKey: this.apiKey,
|
|
1458
|
+
version: this.apiVersion,
|
|
1459
|
+
timeoutMs: this.timeoutMs,
|
|
1460
|
+
...options.fetch ? { fetch: options.fetch } : {},
|
|
1461
|
+
...options.driftstoneBaseUrl ? { baseUrl: options.driftstoneBaseUrl } : {}
|
|
1462
|
+
};
|
|
1463
|
+
this.driftstone = new DriftstoneClient(driftstoneOptions);
|
|
1464
|
+
}
|
|
1465
|
+
request(method, path4, payload) {
|
|
1466
|
+
return this.transport.request(method, path4, payload);
|
|
1467
|
+
}
|
|
1468
|
+
get app() {
|
|
1469
|
+
return new HarnesslayerApp(this);
|
|
1470
|
+
}
|
|
1471
|
+
get organization() {
|
|
1472
|
+
return new HarnesslayerOrganization(this);
|
|
1473
|
+
}
|
|
1474
|
+
get response() {
|
|
1475
|
+
return new HarnesslayerResponse(this);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
export {
|
|
1479
|
+
DriftstoneAPIError,
|
|
1480
|
+
DriftstoneError,
|
|
1481
|
+
DriftstoneHTTPError,
|
|
1482
|
+
DriftstoneResponseError,
|
|
1483
|
+
Harnesslayer,
|
|
1484
|
+
HarnesslayerAPIError,
|
|
1485
|
+
HarnesslayerAccessGroup,
|
|
1486
|
+
HarnesslayerApp,
|
|
1487
|
+
HarnesslayerChannel,
|
|
1488
|
+
HarnesslayerConflict,
|
|
1489
|
+
HarnesslayerError,
|
|
1490
|
+
HarnesslayerHTTPError,
|
|
1491
|
+
HarnesslayerInstance,
|
|
1492
|
+
HarnesslayerOrganization,
|
|
1493
|
+
HarnesslayerProfile,
|
|
1494
|
+
HarnesslayerResponse,
|
|
1495
|
+
HarnesslayerResponseError,
|
|
1496
|
+
HarnesslayerSession,
|
|
1497
|
+
HarnesslayerState,
|
|
1498
|
+
HarnesslayerVersion,
|
|
1499
|
+
Harnesslayer as default
|
|
1500
|
+
};
|
|
1501
|
+
//# sourceMappingURL=index.js.map
|