@aliyunrds/ctxdb 1.0.8-beta.2 → 1.0.8-beta.4
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 +47 -10
- package/dist/{chunk-X75B57M2.js → chunk-5DGRNP55.js} +1 -1
- package/dist/{chunk-R67JELM7.js → chunk-5ITT5GSP.js} +3 -2
- package/dist/{chunk-5ZSZMN2T.js → chunk-6MEWI73Y.js} +2 -2
- package/dist/{chunk-LYHGJOHM.js → chunk-76AXXLVE.js} +4 -3
- package/dist/{chunk-4FCZ2TZM.js → chunk-O6MKQ2I3.js} +1 -1
- package/dist/{chunk-ULAWTBBC.js → chunk-OEZM2ZYA.js} +1 -1
- package/dist/{chunk-VIG4SYLU.js → chunk-PI2SUS3M.js} +1 -1
- package/dist/{chunk-DMS5YHIK.js → chunk-SVO6H62S.js} +69 -28
- package/dist/{chunk-H33NUAHP.js → chunk-WJTV7JNR.js} +13 -6
- package/dist/{chunk-CULTDVI2.js → chunk-ZW7YXNJ6.js} +1 -1
- package/dist/cli/main.js +303 -81
- package/dist/hooks/hermes-post-llm-call.js +9 -7
- package/dist/hooks/hermes-pre-llm-call.js +12 -11
- package/dist/hooks/pre-tool-use.js +1 -1
- package/dist/hooks/session-start.js +11 -10
- package/dist/hooks/stop.js +9 -7
- package/dist/hooks/user-prompt-submit.js +11 -10
- package/dist/opencode/index.js +630 -41
- package/dist/workers/version-check.js +3 -3
- package/package.json +2 -2
package/dist/opencode/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// <define:__CTXDB_DISTRIBUTION_MANIFEST__>
|
|
2
|
-
var define_CTXDB_DISTRIBUTION_MANIFEST_default = { id: "public", packageName: "@aliyunrds/ctxdb", packageRegistry: null, capabilities: { interactiveLogin: false, managedCredentials: false } };
|
|
2
|
+
var define_CTXDB_DISTRIBUTION_MANIFEST_default = { id: "public", packageName: "@aliyunrds/ctxdb", packageRegistry: null, capabilities: { interactiveLogin: false, managedCredentials: false, deviceFlow: true } };
|
|
3
3
|
|
|
4
4
|
// src/config.ts
|
|
5
|
-
import { readFileSync as
|
|
5
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
6
6
|
import { homedir as homedir2 } from "os";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
8
8
|
|
|
9
9
|
// ../shared/src/graph-context.ts
|
|
10
10
|
var GRAPH_CONTEXT_TAG = "graphrag";
|
|
@@ -435,8 +435,8 @@ function getMemoryImportance(memory) {
|
|
|
435
435
|
};
|
|
436
436
|
return defaults[cat] ?? 0.5;
|
|
437
437
|
}
|
|
438
|
-
function estimateTokens(
|
|
439
|
-
return Math.ceil(
|
|
438
|
+
function estimateTokens(text2) {
|
|
439
|
+
return Math.ceil(text2.length / CHARS_PER_TOKEN);
|
|
440
440
|
}
|
|
441
441
|
function rankMemories(memories, categoryOrder) {
|
|
442
442
|
const orderMap = new Map(categoryOrder.map((cat, i) => [cat, i]));
|
|
@@ -690,9 +690,9 @@ function sanitizeRecallFailureReason(reason, secrets = []) {
|
|
|
690
690
|
}
|
|
691
691
|
function sanitizeTraceString(value, secrets) {
|
|
692
692
|
let sanitized = value.replace(/authorization\s*[:=]\s*[^\r\n]*/gi, "Authorization: [REDACTED]").replace(/(api[_-]?key|access[_-]?token)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
693
|
-
for (const
|
|
694
|
-
if (!
|
|
695
|
-
sanitized = sanitized.split(
|
|
693
|
+
for (const secret2 of secrets) {
|
|
694
|
+
if (!secret2) continue;
|
|
695
|
+
sanitized = sanitized.split(secret2).join("[REDACTED]");
|
|
696
696
|
}
|
|
697
697
|
return sanitized;
|
|
698
698
|
}
|
|
@@ -827,10 +827,568 @@ function isRecallTraceRecord(value) {
|
|
|
827
827
|
const baseValid = record.schema_version === RECALL_TRACE_SCHEMA_VERSION && (record.type === "recall.start" || record.type === "recall.finish") && isIsoTimestamp(record.timestamp) && typeof record.recall_event_id === "string" && record.recall_event_id.length > 0 && (typeof record.session_id === "string" || record.session_id === null) && isRecallTraceAgent(record.agent) && (record.kind === "prompt" || record.kind === "warmup") && typeof record.query === "string" && isRecallTraceRequest(record.request);
|
|
828
828
|
if (!baseValid) return false;
|
|
829
829
|
if (record.type === "recall.start") return true;
|
|
830
|
+
if (record.type !== "recall.finish") return false;
|
|
830
831
|
const selection = record.selection;
|
|
831
832
|
return isNonNegativeFinite(record.duration_ms) && typeof record.outcome === "string" && TRACE_OUTCOMES.has(record.outcome) && (typeof record.failure_reason === "string" || record.failure_reason === null) && Array.isArray(record.returned_memories) && Array.isArray(record.returned_knowledge) && Boolean(selection) && Array.isArray(selection?.selected) && Array.isArray(selection?.excluded) && isNonNegativeFinite(selection?.token_estimate) && typeof record.recall_context === "string";
|
|
832
833
|
}
|
|
833
834
|
|
|
835
|
+
// ../shared/src/device-flow.ts
|
|
836
|
+
import { setTimeout as delay } from "timers/promises";
|
|
837
|
+
var DeviceFlowError = class extends Error {
|
|
838
|
+
code;
|
|
839
|
+
constructor(code) {
|
|
840
|
+
super(code === "service_unavailable" ? "ContextDB authentication service is unavailable. No automatic retry was performed; start a new login later if authorization could not be completed." : `ContextDB device authorization: ${code}. Start a new login if authorization could not be completed.`);
|
|
841
|
+
this.name = "DeviceFlowError";
|
|
842
|
+
this.code = code;
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
var invalid = () => new DeviceFlowError("invalid_response");
|
|
846
|
+
var scopes = ["read_only", "read_write", "default"];
|
|
847
|
+
var clientId = "ctxdb-cli";
|
|
848
|
+
var grantType = "urn:ietf:params:oauth:grant-type:device_code";
|
|
849
|
+
function normalizeCoreOrigin(value) {
|
|
850
|
+
let url;
|
|
851
|
+
try {
|
|
852
|
+
url = new URL(value);
|
|
853
|
+
} catch {
|
|
854
|
+
throw new Error("Invalid Core server origin");
|
|
855
|
+
}
|
|
856
|
+
const local = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
857
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || local && url.protocol === "http:")) throw new Error("Invalid Core server origin");
|
|
858
|
+
return url.origin;
|
|
859
|
+
}
|
|
860
|
+
var CoreDeviceClient = class {
|
|
861
|
+
baseUrl;
|
|
862
|
+
fetchImpl;
|
|
863
|
+
now;
|
|
864
|
+
sleep;
|
|
865
|
+
constructor(baseUrl, options = {}) {
|
|
866
|
+
this.baseUrl = normalizeCoreOrigin(baseUrl);
|
|
867
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
868
|
+
this.now = options.now ?? Date.now;
|
|
869
|
+
this.sleep = options.sleep ?? ((ms, signal) => delay(ms, void 0, { signal }));
|
|
870
|
+
}
|
|
871
|
+
async login(options) {
|
|
872
|
+
const started = this.now();
|
|
873
|
+
const initial = await this.post("/v1/auth/device/authorize", { client_id: clientId }, options.signal);
|
|
874
|
+
if (!initial.ok) throw new DeviceFlowError("request_rejected");
|
|
875
|
+
const data = initial.data;
|
|
876
|
+
const requestId = text(data.request_id, 32), userCode = text(data.user_code, 9), code = text(data.device_code, 43);
|
|
877
|
+
if (!/^[0-9a-f]{32}$/.test(requestId) || !/^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/.test(userCode) || !/^[A-Za-z0-9_-]{43}$/.test(code)) throw invalid();
|
|
878
|
+
const expiresIn = integer(data.expires_in, 1, 3600);
|
|
879
|
+
let interval = integer(data.interval, 1, 3600);
|
|
880
|
+
const deadline = started + expiresIn * 1e3;
|
|
881
|
+
options.onPrompt({ requestId, userCode, expiresIn, interval });
|
|
882
|
+
while (true) {
|
|
883
|
+
if (options.signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
884
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
885
|
+
try {
|
|
886
|
+
await this.sleep(Math.min(interval * 1e3, deadline - this.now()), options.signal);
|
|
887
|
+
} catch {
|
|
888
|
+
throw new DeviceFlowError("cancelled");
|
|
889
|
+
}
|
|
890
|
+
if (this.now() >= deadline) throw new DeviceFlowError("expired_token");
|
|
891
|
+
const issuedAt = this.now();
|
|
892
|
+
const result = await this.post("/v1/auth/device/token", { grant_type: grantType, client_id: clientId, device_code: code }, options.signal);
|
|
893
|
+
if (result.ok) return loginResult(result.data, this.baseUrl, issuedAt, this.now());
|
|
894
|
+
const error = result.data.error;
|
|
895
|
+
if (error === "authorization_pending") continue;
|
|
896
|
+
if (error === "slow_down") {
|
|
897
|
+
interval = Math.max(interval + 5, integer(result.data.interval, 1, 3605));
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (error === "access_denied" || error === "expired_token" || error === "invalid_grant") throw new DeviceFlowError(error);
|
|
901
|
+
throw new DeviceFlowError("request_rejected");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async refresh(refreshToken, baseUrl) {
|
|
905
|
+
this.requireSameOrigin(baseUrl);
|
|
906
|
+
const result = await this.post("/v1/auth/token/refresh", { refresh_token: secret(refreshToken) });
|
|
907
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
908
|
+
return credentialResult(result.data);
|
|
909
|
+
}
|
|
910
|
+
async revoke(refreshToken, baseUrl) {
|
|
911
|
+
this.requireSameOrigin(baseUrl);
|
|
912
|
+
const result = await this.post("/v1/auth/token/revoke", { refresh_token: secret(refreshToken) });
|
|
913
|
+
if (!result.ok) throw new DeviceFlowError("request_rejected");
|
|
914
|
+
}
|
|
915
|
+
requireSameOrigin(baseUrl) {
|
|
916
|
+
if (normalizeCoreOrigin(baseUrl) !== this.baseUrl) throw new Error("OAuth credential Core origin cannot change");
|
|
917
|
+
}
|
|
918
|
+
async post(path, body, signal) {
|
|
919
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
920
|
+
const controller = new AbortController();
|
|
921
|
+
const abort = () => controller.abort();
|
|
922
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
923
|
+
const timer = setTimeout(abort, 3e4);
|
|
924
|
+
timer.unref?.();
|
|
925
|
+
try {
|
|
926
|
+
const response = await this.fetchImpl(this.baseUrl + path, {
|
|
927
|
+
method: "POST",
|
|
928
|
+
body: JSON.stringify(body),
|
|
929
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
930
|
+
redirect: "error",
|
|
931
|
+
credentials: "omit",
|
|
932
|
+
cache: "no-store",
|
|
933
|
+
signal: controller.signal
|
|
934
|
+
});
|
|
935
|
+
const box = await boundedJson(response);
|
|
936
|
+
if (!Number.isInteger(box.errorCode)) throw invalid();
|
|
937
|
+
const ok = response.ok && box.errorCode === 0;
|
|
938
|
+
const data = (!ok || path === "/v1/auth/token/revoke") && box.data == null ? {} : object(box.data);
|
|
939
|
+
if (!ok && response.status >= 500) throw new DeviceFlowError("service_unavailable");
|
|
940
|
+
return { ok, data };
|
|
941
|
+
} catch (error) {
|
|
942
|
+
if (signal?.aborted) throw new DeviceFlowError("cancelled");
|
|
943
|
+
if (error instanceof DeviceFlowError) throw error;
|
|
944
|
+
throw new DeviceFlowError("network_error");
|
|
945
|
+
} finally {
|
|
946
|
+
clearTimeout(timer);
|
|
947
|
+
signal?.removeEventListener("abort", abort);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
function text(value, maximum) {
|
|
952
|
+
if (typeof value !== "string" || !value || value.length > maximum || /[\x00-\x1f\x7f]/.test(value)) throw invalid();
|
|
953
|
+
return value;
|
|
954
|
+
}
|
|
955
|
+
function integer(value, min, max) {
|
|
956
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw invalid();
|
|
957
|
+
return value;
|
|
958
|
+
}
|
|
959
|
+
function secret(value) {
|
|
960
|
+
const token = text(value, 8192);
|
|
961
|
+
if (/\s/.test(token)) throw invalid();
|
|
962
|
+
return token;
|
|
963
|
+
}
|
|
964
|
+
function object(value) {
|
|
965
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
|
|
966
|
+
return value;
|
|
967
|
+
}
|
|
968
|
+
function credentialResult(data) {
|
|
969
|
+
if (data.token_type !== "Bearer" || !scopes.includes(data.scope)) throw invalid();
|
|
970
|
+
return {
|
|
971
|
+
access_token: secret(data.access_token),
|
|
972
|
+
refresh_token: secret(data.refresh_token),
|
|
973
|
+
token_type: "Bearer",
|
|
974
|
+
expires_in: integer(data.expires_in, 1, 3600),
|
|
975
|
+
session_id: text(data.session_id, 64),
|
|
976
|
+
scope: data.scope
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function loginResult(data, baseUrl, issuedAt, now) {
|
|
980
|
+
const tokens = credentialResult(data), scope = tokens.scope;
|
|
981
|
+
if (data.client_id !== clientId || scope !== "default") throw invalid();
|
|
982
|
+
const expiresAt = issuedAt + tokens.expires_in * 1e3;
|
|
983
|
+
if (expiresAt <= now) throw invalid();
|
|
984
|
+
return {
|
|
985
|
+
baseUrl,
|
|
986
|
+
workspaceId: text(data.workspace_id, 64),
|
|
987
|
+
memberId: text(data.member_id, 64),
|
|
988
|
+
scope,
|
|
989
|
+
clientId,
|
|
990
|
+
sessionId: tokens.session_id,
|
|
991
|
+
accountDisplay: text(data.account_display, 256),
|
|
992
|
+
issuedAt,
|
|
993
|
+
expiresAt,
|
|
994
|
+
accessToken: tokens.access_token,
|
|
995
|
+
refreshToken: tokens.refresh_token
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
async function boundedJson(response) {
|
|
999
|
+
if (!response.body || Number(response.headers.get("content-length")) > 65536) throw invalid();
|
|
1000
|
+
const reader = response.body.getReader();
|
|
1001
|
+
const chunks = [];
|
|
1002
|
+
let size = 0;
|
|
1003
|
+
try {
|
|
1004
|
+
while (true) {
|
|
1005
|
+
const { done, value } = await reader.read();
|
|
1006
|
+
if (done) break;
|
|
1007
|
+
size += value.byteLength;
|
|
1008
|
+
if (size > 65536) {
|
|
1009
|
+
await reader.cancel();
|
|
1010
|
+
throw invalid();
|
|
1011
|
+
}
|
|
1012
|
+
chunks.push(value);
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
return object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1016
|
+
} catch {
|
|
1017
|
+
throw invalid();
|
|
1018
|
+
}
|
|
1019
|
+
} finally {
|
|
1020
|
+
reader.releaseLock();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ../shared/src/oauth-credentials.ts
|
|
1025
|
+
import {
|
|
1026
|
+
constants,
|
|
1027
|
+
closeSync as closeSync2,
|
|
1028
|
+
existsSync as existsSync2,
|
|
1029
|
+
fstatSync,
|
|
1030
|
+
fsyncSync,
|
|
1031
|
+
lstatSync,
|
|
1032
|
+
mkdirSync as mkdirSync2,
|
|
1033
|
+
openSync as openSync2,
|
|
1034
|
+
readFileSync as readFileSync2,
|
|
1035
|
+
renameSync as renameSync2,
|
|
1036
|
+
unlinkSync,
|
|
1037
|
+
writeFileSync
|
|
1038
|
+
} from "fs";
|
|
1039
|
+
import { randomBytes } from "crypto";
|
|
1040
|
+
import { join as join3 } from "path";
|
|
1041
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
1042
|
+
|
|
1043
|
+
// ../shared/src/windows-private-storage.ts
|
|
1044
|
+
import { execFileSync } from "child_process";
|
|
1045
|
+
import { join as join2 } from "path";
|
|
1046
|
+
var SCRIPT = `
|
|
1047
|
+
$ErrorActionPreference = 'Stop'
|
|
1048
|
+
$path = $env:CTXDB_PRIVATE_STORAGE_PATH
|
|
1049
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
1050
|
+
$item = Get-Item -LiteralPath $path -Force
|
|
1051
|
+
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'reparse' }
|
|
1052
|
+
if ($env:CTXDB_PRIVATE_STORAGE_INITIALIZE -eq '1') {
|
|
1053
|
+
if (-not $item.PSIsContainer) { throw 'not directory' }
|
|
1054
|
+
$acl = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
1055
|
+
$acl.SetOwner($sid)
|
|
1056
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
1057
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
1058
|
+
$acl.AddAccessRule($rule)
|
|
1059
|
+
Set-Acl -LiteralPath $path -AclObject $acl
|
|
1060
|
+
}
|
|
1061
|
+
$acl = Get-Acl -LiteralPath $path
|
|
1062
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'owner' }
|
|
1063
|
+
$self = $false
|
|
1064
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
1065
|
+
if ($rule.AccessControlType -eq 'Allow') {
|
|
1066
|
+
if ($rule.IdentityReference.Value -ne $sid.Value) { throw 'other principal' }
|
|
1067
|
+
if (($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl) { $self = $true }
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (-not $self) { throw 'missing owner access' }
|
|
1071
|
+
Write-Output 'private'
|
|
1072
|
+
`;
|
|
1073
|
+
function checkWindowsPrivateStorage(path, initialize, run = execFileSync) {
|
|
1074
|
+
const systemRoot = process.env.SystemRoot;
|
|
1075
|
+
if (!systemRoot) throw new Error("Private Windows credential storage could not be verified.");
|
|
1076
|
+
try {
|
|
1077
|
+
const result = run(
|
|
1078
|
+
join2(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
1079
|
+
["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(SCRIPT, "utf16le").toString("base64")],
|
|
1080
|
+
{
|
|
1081
|
+
env: { ...process.env, CTXDB_PRIVATE_STORAGE_PATH: path, CTXDB_PRIVATE_STORAGE_INITIALIZE: initialize ? "1" : "0" },
|
|
1082
|
+
encoding: "utf8",
|
|
1083
|
+
windowsHide: true,
|
|
1084
|
+
timeout: 1e4,
|
|
1085
|
+
maxBuffer: 4096,
|
|
1086
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
if (String(result).trim() !== "private") throw new Error();
|
|
1090
|
+
} catch {
|
|
1091
|
+
throw new Error("Private Windows credential storage could not be verified.");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ../shared/src/oauth-credentials.ts
|
|
1096
|
+
var OAuthCredentialError = class extends Error {
|
|
1097
|
+
code;
|
|
1098
|
+
constructor(code) {
|
|
1099
|
+
const messages = {
|
|
1100
|
+
login_required: "OAuth session needs login; run ctxdb login for this profile.",
|
|
1101
|
+
credential_busy: "OAuth credential is busy. If its previous process crashed, run ctxdb login for a new credential.",
|
|
1102
|
+
invalid_profile: "OAuth credential does not belong to this profile.",
|
|
1103
|
+
unsafe_storage: "OAuth credential storage is not private or is invalid."
|
|
1104
|
+
};
|
|
1105
|
+
super(messages[code]);
|
|
1106
|
+
this.name = "OAuthCredentialError";
|
|
1107
|
+
this.code = code;
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
var loginRequired = () => new OAuthCredentialError("login_required");
|
|
1111
|
+
var unsafe = () => new OAuthCredentialError("unsafe_storage");
|
|
1112
|
+
var OAuthSessionStore = class {
|
|
1113
|
+
directory;
|
|
1114
|
+
now;
|
|
1115
|
+
lockWaitMs;
|
|
1116
|
+
constructor(directory, options = {}) {
|
|
1117
|
+
this.directory = directory;
|
|
1118
|
+
this.now = options.now ?? Date.now;
|
|
1119
|
+
this.lockWaitMs = options.lockWaitMs ?? 3e4;
|
|
1120
|
+
}
|
|
1121
|
+
saveLogin(profile, session) {
|
|
1122
|
+
requireProfile(profile);
|
|
1123
|
+
validateSession(session);
|
|
1124
|
+
this.prepareDirectory();
|
|
1125
|
+
const ref = randomBytes(16).toString("hex");
|
|
1126
|
+
this.write(ref, { version: 1, profile, state: "ready", session });
|
|
1127
|
+
return ref;
|
|
1128
|
+
}
|
|
1129
|
+
status(ref, profile) {
|
|
1130
|
+
const record = this.read(ref, profile);
|
|
1131
|
+
if (!record) return { state: this.isLoggedOut(ref) ? "logged_out" : "missing" };
|
|
1132
|
+
const s = record.session;
|
|
1133
|
+
return {
|
|
1134
|
+
state: record.state,
|
|
1135
|
+
profile,
|
|
1136
|
+
baseUrl: s.baseUrl,
|
|
1137
|
+
workspaceId: s.workspaceId,
|
|
1138
|
+
memberId: s.memberId,
|
|
1139
|
+
clientId: s.clientId,
|
|
1140
|
+
scope: s.scope,
|
|
1141
|
+
sessionId: s.sessionId,
|
|
1142
|
+
accountDisplay: s.accountDisplay,
|
|
1143
|
+
accessExpiresAt: s.expiresAt
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
async resolve(ref, profile, transport) {
|
|
1147
|
+
const first = this.read(ref, profile);
|
|
1148
|
+
if (!first || first.state === "reauth_required") throw loginRequired();
|
|
1149
|
+
if (first.state === "ready" && !this.needsRefresh(first.session)) return authorization(first.session);
|
|
1150
|
+
return this.withLock(ref, async () => {
|
|
1151
|
+
const current = this.read(ref, profile);
|
|
1152
|
+
if (!current || current.state !== "ready") throw loginRequired();
|
|
1153
|
+
if (!this.needsRefresh(current.session)) return authorization(current.session);
|
|
1154
|
+
const started = this.now();
|
|
1155
|
+
this.write(ref, { ...current, state: "refreshing" });
|
|
1156
|
+
try {
|
|
1157
|
+
const response = await transport.refresh(current.session.refreshToken, current.session.baseUrl);
|
|
1158
|
+
const next = rotated(current.session, response, started);
|
|
1159
|
+
if (next.expiresAt <= this.now()) throw loginRequired();
|
|
1160
|
+
this.write(ref, { ...current, state: "ready", session: next });
|
|
1161
|
+
return authorization(next);
|
|
1162
|
+
} catch {
|
|
1163
|
+
if (!this.isLoggedOut(ref)) this.write(ref, { ...current, state: "reauth_required" });
|
|
1164
|
+
throw loginRequired();
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
async logout(ref, profile, transport) {
|
|
1169
|
+
const current = this.read(ref, profile);
|
|
1170
|
+
let remoteRevoked = false;
|
|
1171
|
+
try {
|
|
1172
|
+
if (current) {
|
|
1173
|
+
await transport.revoke(current.session.refreshToken, current.session.baseUrl);
|
|
1174
|
+
remoteRevoked = true;
|
|
1175
|
+
}
|
|
1176
|
+
} catch {
|
|
1177
|
+
} finally {
|
|
1178
|
+
this.prepareDirectory();
|
|
1179
|
+
this.atomicWrite(this.path(ref, ".logged-out"), "");
|
|
1180
|
+
remove(this.path(ref));
|
|
1181
|
+
}
|
|
1182
|
+
return { remoteRevoked };
|
|
1183
|
+
}
|
|
1184
|
+
needsRefresh(session) {
|
|
1185
|
+
const advance = Math.min(3e4, (session.expiresAt - session.issuedAt) / 2);
|
|
1186
|
+
return this.now() >= session.expiresAt - advance;
|
|
1187
|
+
}
|
|
1188
|
+
path(ref, suffix = ".json") {
|
|
1189
|
+
if (!/^[a-f0-9]{32}$/.test(ref)) throw unsafe();
|
|
1190
|
+
return join3(this.directory, ref + suffix);
|
|
1191
|
+
}
|
|
1192
|
+
isLoggedOut(ref) {
|
|
1193
|
+
return existsSync2(this.path(ref, ".logged-out"));
|
|
1194
|
+
}
|
|
1195
|
+
prepareDirectory() {
|
|
1196
|
+
if (!existsSync2(this.directory)) {
|
|
1197
|
+
mkdirSync2(join3(this.directory, ".."), { recursive: true, mode: 448 });
|
|
1198
|
+
try {
|
|
1199
|
+
mkdirSync2(this.directory, { mode: 448 });
|
|
1200
|
+
if (process.platform === "win32") checkWindowsPrivateStorage(this.directory, true);
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
if (error.code !== "EEXIST") throw unsafe();
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
requirePrivate(this.directory, true);
|
|
1206
|
+
}
|
|
1207
|
+
read(ref, profile) {
|
|
1208
|
+
requireProfile(profile);
|
|
1209
|
+
if (this.isLoggedOut(ref)) return void 0;
|
|
1210
|
+
const path = this.path(ref);
|
|
1211
|
+
if (!existsSync2(path)) return void 0;
|
|
1212
|
+
requirePrivate(this.directory, true);
|
|
1213
|
+
requirePrivate(path, false);
|
|
1214
|
+
const fd = openSync2(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1215
|
+
let text2;
|
|
1216
|
+
try {
|
|
1217
|
+
const stat = fstatSync(fd);
|
|
1218
|
+
if (stat.size > 65536 || !stat.isFile()) throw unsafe();
|
|
1219
|
+
text2 = readFileSync2(fd, "utf8");
|
|
1220
|
+
} finally {
|
|
1221
|
+
closeSync2(fd);
|
|
1222
|
+
}
|
|
1223
|
+
let doc;
|
|
1224
|
+
try {
|
|
1225
|
+
doc = JSON.parse(text2);
|
|
1226
|
+
} catch {
|
|
1227
|
+
throw unsafe();
|
|
1228
|
+
}
|
|
1229
|
+
if (!doc || doc.version !== 1 || !["ready", "refreshing", "reauth_required"].includes(doc.state)) throw unsafe();
|
|
1230
|
+
if (doc.profile !== profile) throw new OAuthCredentialError("invalid_profile");
|
|
1231
|
+
validateSession(doc.session);
|
|
1232
|
+
return this.isLoggedOut(ref) ? void 0 : doc;
|
|
1233
|
+
}
|
|
1234
|
+
write(ref, doc) {
|
|
1235
|
+
if (this.isLoggedOut(ref)) throw loginRequired();
|
|
1236
|
+
this.atomicWrite(this.path(ref), JSON.stringify(doc));
|
|
1237
|
+
if (this.isLoggedOut(ref)) {
|
|
1238
|
+
remove(this.path(ref));
|
|
1239
|
+
throw loginRequired();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
atomicWrite(path, text2) {
|
|
1243
|
+
this.prepareDirectory();
|
|
1244
|
+
const temporary = path + "." + randomBytes(12).toString("hex") + ".tmp";
|
|
1245
|
+
let fd;
|
|
1246
|
+
try {
|
|
1247
|
+
fd = openSync2(temporary, "wx", 384);
|
|
1248
|
+
writeFileSync(fd, text2);
|
|
1249
|
+
fsyncSync(fd);
|
|
1250
|
+
closeSync2(fd);
|
|
1251
|
+
fd = void 0;
|
|
1252
|
+
renameSync2(temporary, path);
|
|
1253
|
+
if (process.platform !== "win32") {
|
|
1254
|
+
const dir = openSync2(this.directory, "r");
|
|
1255
|
+
try {
|
|
1256
|
+
fsyncSync(dir);
|
|
1257
|
+
} finally {
|
|
1258
|
+
closeSync2(dir);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
} finally {
|
|
1262
|
+
if (fd !== void 0) closeSync2(fd);
|
|
1263
|
+
remove(temporary);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
async withLock(ref, task) {
|
|
1267
|
+
const path = this.path(ref, ".lock");
|
|
1268
|
+
const deadline = Date.now() + this.lockWaitMs;
|
|
1269
|
+
this.prepareDirectory();
|
|
1270
|
+
while (true) {
|
|
1271
|
+
let fd;
|
|
1272
|
+
try {
|
|
1273
|
+
fd = openSync2(path, "wx", 384);
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
if (error.code !== "EEXIST") throw unsafe();
|
|
1276
|
+
if (Date.now() >= deadline) throw new OAuthCredentialError("credential_busy");
|
|
1277
|
+
await delay2(25);
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
writeFileSync(fd, String(process.pid));
|
|
1282
|
+
fsyncSync(fd);
|
|
1283
|
+
} catch {
|
|
1284
|
+
closeSync2(fd);
|
|
1285
|
+
remove(path);
|
|
1286
|
+
throw unsafe();
|
|
1287
|
+
}
|
|
1288
|
+
closeSync2(fd);
|
|
1289
|
+
try {
|
|
1290
|
+
return await task();
|
|
1291
|
+
} finally {
|
|
1292
|
+
remove(path);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
function requireProfile(profile) {
|
|
1298
|
+
if (typeof profile !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("invalid_profile");
|
|
1299
|
+
}
|
|
1300
|
+
function requirePrivate(path, directory) {
|
|
1301
|
+
const s = lstatSync(path);
|
|
1302
|
+
if (s.isSymbolicLink() || (directory ? !s.isDirectory() : !s.isFile())) throw unsafe();
|
|
1303
|
+
if (process.platform === "win32") {
|
|
1304
|
+
try {
|
|
1305
|
+
checkWindowsPrivateStorage(path, false);
|
|
1306
|
+
} catch {
|
|
1307
|
+
throw unsafe();
|
|
1308
|
+
}
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if ((s.mode & 63) !== 0 || process.getuid && s.uid !== process.getuid()) throw unsafe();
|
|
1312
|
+
}
|
|
1313
|
+
function remove(path) {
|
|
1314
|
+
try {
|
|
1315
|
+
unlinkSync(path);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
if (error.code !== "ENOENT") throw unsafe();
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function validateSession(s) {
|
|
1321
|
+
if (!s || typeof s !== "object" || s.clientId !== "ctxdb-cli" || !["read_only", "read_write", "default"].includes(s.scope) || !Number.isSafeInteger(s.issuedAt) || !Number.isSafeInteger(s.expiresAt) || s.expiresAt - s.issuedAt < 1e3 || s.expiresAt - s.issuedAt > 36e5) throw unsafe();
|
|
1322
|
+
for (const value of [s.workspaceId, s.memberId, s.sessionId, s.accountDisplay]) {
|
|
1323
|
+
if (typeof value !== "string" || !value || value.length > 256 || /[\r\n\x00-\x1f]/.test(value)) throw unsafe();
|
|
1324
|
+
}
|
|
1325
|
+
for (const value of [s.accessToken, s.refreshToken]) {
|
|
1326
|
+
if (typeof value !== "string" || !value || value.length > 8192 || /\s/.test(value)) throw unsafe();
|
|
1327
|
+
}
|
|
1328
|
+
let url;
|
|
1329
|
+
try {
|
|
1330
|
+
url = new URL(s.baseUrl);
|
|
1331
|
+
} catch {
|
|
1332
|
+
throw unsafe();
|
|
1333
|
+
}
|
|
1334
|
+
const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname);
|
|
1335
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || !(url.protocol === "https:" || loopback && url.protocol === "http:")) throw unsafe();
|
|
1336
|
+
}
|
|
1337
|
+
function rotated(current, response, issuedAt) {
|
|
1338
|
+
if (!response || response.token_type !== "Bearer" || response.session_id !== current.sessionId || response.scope !== current.scope || !Number.isSafeInteger(response.expires_in) || response.expires_in < 1 || response.expires_in > 3600 || response.refresh_token === current.refreshToken) throw loginRequired();
|
|
1339
|
+
const next = {
|
|
1340
|
+
...current,
|
|
1341
|
+
accessToken: response.access_token,
|
|
1342
|
+
refreshToken: response.refresh_token,
|
|
1343
|
+
issuedAt,
|
|
1344
|
+
expiresAt: issuedAt + response.expires_in * 1e3
|
|
1345
|
+
};
|
|
1346
|
+
validateSession(next);
|
|
1347
|
+
return next;
|
|
1348
|
+
}
|
|
1349
|
+
function authorization(session) {
|
|
1350
|
+
return { scheme: "Bearer", value: session.accessToken, baseUrl: session.baseUrl, expiresAt: session.expiresAt };
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// ../shared/src/oauth-profile.ts
|
|
1354
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1355
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
1356
|
+
function oauthProfile(reference, configPath, profile, baseUrl) {
|
|
1357
|
+
if (reference === void 0) return void 0;
|
|
1358
|
+
if (typeof reference !== "string" || !/^[a-f0-9]{32}$/.test(reference) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(profile)) throw new OAuthCredentialError("unsafe_storage");
|
|
1359
|
+
const path = resolve(configPath);
|
|
1360
|
+
return {
|
|
1361
|
+
reference,
|
|
1362
|
+
profile,
|
|
1363
|
+
configPath: path,
|
|
1364
|
+
directory: join4(dirname2(path), "oauth-credentials"),
|
|
1365
|
+
baseUrl: normalizeCoreOrigin(baseUrl)
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
var OAuthProfileProvider = class {
|
|
1369
|
+
selected;
|
|
1370
|
+
constructor(selected) {
|
|
1371
|
+
this.selected = selected;
|
|
1372
|
+
}
|
|
1373
|
+
async resolve() {
|
|
1374
|
+
const selected = this.selected;
|
|
1375
|
+
let raw;
|
|
1376
|
+
try {
|
|
1377
|
+
raw = JSON.parse(readFileSync3(selected.configPath, "utf8"));
|
|
1378
|
+
} catch {
|
|
1379
|
+
throw new OAuthCredentialError("login_required");
|
|
1380
|
+
}
|
|
1381
|
+
const profile = raw?.version === 2 ? raw.agents?.[selected.profile] : void 0;
|
|
1382
|
+
if (!profile || !Object.hasOwn(profile, "oauth_credential_ref")) throw new OAuthCredentialError("login_required");
|
|
1383
|
+
const current = oauthProfile(profile.oauth_credential_ref, selected.configPath, selected.profile, profile.base_url);
|
|
1384
|
+
if (!current || current.baseUrl !== selected.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1385
|
+
const store = new OAuthSessionStore(current.directory);
|
|
1386
|
+
const status = store.status(current.reference, current.profile);
|
|
1387
|
+
if (!("baseUrl" in status) || typeof status.baseUrl !== "string" || normalizeCoreOrigin(status.baseUrl) !== current.baseUrl) throw new OAuthCredentialError("login_required");
|
|
1388
|
+
return store.resolve(current.reference, current.profile, new CoreDeviceClient(current.baseUrl));
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
|
|
834
1392
|
// src/distribution-capabilities.ts
|
|
835
1393
|
var PUBLIC_DISTRIBUTION = {
|
|
836
1394
|
id: "public",
|
|
@@ -838,7 +1396,8 @@ var PUBLIC_DISTRIBUTION = {
|
|
|
838
1396
|
packageRegistry: null,
|
|
839
1397
|
capabilities: {
|
|
840
1398
|
interactiveLogin: false,
|
|
841
|
-
managedCredentials: false
|
|
1399
|
+
managedCredentials: false,
|
|
1400
|
+
deviceFlow: true
|
|
842
1401
|
}
|
|
843
1402
|
};
|
|
844
1403
|
var DISTRIBUTION_MANIFEST = typeof define_CTXDB_DISTRIBUTION_MANIFEST_default === "undefined" ? PUBLIC_DISTRIBUTION : define_CTXDB_DISTRIBUTION_MANIFEST_default;
|
|
@@ -852,7 +1411,7 @@ var DEFAULT_KNOWLEDGE_TOP_K = 6;
|
|
|
852
1411
|
var DEFAULT_KB_CATALOG_INJECTION = "session_start";
|
|
853
1412
|
function defaultPath(env) {
|
|
854
1413
|
if (env.CTXDB_CONFIG_PATH) return env.CTXDB_CONFIG_PATH;
|
|
855
|
-
return
|
|
1414
|
+
return join5(homedir2(), ".ctxdb", "ctxdb.json");
|
|
856
1415
|
}
|
|
857
1416
|
function coerceInt(v, fallback) {
|
|
858
1417
|
if (v === null || v === void 0 || v === "") return fallback;
|
|
@@ -874,9 +1433,9 @@ function coerceKbCatalogInjection(v) {
|
|
|
874
1433
|
return DEFAULT_KB_CATALOG_INJECTION;
|
|
875
1434
|
}
|
|
876
1435
|
function readRaw(path) {
|
|
877
|
-
if (!
|
|
1436
|
+
if (!existsSync3(path)) return {};
|
|
878
1437
|
try {
|
|
879
|
-
const parsed = JSON.parse(
|
|
1438
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
880
1439
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
881
1440
|
return parsed;
|
|
882
1441
|
}
|
|
@@ -892,6 +1451,9 @@ function agentRaw(raw) {
|
|
|
892
1451
|
if (!section || typeof section !== "object" || Array.isArray(section)) return {};
|
|
893
1452
|
return section;
|
|
894
1453
|
}
|
|
1454
|
+
function accessTokenFromEnv(env = process.env) {
|
|
1455
|
+
return env.CTXDB_ACCESS_TOKEN?.trim() || null;
|
|
1456
|
+
}
|
|
895
1457
|
function applyEnv(cfg, env) {
|
|
896
1458
|
if (env.CTXDB_API_KEY) cfg.apiKey = env.CTXDB_API_KEY;
|
|
897
1459
|
if (env.CTXDB_BASE_URL) cfg.baseUrl = env.CTXDB_BASE_URL.replace(/\/+$/, "");
|
|
@@ -909,11 +1471,11 @@ function applyDebugPolicy(cfg) {
|
|
|
909
1471
|
return cfg;
|
|
910
1472
|
}
|
|
911
1473
|
function managedCredential(path) {
|
|
912
|
-
if (!
|
|
1474
|
+
if (!existsSync3(path)) return null;
|
|
913
1475
|
if (process.platform !== "win32" && (statSync2(path).mode & 63) !== 0) {
|
|
914
1476
|
throw new Error(`credentials: ${path} must be owner-only (run chmod 600)`);
|
|
915
1477
|
}
|
|
916
|
-
const raw = JSON.parse(
|
|
1478
|
+
const raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
917
1479
|
if (raw.version !== 1 || !raw.records || typeof raw.records !== "object") {
|
|
918
1480
|
throw new Error("credentials: unsupported credentials.json schema");
|
|
919
1481
|
}
|
|
@@ -968,16 +1530,21 @@ function loadOpencodeConfig(options = {}) {
|
|
|
968
1530
|
kbCatalogInjection: coerceKbCatalogInjection(section.kb_catalog_injection)
|
|
969
1531
|
};
|
|
970
1532
|
const managed = DISTRIBUTION_MANIFEST.capabilities.managedCredentials && options.managedCredentials !== false ? managedCredential(
|
|
971
|
-
options.credentialsPath ??
|
|
1533
|
+
options.credentialsPath ?? join5(dirname3(path), "credentials.json")
|
|
972
1534
|
) : null;
|
|
973
1535
|
if (managed) {
|
|
974
1536
|
cfg.apiKey = managed.apiKey;
|
|
975
1537
|
cfg.baseUrl = managed.baseUrl;
|
|
976
1538
|
}
|
|
977
|
-
|
|
1539
|
+
applyEnv(cfg, env);
|
|
1540
|
+
if (DISTRIBUTION_MANIFEST.capabilities.deviceFlow) {
|
|
1541
|
+
cfg.oauthCredential = oauthProfile(section.oauth_credential_ref, path, "opencode", cfg.baseUrl);
|
|
1542
|
+
}
|
|
1543
|
+
if (env.CTXDB_API_KEY || accessTokenFromEnv(env)) cfg.oauthCredential = void 0;
|
|
1544
|
+
return cfg;
|
|
978
1545
|
}
|
|
979
|
-
function isConfigured(cfg) {
|
|
980
|
-
return Boolean(cfg.apiKey && cfg.baseUrl);
|
|
1546
|
+
function isConfigured(cfg, env = process.env) {
|
|
1547
|
+
return Boolean((accessTokenFromEnv(env) || cfg.oauthCredential || cfg.apiKey) && cfg.baseUrl);
|
|
981
1548
|
}
|
|
982
1549
|
|
|
983
1550
|
// src/http-client.ts
|
|
@@ -995,20 +1562,29 @@ var CtxdbHttpError = class extends Error {
|
|
|
995
1562
|
var HttpClient = class {
|
|
996
1563
|
baseUrl;
|
|
997
1564
|
apiKey;
|
|
1565
|
+
accessToken;
|
|
998
1566
|
userAgent;
|
|
999
1567
|
fetchImpl;
|
|
1568
|
+
oauthProvider;
|
|
1000
1569
|
constructor(opts) {
|
|
1001
1570
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
1002
1571
|
this.apiKey = opts.apiKey;
|
|
1572
|
+
this.accessToken = opts.accessToken ?? null;
|
|
1573
|
+
this.oauthProvider = opts.oauthCredential ? new OAuthProfileProvider(opts.oauthCredential) : void 0;
|
|
1003
1574
|
this.userAgent = opts.userAgent ?? "ctxdb-opencode-plugin/0.0.0";
|
|
1004
1575
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
1005
1576
|
}
|
|
1006
|
-
headers(contentType) {
|
|
1577
|
+
async headers(contentType) {
|
|
1007
1578
|
const h = {
|
|
1008
1579
|
"User-Agent": this.userAgent,
|
|
1009
1580
|
Connection: "close"
|
|
1010
1581
|
};
|
|
1011
|
-
if (this.
|
|
1582
|
+
if (this.accessToken) h.Authorization = `Bearer ${this.accessToken}`;
|
|
1583
|
+
else if (this.oauthProvider) {
|
|
1584
|
+
const authorization2 = await this.oauthProvider.resolve();
|
|
1585
|
+
if (authorization2.baseUrl !== this.baseUrl) throw new Error("OAuth environment changed; reload this runtime.");
|
|
1586
|
+
h.Authorization = `Bearer ${authorization2.value}`;
|
|
1587
|
+
} else if (this.apiKey) h.Authorization = `Token ${this.apiKey}`;
|
|
1012
1588
|
if (contentType) h["Content-Type"] = contentType;
|
|
1013
1589
|
return h;
|
|
1014
1590
|
}
|
|
@@ -1029,6 +1605,9 @@ var HttpClient = class {
|
|
|
1029
1605
|
return this.request("POST", url, path, JSON.stringify(body), "application/json", options.timeoutMs);
|
|
1030
1606
|
}
|
|
1031
1607
|
async request(method, url, path, body, contentType, timeoutMs) {
|
|
1608
|
+
const headers = await this.headers(contentType);
|
|
1609
|
+
const bearer = headers.Authorization?.startsWith("Bearer ") ? headers.Authorization.slice(7) : void 0;
|
|
1610
|
+
const safeText = (value) => bearer ? String(value).split(bearer).join("[REDACTED]") : String(value);
|
|
1032
1611
|
const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1033
1612
|
const controller = new AbortController();
|
|
1034
1613
|
const timer = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
@@ -1037,41 +1616,42 @@ var HttpClient = class {
|
|
|
1037
1616
|
try {
|
|
1038
1617
|
resp = await this.fetchImpl(url, {
|
|
1039
1618
|
method,
|
|
1040
|
-
headers
|
|
1619
|
+
headers,
|
|
1041
1620
|
body,
|
|
1042
|
-
signal: controller.signal
|
|
1621
|
+
signal: controller.signal,
|
|
1622
|
+
...headers.Authorization?.startsWith("Bearer ") ? { redirect: "error" } : {}
|
|
1043
1623
|
});
|
|
1044
1624
|
} catch (err) {
|
|
1045
1625
|
if (err?.name === "AbortError") {
|
|
1046
1626
|
throw new CtxdbHttpError(path, null, `timeout after ${effectiveTimeout}ms`);
|
|
1047
1627
|
}
|
|
1048
|
-
throw new CtxdbHttpError(path, null, `network error: ${err?.message ?? err}`);
|
|
1628
|
+
throw new CtxdbHttpError(path, null, `network error: ${safeText(err?.message ?? err)}`);
|
|
1049
1629
|
}
|
|
1050
1630
|
if (resp.status === 204) return {};
|
|
1051
|
-
let
|
|
1631
|
+
let text2;
|
|
1052
1632
|
try {
|
|
1053
|
-
|
|
1633
|
+
text2 = safeText(await resp.text());
|
|
1054
1634
|
} catch (err) {
|
|
1055
|
-
throw new CtxdbHttpError(path, resp.status, `body read failed: ${err?.message ?? err}`);
|
|
1635
|
+
throw new CtxdbHttpError(path, resp.status, `body read failed: ${safeText(err?.message ?? err)}`);
|
|
1056
1636
|
}
|
|
1057
1637
|
if (!resp.ok) {
|
|
1058
|
-
throw new CtxdbHttpError(path, resp.status, extractDetail(
|
|
1638
|
+
throw new CtxdbHttpError(path, resp.status, extractDetail(text2) || `HTTP ${resp.status}`);
|
|
1059
1639
|
}
|
|
1060
|
-
if (!
|
|
1640
|
+
if (!text2) return {};
|
|
1061
1641
|
try {
|
|
1062
|
-
return JSON.parse(
|
|
1642
|
+
return JSON.parse(text2);
|
|
1063
1643
|
} catch {
|
|
1064
|
-
return
|
|
1644
|
+
return text2;
|
|
1065
1645
|
}
|
|
1066
1646
|
} finally {
|
|
1067
1647
|
clearTimeout(timer);
|
|
1068
1648
|
}
|
|
1069
1649
|
}
|
|
1070
1650
|
};
|
|
1071
|
-
function extractDetail(
|
|
1072
|
-
if (!
|
|
1651
|
+
function extractDetail(text2) {
|
|
1652
|
+
if (!text2) return "";
|
|
1073
1653
|
try {
|
|
1074
|
-
const parsed = JSON.parse(
|
|
1654
|
+
const parsed = JSON.parse(text2);
|
|
1075
1655
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1076
1656
|
for (const k of ["detail", "message", "error"]) {
|
|
1077
1657
|
const v = parsed[k];
|
|
@@ -1081,7 +1661,7 @@ function extractDetail(text) {
|
|
|
1081
1661
|
}
|
|
1082
1662
|
return String(parsed);
|
|
1083
1663
|
} catch {
|
|
1084
|
-
return
|
|
1664
|
+
return text2;
|
|
1085
1665
|
}
|
|
1086
1666
|
}
|
|
1087
1667
|
|
|
@@ -1125,7 +1705,11 @@ async function searchAndFormatRecall(prompt, cfg, client, timeoutMs, traceOption
|
|
|
1125
1705
|
},
|
|
1126
1706
|
store: {
|
|
1127
1707
|
...traceOptions.traceStore,
|
|
1128
|
-
secrets: [
|
|
1708
|
+
secrets: [
|
|
1709
|
+
...traceOptions.traceStore?.secrets ?? [],
|
|
1710
|
+
cfg.apiKey,
|
|
1711
|
+
client.accessToken
|
|
1712
|
+
]
|
|
1129
1713
|
}
|
|
1130
1714
|
} : null;
|
|
1131
1715
|
if (trace) {
|
|
@@ -1454,10 +2038,15 @@ var SESSION_TTL_MS = 15 * 60 * 1e3;
|
|
|
1454
2038
|
var SESSION_MAX = 100;
|
|
1455
2039
|
var RECALL_TIMEOUT_MS = 5e3;
|
|
1456
2040
|
var CAPTURE_TIMEOUT_MS = 8e3;
|
|
1457
|
-
function buildRuntime(config, cwd) {
|
|
2041
|
+
function buildRuntime(config, cwd, env = process.env) {
|
|
1458
2042
|
return {
|
|
1459
2043
|
config,
|
|
1460
|
-
http: new HttpClient({
|
|
2044
|
+
http: new HttpClient({
|
|
2045
|
+
baseUrl: config.baseUrl,
|
|
2046
|
+
apiKey: config.apiKey,
|
|
2047
|
+
oauthCredential: config.oauthCredential,
|
|
2048
|
+
accessToken: accessTokenFromEnv(env)
|
|
2049
|
+
}),
|
|
1461
2050
|
sessionState: /* @__PURE__ */ new Map(),
|
|
1462
2051
|
cwd
|
|
1463
2052
|
};
|
|
@@ -1500,7 +2089,7 @@ async function buildHooks(input) {
|
|
|
1500
2089
|
logDebug(
|
|
1501
2090
|
config,
|
|
1502
2091
|
"config",
|
|
1503
|
-
"opencode integration disabled: missing agents.opencode.api_key
|
|
2092
|
+
"opencode integration disabled: missing CTXDB_ACCESS_TOKEN or API key in agents.opencode.api_key / CTXDB_API_KEY"
|
|
1504
2093
|
);
|
|
1505
2094
|
return {};
|
|
1506
2095
|
}
|
|
@@ -1509,10 +2098,10 @@ async function buildHooks(input) {
|
|
|
1509
2098
|
const hooks = {
|
|
1510
2099
|
"chat.message": async (input2, output) => {
|
|
1511
2100
|
try {
|
|
1512
|
-
const
|
|
1513
|
-
if (!
|
|
2101
|
+
const text2 = extractTextPrompt(output.parts);
|
|
2102
|
+
if (!text2) return;
|
|
1514
2103
|
const s = touchSession(rt, input2.sessionID);
|
|
1515
|
-
s.lastPrompt =
|
|
2104
|
+
s.lastPrompt = text2;
|
|
1516
2105
|
} catch (err) {
|
|
1517
2106
|
logError(rt.config, "chat.message", err);
|
|
1518
2107
|
}
|