@buildwithtrace/sdk 0.1.0 → 0.1.2
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/README.md +19 -3
- package/dist/index.d.mts +170 -3
- package/dist/index.d.ts +170 -3
- package/dist/index.js +614 -42
- package/dist/index.mjs +601 -48
- package/package.json +13 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -22,8 +32,17 @@ var index_exports = {};
|
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
Trace: () => Trace,
|
|
24
34
|
TraceError: () => TraceError,
|
|
35
|
+
TracePlanRestrictedError: () => TracePlanRestrictedError,
|
|
25
36
|
TraceToolExecutionError: () => TraceToolExecutionError,
|
|
26
|
-
|
|
37
|
+
browserLogin: () => browserLogin,
|
|
38
|
+
clearCredentials: () => clearCredentials,
|
|
39
|
+
default: () => index_default,
|
|
40
|
+
deriveFrontendUrl: () => deriveFrontendUrl,
|
|
41
|
+
getConfigDir: () => getConfigDir,
|
|
42
|
+
getStoredAccessToken: () => getStoredAccessToken,
|
|
43
|
+
openBrowser: () => openBrowser,
|
|
44
|
+
readCredentials: () => readCredentials,
|
|
45
|
+
storeCredentials: () => storeCredentials
|
|
27
46
|
});
|
|
28
47
|
module.exports = __toCommonJS(index_exports);
|
|
29
48
|
var import_fs2 = require("fs");
|
|
@@ -439,8 +458,401 @@ function executeFileTool(toolName, toolArgs, projectDir, defaultFile = "") {
|
|
|
439
458
|
}
|
|
440
459
|
}
|
|
441
460
|
|
|
442
|
-
// src/
|
|
461
|
+
// src/auth.ts
|
|
462
|
+
var childProcess = __toESM(require("child_process"));
|
|
463
|
+
var import_node_http = require("http");
|
|
464
|
+
var import_node_os = require("os");
|
|
465
|
+
var import_node_path = require("path");
|
|
466
|
+
var import_node_fs = require("fs");
|
|
467
|
+
var import_node_url = require("url");
|
|
443
468
|
var DEFAULT_API_URL = "https://api.buildwithtrace.com";
|
|
469
|
+
var PRODUCTION_FRONTEND_URL = "https://buildwithtrace.com";
|
|
470
|
+
var LOCAL_FRONTEND_URL = "http://localhost:3000";
|
|
471
|
+
var DEFAULT_LOGIN_TIMEOUT_MS = 12e4;
|
|
472
|
+
function deriveFrontendUrl(baseUrl) {
|
|
473
|
+
const envOverride = process.env.TRACE_FRONTEND_URL;
|
|
474
|
+
if (envOverride && envOverride.trim()) {
|
|
475
|
+
return envOverride.trim().replace(/\/$/, "");
|
|
476
|
+
}
|
|
477
|
+
const api = (baseUrl || process.env.TRACE_BASE_URL || DEFAULT_API_URL).trim();
|
|
478
|
+
try {
|
|
479
|
+
const u = new import_node_url.URL(api);
|
|
480
|
+
const host = u.hostname;
|
|
481
|
+
if (host === "localhost" || host === "127.0.0.1") {
|
|
482
|
+
return LOCAL_FRONTEND_URL;
|
|
483
|
+
}
|
|
484
|
+
if (host.startsWith("api.")) {
|
|
485
|
+
return `${u.protocol}//${host.slice("api.".length)}`;
|
|
486
|
+
}
|
|
487
|
+
return PRODUCTION_FRONTEND_URL;
|
|
488
|
+
} catch {
|
|
489
|
+
return PRODUCTION_FRONTEND_URL;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
var CREDENTIALS_FILENAME = "credentials.json";
|
|
493
|
+
function getConfigDir() {
|
|
494
|
+
const override = process.env.TRACE_CONFIG_DIR;
|
|
495
|
+
if (override && override.trim()) return override.trim();
|
|
496
|
+
const home = (0, import_node_os.homedir)();
|
|
497
|
+
switch ((0, import_node_os.platform)()) {
|
|
498
|
+
case "darwin":
|
|
499
|
+
return (0, import_node_path.join)(home, "Library", "Application Support", "trace");
|
|
500
|
+
case "win32": {
|
|
501
|
+
const appData = process.env.APPDATA || (0, import_node_path.join)(home, "AppData", "Roaming");
|
|
502
|
+
return (0, import_node_path.join)(appData, "buildwithtrace", "trace");
|
|
503
|
+
}
|
|
504
|
+
default: {
|
|
505
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
506
|
+
const base = xdg && xdg.trim() ? xdg.trim() : (0, import_node_path.join)(home, ".config");
|
|
507
|
+
return (0, import_node_path.join)(base, "trace");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function credentialsPath() {
|
|
512
|
+
return (0, import_node_path.join)(getConfigDir(), CREDENTIALS_FILENAME);
|
|
513
|
+
}
|
|
514
|
+
function storeCredentials(creds) {
|
|
515
|
+
const dir = getConfigDir();
|
|
516
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
517
|
+
const path = credentialsPath();
|
|
518
|
+
const data = {
|
|
519
|
+
access_token: creds.access_token,
|
|
520
|
+
refresh_token: creds.refresh_token
|
|
521
|
+
};
|
|
522
|
+
if (creds.user_data) data.user_data = creds.user_data;
|
|
523
|
+
(0, import_node_fs.writeFileSync)(path, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
524
|
+
try {
|
|
525
|
+
(0, import_node_fs.chmodSync)(path, 384);
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
return path;
|
|
529
|
+
}
|
|
530
|
+
function readCredentials() {
|
|
531
|
+
const path = credentialsPath();
|
|
532
|
+
if (!(0, import_node_fs.existsSync)(path)) return null;
|
|
533
|
+
try {
|
|
534
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf-8"));
|
|
535
|
+
if (parsed && typeof parsed.access_token === "string" && parsed.access_token) {
|
|
536
|
+
return parsed;
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
} catch {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function getStoredAccessToken() {
|
|
544
|
+
return readCredentials()?.access_token ?? null;
|
|
545
|
+
}
|
|
546
|
+
function clearCredentials() {
|
|
547
|
+
const path = credentialsPath();
|
|
548
|
+
if ((0, import_node_fs.existsSync)(path)) {
|
|
549
|
+
(0, import_node_fs.rmSync)(path, { force: true });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function openBrowser(url) {
|
|
553
|
+
let command;
|
|
554
|
+
let args;
|
|
555
|
+
switch ((0, import_node_os.platform)()) {
|
|
556
|
+
case "darwin":
|
|
557
|
+
command = "open";
|
|
558
|
+
args = [url];
|
|
559
|
+
break;
|
|
560
|
+
case "win32":
|
|
561
|
+
command = "cmd";
|
|
562
|
+
args = ["/c", "start", "", url];
|
|
563
|
+
break;
|
|
564
|
+
default:
|
|
565
|
+
command = "xdg-open";
|
|
566
|
+
args = [url];
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const child = childProcess.spawn(command, args, { stdio: "ignore", detached: true });
|
|
571
|
+
child.on("error", () => {
|
|
572
|
+
});
|
|
573
|
+
child.unref();
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
var SUCCESS_HTML = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Trace</title></head>
|
|
578
|
+
<body style="font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:64px;color:#2a2119">
|
|
579
|
+
<h1 style="font-weight:600">✓ Authentication successful</h1>
|
|
580
|
+
<p>You can close this window and return to your terminal.</p>
|
|
581
|
+
<script>setTimeout(function(){window.close();},500)</script>
|
|
582
|
+
</body></html>`;
|
|
583
|
+
var PENDING_HTML = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Trace</title></head>
|
|
584
|
+
<body style="font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:64px;color:#2a2119">
|
|
585
|
+
<h1 style="font-weight:600">Waiting for authentication…</h1>
|
|
586
|
+
<p>This page is the Trace login callback. Complete sign-in in the other tab.</p>
|
|
587
|
+
</body></html>`;
|
|
588
|
+
function parseCallback(query) {
|
|
589
|
+
const accessToken = query.get("token") || query.get("access_token");
|
|
590
|
+
if (!accessToken) return null;
|
|
591
|
+
const refreshToken = query.get("refresh_token") || "";
|
|
592
|
+
let user = null;
|
|
593
|
+
const userRaw = query.get("user");
|
|
594
|
+
if (userRaw) {
|
|
595
|
+
try {
|
|
596
|
+
user = JSON.parse(userRaw);
|
|
597
|
+
} catch {
|
|
598
|
+
user = null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { accessToken, refreshToken, user };
|
|
602
|
+
}
|
|
603
|
+
function browserLogin(opts = {}) {
|
|
604
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS;
|
|
605
|
+
const frontendUrl = opts.frontendUrl?.trim().replace(/\/$/, "") || deriveFrontendUrl(opts.baseUrl);
|
|
606
|
+
const openFn = opts.open ?? openBrowser;
|
|
607
|
+
return new Promise((resolve3, reject) => {
|
|
608
|
+
let settled = false;
|
|
609
|
+
let timer;
|
|
610
|
+
const shutdown = () => {
|
|
611
|
+
try {
|
|
612
|
+
server.closeAllConnections?.();
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
server.close();
|
|
616
|
+
};
|
|
617
|
+
const settle = (run) => {
|
|
618
|
+
if (settled) return;
|
|
619
|
+
settled = true;
|
|
620
|
+
if (timer) clearTimeout(timer);
|
|
621
|
+
run();
|
|
622
|
+
};
|
|
623
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
624
|
+
let url;
|
|
625
|
+
try {
|
|
626
|
+
url = new import_node_url.URL(req.url || "/", "http://127.0.0.1");
|
|
627
|
+
} catch {
|
|
628
|
+
res.writeHead(400).end();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (!url.pathname.startsWith("/callback")) {
|
|
632
|
+
res.writeHead(204).end();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const result = parseCallback(url.searchParams);
|
|
636
|
+
if (!result) {
|
|
637
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(PENDING_HTML);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(SUCCESS_HTML);
|
|
641
|
+
res.on("finish", shutdown);
|
|
642
|
+
res.on("close", shutdown);
|
|
643
|
+
settle(() => resolve3(result));
|
|
644
|
+
});
|
|
645
|
+
server.on("error", (err) => {
|
|
646
|
+
settle(() => {
|
|
647
|
+
shutdown();
|
|
648
|
+
reject(new Error(`Failed to start the local login callback server: ${err.message}`));
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
server.listen(0, "127.0.0.1", () => {
|
|
652
|
+
const address = server.address();
|
|
653
|
+
if (!address) {
|
|
654
|
+
settle(() => {
|
|
655
|
+
shutdown();
|
|
656
|
+
reject(new Error("Could not determine the local callback port."));
|
|
657
|
+
});
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const port = address.port;
|
|
661
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
662
|
+
const loginUrl = `${frontendUrl}/login?callback=${encodeURIComponent(callbackUrl)}`;
|
|
663
|
+
console.error(`Opening your browser to sign in to Trace...
|
|
664
|
+
If it does not open, visit:
|
|
665
|
+
${loginUrl}
|
|
666
|
+
`);
|
|
667
|
+
openFn(loginUrl);
|
|
668
|
+
timer = setTimeout(() => {
|
|
669
|
+
settle(() => {
|
|
670
|
+
shutdown();
|
|
671
|
+
reject(
|
|
672
|
+
new Error(
|
|
673
|
+
`Login timed out after ${Math.round(timeoutMs / 1e3)}s. Re-run login, or open this URL manually:
|
|
674
|
+
${loginUrl}`
|
|
675
|
+
)
|
|
676
|
+
);
|
|
677
|
+
});
|
|
678
|
+
}, timeoutMs);
|
|
679
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/analytics.ts
|
|
685
|
+
var import_node_crypto = require("crypto");
|
|
686
|
+
var import_node_os2 = require("os");
|
|
687
|
+
var import_node_fs2 = require("fs");
|
|
688
|
+
var import_node_path2 = require("path");
|
|
689
|
+
var import_posthog_node = require("posthog-node");
|
|
690
|
+
|
|
691
|
+
// src/_build_config.ts
|
|
692
|
+
var BUILD_POSTHOG_KEY = "phc_YbXW9ynLyGf9qHVxOY87InoZNSRikTCQ14GDJOZtSxX";
|
|
693
|
+
var BUILD_POSTHOG_HOST = "https://us.i.posthog.com";
|
|
694
|
+
var BUILD_SDK_VERSION = "0.1.2";
|
|
695
|
+
|
|
696
|
+
// src/analytics.ts
|
|
697
|
+
var DEFAULT_HOST = "https://us.i.posthog.com";
|
|
698
|
+
var INSTALL_ID_FILENAME = "install_id";
|
|
699
|
+
var MAX_STRING_LEN = 80;
|
|
700
|
+
var MAX_KEY_LEN = 64;
|
|
701
|
+
function envSet(name) {
|
|
702
|
+
const v = process.env[name];
|
|
703
|
+
if (!v) return false;
|
|
704
|
+
const t = v.trim().toLowerCase();
|
|
705
|
+
return t !== "" && t !== "0" && t !== "false";
|
|
706
|
+
}
|
|
707
|
+
function isOptedOut() {
|
|
708
|
+
return envSet("DO_NOT_TRACK") || envSet("TRACE_NO_ANALYTICS");
|
|
709
|
+
}
|
|
710
|
+
function isCI() {
|
|
711
|
+
return envSet("CI") || envSet("CONTINUOUS_INTEGRATION") || envSet("GITHUB_ACTIONS");
|
|
712
|
+
}
|
|
713
|
+
function resolveKey() {
|
|
714
|
+
return (process.env.POSTHOG_API_KEY || BUILD_POSTHOG_KEY || "").trim();
|
|
715
|
+
}
|
|
716
|
+
function resolveHost() {
|
|
717
|
+
return (process.env.POSTHOG_HOST || BUILD_POSTHOG_HOST || DEFAULT_HOST).trim() || DEFAULT_HOST;
|
|
718
|
+
}
|
|
719
|
+
function isAnalyticsEnabled() {
|
|
720
|
+
try {
|
|
721
|
+
if (isOptedOut()) return false;
|
|
722
|
+
if (isCI()) return false;
|
|
723
|
+
return resolveKey().length > 0;
|
|
724
|
+
} catch {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
var _installId;
|
|
729
|
+
function getInstallId() {
|
|
730
|
+
if (_installId) return _installId;
|
|
731
|
+
try {
|
|
732
|
+
const dir = getConfigDir();
|
|
733
|
+
const path = (0, import_node_path2.join)(dir, INSTALL_ID_FILENAME);
|
|
734
|
+
if ((0, import_node_fs2.existsSync)(path)) {
|
|
735
|
+
const existing = (0, import_node_fs2.readFileSync)(path, "utf-8").trim();
|
|
736
|
+
if (existing) {
|
|
737
|
+
_installId = existing;
|
|
738
|
+
return existing;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const id = (0, import_node_crypto.randomUUID)();
|
|
742
|
+
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
743
|
+
(0, import_node_fs2.writeFileSync)(path, `${id}
|
|
744
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
745
|
+
_installId = id;
|
|
746
|
+
return id;
|
|
747
|
+
} catch {
|
|
748
|
+
if (!_installId) _installId = (0, import_node_crypto.randomUUID)();
|
|
749
|
+
return _installId;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function storedUserId() {
|
|
753
|
+
try {
|
|
754
|
+
const id = readCredentials()?.user_data?.id;
|
|
755
|
+
return typeof id === "string" && id ? id : null;
|
|
756
|
+
} catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function sanitizeProps(props) {
|
|
761
|
+
const out = {};
|
|
762
|
+
if (!props || typeof props !== "object") return out;
|
|
763
|
+
for (const [key, value] of Object.entries(props)) {
|
|
764
|
+
if (typeof key !== "string" || !key || key.length > MAX_KEY_LEN) continue;
|
|
765
|
+
if (typeof value === "boolean") {
|
|
766
|
+
out[key] = value;
|
|
767
|
+
} else if (typeof value === "number") {
|
|
768
|
+
if (Number.isFinite(value)) out[key] = value;
|
|
769
|
+
} else if (typeof value === "string") {
|
|
770
|
+
if (!value) continue;
|
|
771
|
+
if (value.length > MAX_STRING_LEN) continue;
|
|
772
|
+
if (/[\\/\n\r\t]/.test(value)) continue;
|
|
773
|
+
out[key] = value;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return out;
|
|
777
|
+
}
|
|
778
|
+
function superProps() {
|
|
779
|
+
return {
|
|
780
|
+
client: "sdk-node",
|
|
781
|
+
client_version: BUILD_SDK_VERSION || "0.0.0",
|
|
782
|
+
os: process.platform,
|
|
783
|
+
os_version: (0, import_node_os2.release)(),
|
|
784
|
+
node_version: process.version,
|
|
785
|
+
is_ci: isCI(),
|
|
786
|
+
install_id: getInstallId()
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
var _client;
|
|
790
|
+
var _exitHookRegistered = false;
|
|
791
|
+
function registerExitFlush() {
|
|
792
|
+
if (_exitHookRegistered) return;
|
|
793
|
+
_exitHookRegistered = true;
|
|
794
|
+
try {
|
|
795
|
+
process.once("beforeExit", () => {
|
|
796
|
+
try {
|
|
797
|
+
_client?.shutdown?.();
|
|
798
|
+
} catch {
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function getClient() {
|
|
805
|
+
if (_client !== void 0) return _client;
|
|
806
|
+
if (!isAnalyticsEnabled()) {
|
|
807
|
+
_client = null;
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
_client = new import_posthog_node.PostHog(resolveKey(), {
|
|
812
|
+
host: resolveHost(),
|
|
813
|
+
flushAt: 1,
|
|
814
|
+
// short-lived SDK processes: send promptly
|
|
815
|
+
flushInterval: 1e4
|
|
816
|
+
});
|
|
817
|
+
registerExitFlush();
|
|
818
|
+
return _client;
|
|
819
|
+
} catch {
|
|
820
|
+
_client = null;
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function track(event, props) {
|
|
825
|
+
try {
|
|
826
|
+
if (!event) return;
|
|
827
|
+
const client = getClient();
|
|
828
|
+
if (!client) return;
|
|
829
|
+
const distinctId = storedUserId() || getInstallId();
|
|
830
|
+
const properties = { ...sanitizeProps(props), ...superProps() };
|
|
831
|
+
client.capture({ distinctId, event, properties });
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function identify(distinctId, traits) {
|
|
836
|
+
try {
|
|
837
|
+
if (!distinctId) return;
|
|
838
|
+
const client = getClient();
|
|
839
|
+
if (!client) return;
|
|
840
|
+
client.identify({ distinctId, properties: { ...sanitizeProps(traits), ...superProps() } });
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function alias(distinctId, aliasId) {
|
|
845
|
+
try {
|
|
846
|
+
if (!distinctId || !aliasId || distinctId === aliasId) return;
|
|
847
|
+
const client = getClient();
|
|
848
|
+
if (!client) return;
|
|
849
|
+
client.alias?.({ distinctId, alias: aliasId });
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/index.ts
|
|
855
|
+
var DEFAULT_API_URL2 = "https://api.buildwithtrace.com";
|
|
444
856
|
var API_VERSION = "latest";
|
|
445
857
|
var TraceError = class extends Error {
|
|
446
858
|
constructor(message) {
|
|
@@ -454,66 +866,179 @@ var TraceToolExecutionError = class extends TraceError {
|
|
|
454
866
|
this.name = "TraceToolExecutionError";
|
|
455
867
|
}
|
|
456
868
|
};
|
|
869
|
+
var TracePlanRestrictedError = class extends TraceError {
|
|
870
|
+
constructor(message, opts) {
|
|
871
|
+
super(message);
|
|
872
|
+
/** Always true — lets callers branch on a flag, mirroring the backend `code`. */
|
|
873
|
+
this.planRestricted = true;
|
|
874
|
+
this.name = "TracePlanRestrictedError";
|
|
875
|
+
this.mode = opts?.mode;
|
|
876
|
+
this.plan = opts?.plan;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
function parsePlanRestricted(text) {
|
|
880
|
+
if (!text) return null;
|
|
881
|
+
try {
|
|
882
|
+
const data = JSON.parse(text);
|
|
883
|
+
const detail = typeof data.detail === "object" && data.detail !== null ? data.detail : data;
|
|
884
|
+
if (detail.error === "plan_restricted" || detail.code === "plan_restricted") {
|
|
885
|
+
const message = detail.message || "This feature requires a paid plan.";
|
|
886
|
+
const plan = typeof detail.current_plan_id === "string" ? detail.current_plan_id : void 0;
|
|
887
|
+
return { message, plan };
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
if (text.includes("plan_restricted")) {
|
|
892
|
+
return { message: "This feature requires a paid plan." };
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
457
896
|
var Trace = class _Trace {
|
|
458
|
-
constructor(config) {
|
|
459
|
-
this.apiKey = config.apiKey || process.env.TRACE_API_KEY || "";
|
|
897
|
+
constructor(config = {}) {
|
|
898
|
+
this.apiKey = config.apiKey || process.env.TRACE_API_KEY || getStoredAccessToken() || "";
|
|
460
899
|
if (!this.apiKey) {
|
|
461
900
|
throw new Error(
|
|
462
|
-
"API key required.
|
|
901
|
+
"API key required. Sign in with `await Trace.login()`, pass apiKey in config, or set TRACE_API_KEY env var. Get your key at buildwithtrace.com/dashboard/settings > Developer, or via CLI: buildwithtrace auth token"
|
|
463
902
|
);
|
|
464
903
|
}
|
|
465
|
-
this.
|
|
904
|
+
this.authed = true;
|
|
905
|
+
this.baseUrl = (config.baseUrl || process.env.TRACE_BASE_URL || DEFAULT_API_URL2).replace(/\/$/, "");
|
|
466
906
|
this.timeout = config.timeout || 3e5;
|
|
907
|
+
this.byokProvider = config.llmProvider;
|
|
908
|
+
this.byokApiKey = config.llmApiKey;
|
|
909
|
+
this.byokModelId = config.llmModelId;
|
|
910
|
+
track("sdk_init", { authed: this.authed });
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Wrap a public method call: emit `sdk_call` (with timing + outcome) on every
|
|
914
|
+
* call and `sdk_error` (with the error class) on failure. Analytics is
|
|
915
|
+
* fire-and-forget; it must never change behavior, so the underlying result /
|
|
916
|
+
* thrown error always propagates unchanged.
|
|
917
|
+
*/
|
|
918
|
+
async _instrument(method, streaming, byok, fn) {
|
|
919
|
+
const start = Date.now();
|
|
920
|
+
try {
|
|
921
|
+
const result = await fn();
|
|
922
|
+
track("sdk_call", {
|
|
923
|
+
method,
|
|
924
|
+
api_version: API_VERSION,
|
|
925
|
+
streaming,
|
|
926
|
+
byok,
|
|
927
|
+
duration_ms: Date.now() - start,
|
|
928
|
+
ok: true,
|
|
929
|
+
authed: this.authed
|
|
930
|
+
});
|
|
931
|
+
return result;
|
|
932
|
+
} catch (err) {
|
|
933
|
+
track("sdk_call", {
|
|
934
|
+
method,
|
|
935
|
+
api_version: API_VERSION,
|
|
936
|
+
streaming,
|
|
937
|
+
byok,
|
|
938
|
+
duration_ms: Date.now() - start,
|
|
939
|
+
ok: false,
|
|
940
|
+
authed: this.authed
|
|
941
|
+
});
|
|
942
|
+
track("sdk_error", {
|
|
943
|
+
method,
|
|
944
|
+
error_type: err instanceof Error && err.name || "Error",
|
|
945
|
+
authed: this.authed
|
|
946
|
+
});
|
|
947
|
+
throw err;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Sign in via the browser deeplink flow, persist the returned tokens to a
|
|
952
|
+
* 0600 `credentials.json`, and return a ready-to-use `Trace` instance.
|
|
953
|
+
*
|
|
954
|
+
* Opens `{FRONTEND}/login?callback=http://localhost:PORT/callback` in the
|
|
955
|
+
* browser; the user authenticates there (captcha included) and the page
|
|
956
|
+
* redirects back to the loopback server with the final Supabase JWTs.
|
|
957
|
+
*
|
|
958
|
+
* @example
|
|
959
|
+
* ```typescript
|
|
960
|
+
* const trace = await Trace.login();
|
|
961
|
+
* const answer = await trace.ask('What ERC violations exist?');
|
|
962
|
+
* ```
|
|
963
|
+
*/
|
|
964
|
+
static async login(opts = {}) {
|
|
965
|
+
const result = await browserLogin({
|
|
966
|
+
baseUrl: opts.baseUrl,
|
|
967
|
+
frontendUrl: opts.frontendUrl,
|
|
968
|
+
timeoutMs: opts.timeoutMs,
|
|
969
|
+
open: opts.open
|
|
970
|
+
});
|
|
971
|
+
storeCredentials({
|
|
972
|
+
access_token: result.accessToken,
|
|
973
|
+
refresh_token: result.refreshToken,
|
|
974
|
+
user_data: result.user
|
|
975
|
+
});
|
|
976
|
+
const userId = result.user?.id;
|
|
977
|
+
if (typeof userId === "string" && userId) {
|
|
978
|
+
identify(userId);
|
|
979
|
+
alias(userId, getInstallId());
|
|
980
|
+
}
|
|
981
|
+
return new _Trace({ apiKey: result.accessToken, baseUrl: opts.baseUrl, timeout: opts.timeout });
|
|
982
|
+
}
|
|
983
|
+
/** Clear stored credentials (sign out). Subsequent `new Trace()` will need a token again. */
|
|
984
|
+
static logout() {
|
|
985
|
+
clearCredentials();
|
|
467
986
|
}
|
|
468
987
|
async generateSymbol(description, options) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
988
|
+
return this._instrument("generateSymbol", false, false, async () => {
|
|
989
|
+
const body = { description };
|
|
990
|
+
if (options?.datasheetUrl) body.datasheet_url = options.datasheetUrl;
|
|
991
|
+
if (options?.additionalInstructions) body.additional_instructions = options.additionalInstructions;
|
|
992
|
+
const data = await this._post(`/api/${API_VERSION}/components/generate/symbol`, body);
|
|
993
|
+
return this._makeGenerateResult(data, "symbol");
|
|
994
|
+
});
|
|
474
995
|
}
|
|
475
996
|
async generateFootprint(description, options) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
997
|
+
return this._instrument("generateFootprint", false, false, async () => {
|
|
998
|
+
const body = { description };
|
|
999
|
+
if (options?.packageType) body.package_type = options.packageType;
|
|
1000
|
+
if (options?.datasheetUrl) body.datasheet_url = options.datasheetUrl;
|
|
1001
|
+
const data = await this._post(`/api/${API_VERSION}/components/generate/footprint`, body);
|
|
1002
|
+
return this._makeGenerateResult(data, "footprint");
|
|
1003
|
+
});
|
|
481
1004
|
}
|
|
482
1005
|
async search(query, options) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1006
|
+
return this._instrument("search", false, false, async () => {
|
|
1007
|
+
const params = new URLSearchParams({ q: query, limit: String(options?.limit || 20) });
|
|
1008
|
+
if (options?.type) params.set("type", options.type);
|
|
1009
|
+
const data = await this._get(`/api/${API_VERSION}/components/search?${params}`);
|
|
1010
|
+
return (data.results || []).map((r) => ({
|
|
1011
|
+
name: r.name || "",
|
|
1012
|
+
library: r.library || "",
|
|
1013
|
+
description: r.description || "",
|
|
1014
|
+
pinCount: r.pin_count || 0,
|
|
1015
|
+
padCount: r.pad_count || 0,
|
|
1016
|
+
category: r.category || "",
|
|
1017
|
+
source: r.source || "",
|
|
1018
|
+
score: r.score || 0
|
|
1019
|
+
}));
|
|
1020
|
+
});
|
|
496
1021
|
}
|
|
497
1022
|
async ask(question, options) {
|
|
498
|
-
return this._streamChat(question, {
|
|
1023
|
+
return this._instrument("ask", true, false, () => this._streamChat(question, {
|
|
499
1024
|
mode: "ask",
|
|
500
1025
|
appType: options?.appType,
|
|
501
1026
|
projectDir: options?.projectDir,
|
|
502
1027
|
fileContent: options?.fileContent,
|
|
503
1028
|
filePath: options?.filePath,
|
|
504
1029
|
conversationId: options?.conversationId
|
|
505
|
-
});
|
|
1030
|
+
}));
|
|
506
1031
|
}
|
|
507
1032
|
async review(options) {
|
|
508
1033
|
let prompt = "Review this design for issues, best practices, and improvements.";
|
|
509
1034
|
if (options?.focus) prompt += ` Focus on: ${options.focus}`;
|
|
510
|
-
return this._streamChat(prompt, {
|
|
1035
|
+
return this._instrument("review", true, false, () => this._streamChat(prompt, {
|
|
511
1036
|
mode: "ask",
|
|
512
1037
|
appType: options?.appType,
|
|
513
1038
|
projectDir: options?.projectDir,
|
|
514
1039
|
fileContent: options?.fileContent,
|
|
515
1040
|
filePath: options?.filePath
|
|
516
|
-
});
|
|
1041
|
+
}));
|
|
517
1042
|
}
|
|
518
1043
|
/**
|
|
519
1044
|
* Stream a chat turn and collect the full response. Exposes the full backend
|
|
@@ -532,7 +1057,9 @@ var Trace = class _Trace {
|
|
|
532
1057
|
* TraceToolExecutionError — use the `buildwithtrace` CLI for those.
|
|
533
1058
|
*/
|
|
534
1059
|
async chat(message, options) {
|
|
535
|
-
|
|
1060
|
+
const resolved = this._resolveByok(options || {});
|
|
1061
|
+
const byok = !!(resolved.provider && resolved.apiKey);
|
|
1062
|
+
return this._instrument("chat", true, byok, () => this._streamChat(message, options || {}));
|
|
536
1063
|
}
|
|
537
1064
|
async _post(path, body) {
|
|
538
1065
|
const controller = new AbortController();
|
|
@@ -557,10 +1084,14 @@ var Trace = class _Trace {
|
|
|
557
1084
|
clearTimeout(timeoutId);
|
|
558
1085
|
}
|
|
559
1086
|
}
|
|
560
|
-
static _httpError(status, text) {
|
|
1087
|
+
static _httpError(status, text, mode) {
|
|
561
1088
|
if (status === 401) return new TraceError("Invalid or expired API key. Get a new one at buildwithtrace.com/dashboard/settings > Developer");
|
|
562
1089
|
if (status === 402) return new TraceError("Quota exceeded. Upgrade at buildwithtrace.com/dashboard/billing");
|
|
563
|
-
if (status === 403)
|
|
1090
|
+
if (status === 403) {
|
|
1091
|
+
const pr = parsePlanRestricted(text);
|
|
1092
|
+
if (pr) return new TracePlanRestrictedError(pr.message, { mode, plan: pr.plan });
|
|
1093
|
+
return new TraceError("Access denied. This feature requires a paid plan.");
|
|
1094
|
+
}
|
|
564
1095
|
return new Error(`Trace API error ${status}: ${text.slice(0, 200)}`);
|
|
565
1096
|
}
|
|
566
1097
|
async _get(path) {
|
|
@@ -581,6 +1112,37 @@ var Trace = class _Trace {
|
|
|
581
1112
|
* backend ChatRequest contract so the SDK stays in lockstep with the
|
|
582
1113
|
* CLI/desktop instead of sending a stale minimal payload.
|
|
583
1114
|
*/
|
|
1115
|
+
/**
|
|
1116
|
+
* Resolve BYOK routing with precedence: per-call options > constructor config >
|
|
1117
|
+
* env (`TRACE_LLM_PROVIDER` / `TRACE_LLM_API_KEY` / `TRACE_LLM_MODEL`). Provider
|
|
1118
|
+
* and key are resolved as a UNIT from the first source supplying BOTH (we never
|
|
1119
|
+
* mix a per-call provider with an env key); the model id is layered (a more
|
|
1120
|
+
* specific source's model wins). Node has no keyring `byok set` store, so env is
|
|
1121
|
+
* the "persisted" mechanism. Returns `{}` when no complete config is found, so
|
|
1122
|
+
* nothing is attached and the backend uses Trace-hosted Bedrock.
|
|
1123
|
+
*/
|
|
1124
|
+
_resolveByok(opts) {
|
|
1125
|
+
if (opts.llmProvider && opts.llmApiKey) {
|
|
1126
|
+
return { provider: opts.llmProvider, apiKey: opts.llmApiKey, modelId: opts.llmModelId };
|
|
1127
|
+
}
|
|
1128
|
+
if (this.byokProvider && this.byokApiKey) {
|
|
1129
|
+
return {
|
|
1130
|
+
provider: this.byokProvider,
|
|
1131
|
+
apiKey: this.byokApiKey,
|
|
1132
|
+
modelId: opts.llmModelId || this.byokModelId
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const envProvider = process.env.TRACE_LLM_PROVIDER;
|
|
1136
|
+
const envKey = process.env.TRACE_LLM_API_KEY;
|
|
1137
|
+
if (envProvider && envKey) {
|
|
1138
|
+
return {
|
|
1139
|
+
provider: envProvider,
|
|
1140
|
+
apiKey: envKey,
|
|
1141
|
+
modelId: opts.llmModelId || this.byokModelId || process.env.TRACE_LLM_MODEL
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
return {};
|
|
1145
|
+
}
|
|
584
1146
|
_buildChatBody(message, opts, sessionId) {
|
|
585
1147
|
const appType = opts.appType || "eeschema";
|
|
586
1148
|
const body = {
|
|
@@ -601,10 +1163,11 @@ var Trace = class _Trace {
|
|
|
601
1163
|
if (opts.projectDir) body.project_dir = opts.projectDir;
|
|
602
1164
|
if (opts.teamId) body.team_id = opts.teamId;
|
|
603
1165
|
if (opts.attachments) body.attachments = opts.attachments;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
body.
|
|
607
|
-
|
|
1166
|
+
const byok = this._resolveByok(opts);
|
|
1167
|
+
if (byok.provider && byok.apiKey) {
|
|
1168
|
+
body.llm_provider = byok.provider;
|
|
1169
|
+
body.llm_api_key = byok.apiKey;
|
|
1170
|
+
if (byok.modelId) body.llm_model_id = byok.modelId;
|
|
608
1171
|
}
|
|
609
1172
|
return body;
|
|
610
1173
|
}
|
|
@@ -633,7 +1196,7 @@ var Trace = class _Trace {
|
|
|
633
1196
|
});
|
|
634
1197
|
if (!resp.ok) {
|
|
635
1198
|
const errText = await resp.text().catch(() => "");
|
|
636
|
-
throw _Trace._httpError(resp.status, errText);
|
|
1199
|
+
throw _Trace._httpError(resp.status, errText, opts.mode);
|
|
637
1200
|
}
|
|
638
1201
|
if (!resp.body) {
|
|
639
1202
|
throw new TraceError("The backend returned an empty response stream.");
|
|
@@ -782,5 +1345,14 @@ var index_default = Trace;
|
|
|
782
1345
|
0 && (module.exports = {
|
|
783
1346
|
Trace,
|
|
784
1347
|
TraceError,
|
|
785
|
-
|
|
1348
|
+
TracePlanRestrictedError,
|
|
1349
|
+
TraceToolExecutionError,
|
|
1350
|
+
browserLogin,
|
|
1351
|
+
clearCredentials,
|
|
1352
|
+
deriveFrontendUrl,
|
|
1353
|
+
getConfigDir,
|
|
1354
|
+
getStoredAccessToken,
|
|
1355
|
+
openBrowser,
|
|
1356
|
+
readCredentials,
|
|
1357
|
+
storeCredentials
|
|
786
1358
|
});
|