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