primcli 1.33.0 → 1.35.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/README.md +237 -20
- package/bin/claude-pending-mail.mjs +342 -0
- package/bin/claude-wake.mjs +204 -0
- package/bin/run.js +20 -0
- package/dist/{cli-config-Cv2-NAOd.js → cli-config-DmhlcS3s.js} +30 -350
- package/dist/json-output-KAIuP9rG.js +254 -0
- package/dist/listen-background-D6UhIbkO.js +647 -0
- package/dist/oclif/index.js +17087 -9256
- package/dist/oclif/listen-supervisor.js +2 -0
- package/dist/oclif/proxy-auto-detect.js +2 -1
- package/dist/oclif/root-signup-hint.js +1 -1
- package/dist/shared-mail-files-Cbwnf79a.js +351 -0
- package/man/primitive.1 +51 -0
- package/package.json +13 -4
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { c as mailTime, d as readMailJson, h as writeMailJson, i as mailId, l as privateMailDirectory, o as mailObject, r as mailAddress, s as mailString } from "./shared-mail-files-Cbwnf79a.js";
|
|
1
2
|
import { Errors } from "@oclif/core";
|
|
2
|
-
import { chmodSync,
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join
|
|
5
|
-
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
6
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
7
6
|
//#region ../packages/api-core/src/api/core/bodySerializer.gen.ts
|
|
8
7
|
const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
9
8
|
//#endregion
|
|
@@ -638,353 +637,31 @@ var PrimitiveApiClient = class {
|
|
|
638
637
|
}
|
|
639
638
|
};
|
|
640
639
|
//#endregion
|
|
641
|
-
//#region src/oclif/
|
|
642
|
-
|
|
643
|
-
function
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
function privateDirectory(path) {
|
|
652
|
-
mkdirSync(path, {
|
|
653
|
-
recursive: true,
|
|
654
|
-
mode: 448
|
|
655
|
-
});
|
|
656
|
-
if (!lstatSync(path).isDirectory() || lstatSync(path).isSymbolicLink()) throw new ListenStateError("Listener state must be a private directory.");
|
|
657
|
-
chmodSync(path, 448);
|
|
658
|
-
}
|
|
659
|
-
function processAlive(pid) {
|
|
660
|
-
try {
|
|
661
|
-
process.kill(pid, 0);
|
|
662
|
-
return true;
|
|
663
|
-
} catch (error) {
|
|
664
|
-
return error.code !== "ESRCH";
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
function listenProcessIdentity(pid) {
|
|
668
|
-
if (!Number.isSafeInteger(pid) || pid < 1) return null;
|
|
669
|
-
try {
|
|
670
|
-
if (process.platform === "linux") {
|
|
671
|
-
const boot = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
672
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
673
|
-
const end = stat.lastIndexOf(")");
|
|
674
|
-
const started = stat.slice(end + 1).trim().split(/\s+/)[19];
|
|
675
|
-
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(boot) || !stat.startsWith(`${pid} (`) || end < 0 || !started || !/^\d+$/.test(started)) return null;
|
|
676
|
-
return `linux:${boot}:${started}`;
|
|
677
|
-
}
|
|
678
|
-
if (process.platform === "darwin") {
|
|
679
|
-
const options = {
|
|
680
|
-
encoding: "utf8",
|
|
681
|
-
timeout: 1e3,
|
|
682
|
-
maxBuffer: 4096,
|
|
683
|
-
stdio: [
|
|
684
|
-
"ignore",
|
|
685
|
-
"pipe",
|
|
686
|
-
"ignore"
|
|
687
|
-
],
|
|
688
|
-
env: {
|
|
689
|
-
LC_ALL: "C",
|
|
690
|
-
TZ: "UTC",
|
|
691
|
-
PATH: "/usr/bin:/bin:/usr/sbin:/sbin"
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
const bootId = execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], options).trim().toLowerCase();
|
|
695
|
-
const started = execFileSync("/bin/ps", [
|
|
696
|
-
"-p",
|
|
697
|
-
String(pid),
|
|
698
|
-
"-o",
|
|
699
|
-
"lstart="
|
|
700
|
-
], options).trim().replace(/\s+/g, " ");
|
|
701
|
-
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(bootId) || !/^[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} \d{4}$/.test(started)) return null;
|
|
702
|
-
return `darwin-boot:${bootId}:${started}`;
|
|
703
|
-
}
|
|
704
|
-
if (process.platform === "win32") {
|
|
705
|
-
const root = process.env.SystemRoot;
|
|
706
|
-
if (!root || !win32.isAbsolute(root)) return null;
|
|
707
|
-
const started = execFileSync(win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), [
|
|
708
|
-
"-NoLogo",
|
|
709
|
-
"-NoProfile",
|
|
710
|
-
"-NonInteractive",
|
|
711
|
-
"-Command",
|
|
712
|
-
`$ErrorActionPreference='Stop'; (Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
|
|
713
|
-
], {
|
|
714
|
-
encoding: "utf8",
|
|
715
|
-
timeout: 2e3,
|
|
716
|
-
maxBuffer: 4096,
|
|
717
|
-
windowsHide: true,
|
|
718
|
-
stdio: [
|
|
719
|
-
"ignore",
|
|
720
|
-
"pipe",
|
|
721
|
-
"ignore"
|
|
722
|
-
]
|
|
723
|
-
}).trim();
|
|
724
|
-
return /^\d{1,20}$/.test(started) ? `win32:${started}` : null;
|
|
725
|
-
}
|
|
726
|
-
} catch {}
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
|
-
/** Legacy macOS timestamps cannot prove that a live PID changed owners. */
|
|
730
|
-
function compareListenProcessIdentity(saved, current) {
|
|
731
|
-
if (saved === null || current === null) return null;
|
|
732
|
-
if (saved === current) return true;
|
|
733
|
-
if (saved.startsWith("darwin:") || current.startsWith("darwin:")) return null;
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
function ownerIdentity(path) {
|
|
737
|
-
try {
|
|
738
|
-
const stat = lstatSync(path);
|
|
739
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024) return null;
|
|
740
|
-
const saved = JSON.parse(readFileSync(path, "utf8"));
|
|
741
|
-
if (!saved || typeof saved !== "object" || !("version" in saved) || saved.version !== 1 || !("identity" in saved) || typeof saved.identity !== "string") return null;
|
|
742
|
-
return /^(?:linux:[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}:\d+|darwin:\d+:\d+:[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} \d{4}|darwin-boot:[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}:[A-Z][a-z]{2} [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2} \d{4}|win32:\d{1,20})$/.test(saved.identity) ? saved.identity : null;
|
|
743
|
-
} catch {
|
|
744
|
-
return null;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
function removeOwner(path, owner) {
|
|
748
|
-
try {
|
|
749
|
-
unlinkSync(join(path, owner));
|
|
750
|
-
} catch (error) {
|
|
751
|
-
if (error.code !== "ENOENT") throw error;
|
|
752
|
-
}
|
|
753
|
-
try {
|
|
754
|
-
rmdirSync(path);
|
|
755
|
-
} catch (error) {
|
|
756
|
-
if (![
|
|
757
|
-
"ENOENT",
|
|
758
|
-
"ENOTEMPTY",
|
|
759
|
-
"EEXIST"
|
|
760
|
-
].includes(error.code ?? "")) throw error;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
function acquireListenLock(directory, scope) {
|
|
764
|
-
privateDirectory(directory);
|
|
765
|
-
const path = join(directory, `${createHash("sha256").update(scope).digest("hex")}.lock`);
|
|
766
|
-
const owner = `${process.pid}-${randomUUID()}`;
|
|
767
|
-
const candidate = mkdtempSync(join(directory, ".claim-"));
|
|
768
|
-
chmodSync(candidate, 448);
|
|
769
|
-
writeFileSync(join(candidate, owner), JSON.stringify({
|
|
770
|
-
version: 1,
|
|
771
|
-
identity: listenProcessIdentity(process.pid)
|
|
772
|
-
}), {
|
|
773
|
-
mode: 384,
|
|
774
|
-
flag: "wx"
|
|
775
|
-
});
|
|
776
|
-
let installed = false;
|
|
777
|
-
try {
|
|
778
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
779
|
-
try {
|
|
780
|
-
renameSync(candidate, path);
|
|
781
|
-
installed = true;
|
|
782
|
-
break;
|
|
783
|
-
} catch (error) {
|
|
784
|
-
if (!["EEXIST", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
|
|
785
|
-
}
|
|
786
|
-
let entries;
|
|
787
|
-
try {
|
|
788
|
-
entries = readdirSync(path);
|
|
789
|
-
} catch (error) {
|
|
790
|
-
if (error.code === "ENOENT") continue;
|
|
791
|
-
throw error;
|
|
792
|
-
}
|
|
793
|
-
if (entries.length === 0) continue;
|
|
794
|
-
const match = entries.length === 1 ? /^(\d+)-[a-f0-9-]{36}$/.exec(entries[0] ?? "") : null;
|
|
795
|
-
const pid = match ? Number(match[1]) : null;
|
|
796
|
-
let stale = pid !== null && !processAlive(pid);
|
|
797
|
-
if (!stale && pid !== null) {
|
|
798
|
-
const saved = ownerIdentity(join(path, entries[0] ?? ""));
|
|
799
|
-
stale = compareListenProcessIdentity(saved, saved === null ? null : listenProcessIdentity(pid)) === false;
|
|
800
|
-
}
|
|
801
|
-
if (!stale) throw new ListenStateError("Another listener is using this subscription, or its owner cannot be verified. Stop the original listener before reconnecting. To use a separate subscription instead, run: primitive listen --subscription local-" + randomUUID());
|
|
802
|
-
removeOwner(path, entries[0] ?? "");
|
|
803
|
-
}
|
|
804
|
-
if (!installed) throw new ListenStateError("Subscription lock changed concurrently. Retry the command.");
|
|
805
|
-
} finally {
|
|
806
|
-
if (!installed) removeOwner(candidate, owner);
|
|
807
|
-
}
|
|
808
|
-
let released = false;
|
|
809
|
-
return () => {
|
|
810
|
-
if (!released) {
|
|
811
|
-
released = true;
|
|
812
|
-
removeOwner(path, owner);
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
function resolveListenSubscription(configDir, identity, explicit) {
|
|
817
|
-
const directory = join(configDir, "listen", identity);
|
|
818
|
-
privateDirectory(directory);
|
|
819
|
-
let name = explicit;
|
|
820
|
-
if (name === void 0) {
|
|
821
|
-
const unlock = acquireListenLock(directory, "default-name");
|
|
822
|
-
try {
|
|
823
|
-
const file = join(directory, "default.json");
|
|
824
|
-
try {
|
|
825
|
-
if (!lstatSync(file).isFile() || lstatSync(file).isSymbolicLink()) throw new ListenStateError("Invalid listener identity file.");
|
|
826
|
-
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
827
|
-
if (typeof saved.name !== "string" || !/^local-[a-f0-9-]{36}$/.test(saved.name)) throw new ListenStateError("Invalid listener identity file.");
|
|
828
|
-
name = saved.name;
|
|
829
|
-
} catch (error) {
|
|
830
|
-
if (error.code !== "ENOENT") throw error;
|
|
831
|
-
name = `local-${randomUUID()}`;
|
|
832
|
-
const temporary = join(directory, `.state-${randomUUID()}`);
|
|
833
|
-
const fd = openSync(temporary, "wx", 384);
|
|
834
|
-
try {
|
|
835
|
-
writeFileSync(fd, `${JSON.stringify({ name })}\n`);
|
|
836
|
-
fsyncSync(fd);
|
|
837
|
-
} finally {
|
|
838
|
-
closeSync(fd);
|
|
839
|
-
}
|
|
840
|
-
renameSync(temporary, file);
|
|
841
|
-
}
|
|
842
|
-
if (process.platform !== "win32") for (const path of [
|
|
843
|
-
directory,
|
|
844
|
-
join(configDir, "listen"),
|
|
845
|
-
configDir
|
|
846
|
-
]) {
|
|
847
|
-
const directoryFd = openSync(path, "r");
|
|
848
|
-
try {
|
|
849
|
-
fsyncSync(directoryFd);
|
|
850
|
-
} finally {
|
|
851
|
-
closeSync(directoryFd);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
} finally {
|
|
855
|
-
unlock();
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
if (!name || name.length > 64 || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) throw new ListenStateError("Use a subscription name of 1–64 letters, digits, underscores, or hyphens.");
|
|
640
|
+
//#region src/oclif/connected-agent-profile.ts
|
|
641
|
+
const AGENT_PROFILE_ENV = "PRIMITIVE_AGENT_PROFILE";
|
|
642
|
+
function parsePresenceProfile(value, owner) {
|
|
643
|
+
const row = mailObject(value, [
|
|
644
|
+
"protocol",
|
|
645
|
+
"version",
|
|
646
|
+
"authentication_profile",
|
|
647
|
+
"return_address"
|
|
648
|
+
]);
|
|
649
|
+
if (row.protocol !== "primitive.presence" || row.version !== 1 || row.authentication_profile !== "primitive-issued-v1" || mailAddress(row.return_address) !== owner) throw new AgentConnectionSetupError("The presence authentication profile is not supported.");
|
|
859
650
|
return {
|
|
860
|
-
|
|
861
|
-
|
|
651
|
+
protocol: "primitive.presence",
|
|
652
|
+
version: 1,
|
|
653
|
+
authentication_profile: "primitive-issued-v1",
|
|
654
|
+
return_address: owner
|
|
862
655
|
};
|
|
863
656
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
var SharedMailStateError = class extends ListenStateError {};
|
|
867
|
-
const invalidSharedMail = () => new SharedMailStateError("Shared mail state is invalid or inconsistent. Preserve it before retrying to avoid duplicate delivery.");
|
|
868
|
-
function privateMailPermissions(info, platform = process.platform) {
|
|
869
|
-
return platform === "win32" || info.uid === process.getuid?.() && (info.mode & 63) === 0;
|
|
870
|
-
}
|
|
871
|
-
function privateMailDirectory(path, create = false) {
|
|
872
|
-
if (create) {
|
|
873
|
-
const created = mkdirSync(path, {
|
|
874
|
-
recursive: true,
|
|
875
|
-
mode: 448
|
|
876
|
-
});
|
|
877
|
-
if (created !== void 0) {
|
|
878
|
-
let current = path;
|
|
879
|
-
for (;;) {
|
|
880
|
-
syncMailDirectory(current);
|
|
881
|
-
syncMailDirectory(dirname(current));
|
|
882
|
-
if (current === created) break;
|
|
883
|
-
current = dirname(current);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
const stat = lstatSync(path);
|
|
888
|
-
if (!stat.isDirectory() || !privateMailPermissions(stat)) throw invalidSharedMail();
|
|
889
|
-
}
|
|
890
|
-
function syncMailDirectory(path, platform = process.platform) {
|
|
891
|
-
if (platform === "win32") return;
|
|
892
|
-
const fd = openSync(path, "r");
|
|
893
|
-
try {
|
|
894
|
-
fsyncSync(fd);
|
|
895
|
-
} finally {
|
|
896
|
-
closeSync(fd);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
function readMailJson(path, maxBytes = 16384) {
|
|
900
|
-
try {
|
|
901
|
-
const info = lstatSync(path);
|
|
902
|
-
if (!info.isFile() || !privateMailPermissions(info) || info.size > maxBytes) throw invalidSharedMail();
|
|
903
|
-
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
904
|
-
if (value === null) throw invalidSharedMail();
|
|
905
|
-
return value;
|
|
906
|
-
} catch (error) {
|
|
907
|
-
if (error.code === "ENOENT") return null;
|
|
908
|
-
throw invalidSharedMail();
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
function writeMailJson(path, value) {
|
|
912
|
-
privateMailDirectory(dirname(path), true);
|
|
913
|
-
const temporary = join(dirname(path), `.write-${randomUUID()}`);
|
|
657
|
+
function supportedPresenceProfile(value, owner) {
|
|
658
|
+
if (value === void 0) return void 0;
|
|
914
659
|
try {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
fsyncSync(fd);
|
|
919
|
-
} finally {
|
|
920
|
-
closeSync(fd);
|
|
921
|
-
}
|
|
922
|
-
renameSync(temporary, path);
|
|
923
|
-
syncMailDirectory(dirname(path));
|
|
924
|
-
} finally {
|
|
925
|
-
try {
|
|
926
|
-
unlinkSync(temporary);
|
|
927
|
-
} catch {}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
function removeMailFile(path) {
|
|
931
|
-
try {
|
|
932
|
-
unlinkSync(path);
|
|
933
|
-
} catch (error) {
|
|
934
|
-
if (error.code !== "ENOENT") throw error;
|
|
935
|
-
}
|
|
936
|
-
syncMailDirectory(dirname(path));
|
|
937
|
-
}
|
|
938
|
-
function mailLockBusy(error) {
|
|
939
|
-
return error instanceof ListenStateError && /^(Another listener|Subscription lock changed)/.test(error.message);
|
|
940
|
-
}
|
|
941
|
-
async function withMailLock(directory, action, signal) {
|
|
942
|
-
const deadline = performance.now() + 5e3;
|
|
943
|
-
for (;;) {
|
|
944
|
-
signal?.throwIfAborted();
|
|
945
|
-
let release;
|
|
946
|
-
try {
|
|
947
|
-
release = acquireListenLock(directory, "shared-mail-state");
|
|
948
|
-
} catch (error) {
|
|
949
|
-
if (!mailLockBusy(error) || performance.now() >= deadline) throw error;
|
|
950
|
-
await setTimeout$1(25, void 0, { signal });
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
try {
|
|
954
|
-
signal?.throwIfAborted();
|
|
955
|
-
return action();
|
|
956
|
-
} finally {
|
|
957
|
-
release();
|
|
958
|
-
}
|
|
660
|
+
return parsePresenceProfile(value, owner);
|
|
661
|
+
} catch {
|
|
662
|
+
return;
|
|
959
663
|
}
|
|
960
664
|
}
|
|
961
|
-
function mailObject(value, keys) {
|
|
962
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidSharedMail();
|
|
963
|
-
const result = value;
|
|
964
|
-
if (Object.keys(result).length !== keys.length || keys.some((key) => !Object.hasOwn(result, key))) throw invalidSharedMail();
|
|
965
|
-
return result;
|
|
966
|
-
}
|
|
967
|
-
function mailId(value) {
|
|
968
|
-
if (typeof value !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value)) throw invalidSharedMail();
|
|
969
|
-
return value.toLowerCase();
|
|
970
|
-
}
|
|
971
|
-
function mailString(value, max = 256) {
|
|
972
|
-
if (typeof value !== "string" || !value || value.length > max || Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) throw invalidSharedMail();
|
|
973
|
-
return value;
|
|
974
|
-
}
|
|
975
|
-
function mailAddress(value) {
|
|
976
|
-
const address = mailString(value, 254).toLowerCase();
|
|
977
|
-
if (!/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(address)) throw invalidSharedMail();
|
|
978
|
-
return address;
|
|
979
|
-
}
|
|
980
|
-
function mailTime(value) {
|
|
981
|
-
const time = mailString(value, 40);
|
|
982
|
-
if (!Number.isFinite(Date.parse(time))) throw invalidSharedMail();
|
|
983
|
-
return new Date(time).toISOString();
|
|
984
|
-
}
|
|
985
|
-
//#endregion
|
|
986
|
-
//#region src/oclif/connected-agent-profile.ts
|
|
987
|
-
const AGENT_PROFILE_ENV = "PRIMITIVE_AGENT_PROFILE";
|
|
988
665
|
var AgentConnectionSetupError = class extends Error {};
|
|
989
666
|
function requireDefaultLoginProfile() {
|
|
990
667
|
if (process.env["PRIMITIVE_AGENT_PROFILE"]?.trim()) throw new AgentConnectionSetupError("This command manages the default OAuth login. Unset PRIMITIVE_AGENT_PROFILE before using it; the connected-agent profile is unchanged.");
|
|
@@ -1008,7 +685,7 @@ function profilePath(configDir, profileName) {
|
|
|
1008
685
|
}
|
|
1009
686
|
function parseConnectedAgentProfile(value) {
|
|
1010
687
|
try {
|
|
1011
|
-
const
|
|
688
|
+
const keys = [
|
|
1012
689
|
"version",
|
|
1013
690
|
"auth_method",
|
|
1014
691
|
"api_key",
|
|
@@ -1018,10 +695,12 @@ function parseConnectedAgentProfile(value) {
|
|
|
1018
695
|
"owner_address",
|
|
1019
696
|
"invitation_hash",
|
|
1020
697
|
"created_at"
|
|
1021
|
-
]
|
|
698
|
+
];
|
|
699
|
+
const row = mailObject(value, value && typeof value === "object" && Object.hasOwn(value, "presence_profile") ? [...keys, "presence_profile"] : keys);
|
|
1022
700
|
const apiKey = mailString(row.api_key, 4096);
|
|
1023
701
|
const invitationHash = mailString(row.invitation_hash, 64);
|
|
1024
702
|
if (row.version !== 1 || row.auth_method !== "agent_connection" || !apiKey.startsWith("pconn_") || /\s/.test(apiKey) || !/^[a-f0-9]{64}$/.test(invitationHash)) throw new Error();
|
|
703
|
+
const presence = supportedPresenceProfile(row.presence_profile, mailAddress(row.owner_address));
|
|
1025
704
|
return {
|
|
1026
705
|
version: 1,
|
|
1027
706
|
auth_method: "agent_connection",
|
|
@@ -1031,7 +710,8 @@ function parseConnectedAgentProfile(value) {
|
|
|
1031
710
|
agent_address: mailAddress(row.agent_address),
|
|
1032
711
|
owner_address: mailAddress(row.owner_address),
|
|
1033
712
|
invitation_hash: invitationHash,
|
|
1034
|
-
created_at: mailTime(row.created_at)
|
|
713
|
+
created_at: mailTime(row.created_at),
|
|
714
|
+
...presence === void 0 ? {} : { presence_profile: presence }
|
|
1035
715
|
};
|
|
1036
716
|
} catch {
|
|
1037
717
|
throw new AgentConnectionSetupError("Saved agent profile is invalid. Preserve its private files and request a fresh setup invitation.");
|
|
@@ -1782,4 +1462,4 @@ function redactCliEnvironment(environment) {
|
|
|
1782
1462
|
};
|
|
1783
1463
|
}
|
|
1784
1464
|
//#endregion
|
|
1785
|
-
export {
|
|
1465
|
+
export { saveActiveChatState as A, requireDefaultLoginProfile as B, resolveCliAuth as C, deleteChatState as D, chatStatePath as E, agentProfilesDirectory as F, PrimitiveApiClient as H, connectedAgentIdentity as I, connectedApiBaseUrl as L, AgentConnectionSetupError as M, agentProfileDirectory as N, loadActiveChatState as O, agentProfileName as P, loadConnectedAgentProfile as R, normalizeApiBaseUrl as S, saveSignupCredentials as T, createClient as U, saveConnectedAgentProfile as V, createConfig as W, deleteCliCredentials as _, normalizeCliEnvironmentName as a, hasStoredCliLogin as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, AGENT_PROFILE_ENV as j, loadChatConversationByLocalId as k, saveCliConfig as l, cliAccessTokenExpiresAt as m, deleteCliConfig as n, redactCliEnvironment as o, acquireCliCredentialsLock as p, emptyCliConfig as r, removeCliEnvironment as s, DEFAULT_ENVIRONMENT as t, upsertCliEnvironment as u, deleteCliCredentialsLock as v, saveCliCredentials as w, loadCliCredentials as x, detectPrimitiveKeyEnvMisname as y, parseConnectedAgentProfile as z };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { format } from "node:util";
|
|
2
|
+
//#region src/oclif/json-output.ts
|
|
3
|
+
/**
|
|
4
|
+
* Parse-safe `--json` output.
|
|
5
|
+
*
|
|
6
|
+
* Agents often run commands with stderr merged into stdout (`2>&1`).
|
|
7
|
+
* A command that prints a JSON document on stdout and a notice on
|
|
8
|
+
* stderr then hands the caller text that is not JSON, and a delivered
|
|
9
|
+
* send can look like an unknown outcome. With `--json`, every guarded
|
|
10
|
+
* command therefore prints exactly one JSON document on stdout, on
|
|
11
|
+
* success and on failure, and writes nothing to stderr:
|
|
12
|
+
*
|
|
13
|
+
* - Anything the command would have written to stderr (notices, hints,
|
|
14
|
+
* cursors, warnings) goes into the document's `warnings` array.
|
|
15
|
+
* - A failure adds `error` and `exit_code` to the document. When the
|
|
16
|
+
* command printed no document of its own, the CLI builds one.
|
|
17
|
+
* - A command whose `--json` output is a bare JSON array keeps that
|
|
18
|
+
* documented shape on success; its stderr notices are dropped.
|
|
19
|
+
*
|
|
20
|
+
* Commands that stream several documents (`listen`) are not guarded.
|
|
21
|
+
*/
|
|
22
|
+
const GUARDED = Symbol.for("primitive.cli.jsonOutputGuard");
|
|
23
|
+
/** True when `--json` appears before any `--` separator. */
|
|
24
|
+
function jsonOutputRequested(argv) {
|
|
25
|
+
const end = argv.indexOf("--");
|
|
26
|
+
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
27
|
+
}
|
|
28
|
+
function chunkToString(chunk, encoding) {
|
|
29
|
+
if (typeof chunk === "string") return chunk;
|
|
30
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString(typeof encoding === "string" ? encoding : "utf8");
|
|
31
|
+
return String(chunk);
|
|
32
|
+
}
|
|
33
|
+
function capturingWrite(sink) {
|
|
34
|
+
return (chunk, encoding, callback) => {
|
|
35
|
+
sink.push(chunkToString(chunk, encoding));
|
|
36
|
+
const done = typeof encoding === "function" ? encoding : callback;
|
|
37
|
+
if (typeof done === "function") done();
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Redirect stdout and stderr (both the streams and the console methods
|
|
43
|
+
* that write to them) into memory until `restore` is called. Nested
|
|
44
|
+
* captures restore in reverse order, so a guarded command that runs
|
|
45
|
+
* another guarded command still produces one document.
|
|
46
|
+
*/
|
|
47
|
+
function captureOutput() {
|
|
48
|
+
const stdout = [];
|
|
49
|
+
const stderr = [];
|
|
50
|
+
const originalStdoutWrite = process.stdout.write;
|
|
51
|
+
const originalStderrWrite = process.stderr.write;
|
|
52
|
+
const originalConsole = {
|
|
53
|
+
debug: console.debug,
|
|
54
|
+
error: console.error,
|
|
55
|
+
info: console.info,
|
|
56
|
+
log: console.log,
|
|
57
|
+
warn: console.warn
|
|
58
|
+
};
|
|
59
|
+
const toStdout = (...args) => {
|
|
60
|
+
stdout.push(`${format(...args)}\n`);
|
|
61
|
+
};
|
|
62
|
+
const toStderr = (...args) => {
|
|
63
|
+
stderr.push(`${format(...args)}\n`);
|
|
64
|
+
};
|
|
65
|
+
process.stdout.write = capturingWrite(stdout);
|
|
66
|
+
process.stderr.write = capturingWrite(stderr);
|
|
67
|
+
console.log = toStdout;
|
|
68
|
+
console.info = toStdout;
|
|
69
|
+
console.debug = toStderr;
|
|
70
|
+
console.error = toStderr;
|
|
71
|
+
console.warn = toStderr;
|
|
72
|
+
return {
|
|
73
|
+
stderr,
|
|
74
|
+
stdout,
|
|
75
|
+
restore: () => {
|
|
76
|
+
process.stdout.write = originalStdoutWrite;
|
|
77
|
+
process.stderr.write = originalStderrWrite;
|
|
78
|
+
console.debug = originalConsole.debug;
|
|
79
|
+
console.error = originalConsole.error;
|
|
80
|
+
console.info = originalConsole.info;
|
|
81
|
+
console.log = originalConsole.log;
|
|
82
|
+
console.warn = originalConsole.warn;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function isPlainObject(value) {
|
|
87
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
function parseJson(text) {
|
|
90
|
+
try {
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
value: JSON.parse(text)
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
return { ok: false };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Split captured stderr into an API error payload (the JSON object the
|
|
101
|
+
* CLI prints for a failed request) and plain-text warning lines.
|
|
102
|
+
*/
|
|
103
|
+
const ANSI_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, "g");
|
|
104
|
+
function stripAnsi(text) {
|
|
105
|
+
return text.replace(ANSI_SEQUENCE, "");
|
|
106
|
+
}
|
|
107
|
+
function summarizeStderr(chunks) {
|
|
108
|
+
let errorPayload = null;
|
|
109
|
+
const warnings = [];
|
|
110
|
+
for (const chunk of chunks) {
|
|
111
|
+
const trimmed = stripAnsi(chunk).trim();
|
|
112
|
+
if (trimmed === "") continue;
|
|
113
|
+
if (trimmed.startsWith("{")) {
|
|
114
|
+
const parsed = parseJson(trimmed);
|
|
115
|
+
if (parsed.ok && isPlainObject(parsed.value)) {
|
|
116
|
+
if (errorPayload === null) errorPayload = parsed.value;
|
|
117
|
+
else warnings.push(JSON.stringify(parsed.value));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const line of trimmed.split("\n")) {
|
|
122
|
+
const text = line.trim();
|
|
123
|
+
if (text !== "" && !warnings.includes(text)) warnings.push(text);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
errorPayload,
|
|
128
|
+
warnings
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function errorExitCode(error) {
|
|
132
|
+
const exit = error?.oclif?.exit;
|
|
133
|
+
return typeof exit === "number" ? exit : void 0;
|
|
134
|
+
}
|
|
135
|
+
/** An oclif `this.exit(n)` unwinds by throwing; it is not a failure message. */
|
|
136
|
+
function isExitSignal(error) {
|
|
137
|
+
return error?.code === "EEXIT";
|
|
138
|
+
}
|
|
139
|
+
function thrownErrorBody(error) {
|
|
140
|
+
if (error instanceof Error) {
|
|
141
|
+
const body = { message: error.message };
|
|
142
|
+
const code = error.code;
|
|
143
|
+
if (typeof code === "string" && code !== "EEXIT") body.code = code;
|
|
144
|
+
return body;
|
|
145
|
+
}
|
|
146
|
+
return { message: String(error) };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Build the single JSON document a guarded `--json` command prints from
|
|
150
|
+
* what it wrote to stdout and stderr and how it ended.
|
|
151
|
+
*/
|
|
152
|
+
function composeJsonDocument(input) {
|
|
153
|
+
const { errorPayload, warnings: stderrLines } = summarizeStderr(input.stderr);
|
|
154
|
+
const failed = input.exitCode !== 0;
|
|
155
|
+
let warnings = stderrLines;
|
|
156
|
+
if (!failed && errorPayload !== null) warnings = [...warnings, JSON.stringify(errorPayload)];
|
|
157
|
+
const failure = () => {
|
|
158
|
+
if (errorPayload !== null) return errorPayload;
|
|
159
|
+
if (input.thrown && !isExitSignal(input.thrown.error)) return thrownErrorBody(input.thrown.error);
|
|
160
|
+
return { message: warnings.at(-1) ?? `The command failed with exit code ${input.exitCode}.` };
|
|
161
|
+
};
|
|
162
|
+
const text = input.stdout.trim();
|
|
163
|
+
let document;
|
|
164
|
+
let hasDocument = false;
|
|
165
|
+
if (text !== "") {
|
|
166
|
+
const parsed = parseJson(text);
|
|
167
|
+
document = parsed.ok ? parsed.value : { output: text };
|
|
168
|
+
hasDocument = true;
|
|
169
|
+
}
|
|
170
|
+
if (!hasDocument) {
|
|
171
|
+
if (failed) return {
|
|
172
|
+
error: failure(),
|
|
173
|
+
exit_code: input.exitCode,
|
|
174
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
175
|
+
};
|
|
176
|
+
return warnings.length > 0 ? { summary: warnings.join("\n") } : {};
|
|
177
|
+
}
|
|
178
|
+
if (!isPlainObject(document)) {
|
|
179
|
+
if (!failed) return document;
|
|
180
|
+
return {
|
|
181
|
+
data: document,
|
|
182
|
+
error: failure(),
|
|
183
|
+
exit_code: input.exitCode,
|
|
184
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const merged = { ...document };
|
|
188
|
+
const stated = new Set([merged.outcome_message, merged.summary].filter((value) => typeof value === "string"));
|
|
189
|
+
warnings = warnings.filter((warning) => !stated.has(warning));
|
|
190
|
+
if (failed) {
|
|
191
|
+
if (merged.error === void 0 || merged.error === null) merged.error = failure();
|
|
192
|
+
if (merged.exit_code === void 0) merged.exit_code = input.exitCode;
|
|
193
|
+
}
|
|
194
|
+
if (warnings.length > 0) {
|
|
195
|
+
const existing = merged.warnings;
|
|
196
|
+
if (existing === void 0 || existing === null) merged.warnings = warnings;
|
|
197
|
+
else if (Array.isArray(existing)) merged.warnings = [...existing, ...warnings.filter((warning) => !existing.includes(warning))];
|
|
198
|
+
else merged.cli_warnings = warnings;
|
|
199
|
+
}
|
|
200
|
+
return merged;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Make `cls` print one JSON document under `--json`. Idempotent per
|
|
204
|
+
* class; command aliases that share a class are guarded once.
|
|
205
|
+
*/
|
|
206
|
+
function guardJsonOutput(cls) {
|
|
207
|
+
const prototype = cls.prototype;
|
|
208
|
+
if (Object.hasOwn(prototype, GUARDED)) return;
|
|
209
|
+
const originalRun = prototype._run;
|
|
210
|
+
prototype[GUARDED] = true;
|
|
211
|
+
prototype._run = async function guardedRun() {
|
|
212
|
+
if (!jsonOutputRequested(this.argv)) return originalRun.call(this);
|
|
213
|
+
const capture = captureOutput();
|
|
214
|
+
let result;
|
|
215
|
+
let thrown;
|
|
216
|
+
try {
|
|
217
|
+
result = await originalRun.call(this);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
thrown = { error };
|
|
220
|
+
} finally {
|
|
221
|
+
capture.restore();
|
|
222
|
+
}
|
|
223
|
+
const document = composeJsonDocument({
|
|
224
|
+
exitCode: thrown ? isExitSignal(thrown.error) ? errorExitCode(thrown.error) ?? 0 : errorExitCode(thrown.error) ?? (typeof process.exitCode === "number" && process.exitCode !== 0 ? process.exitCode : 1) : typeof process.exitCode === "number" ? process.exitCode : 0,
|
|
225
|
+
stderr: capture.stderr,
|
|
226
|
+
stdout: capture.stdout.join(""),
|
|
227
|
+
thrown
|
|
228
|
+
});
|
|
229
|
+
console.log(JSON.stringify(document, null, 2));
|
|
230
|
+
if (thrown) {
|
|
231
|
+
if (thrown.error !== null && typeof thrown.error === "object") thrown.error.skipOclifErrorHandling = true;
|
|
232
|
+
throw thrown.error;
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** Command ids whose `--json` streams several documents over time. */
|
|
238
|
+
const STREAMING_JSON_COMMAND_IDS = new Set(["listen"]);
|
|
239
|
+
/**
|
|
240
|
+
* Guard every command in `commands` that accepts `--json`, except the
|
|
241
|
+
* streaming ones. Called once by the command registry.
|
|
242
|
+
*/
|
|
243
|
+
function guardJsonOutputCommands(commands) {
|
|
244
|
+
const streaming = /* @__PURE__ */ new Set();
|
|
245
|
+
for (const id of STREAMING_JSON_COMMAND_IDS) if (commands[id] !== void 0) streaming.add(commands[id]);
|
|
246
|
+
for (const command of Object.values(commands)) {
|
|
247
|
+
if (streaming.has(command)) continue;
|
|
248
|
+
const flags = command.flags;
|
|
249
|
+
const enableJsonFlag = command.enableJsonFlag;
|
|
250
|
+
if (flags && "json" in flags || enableJsonFlag === true) guardJsonOutput(command);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
export { jsonOutputRequested as n, guardJsonOutputCommands as t };
|