@askexenow/exe-os 0.9.30 → 0.9.32
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/dist/bin/backfill-conversations.js +135 -7
- package/dist/bin/backfill-responses.js +135 -7
- package/dist/bin/backfill-vectors.js +135 -7
- package/dist/bin/cleanup-stale-review-tasks.js +139 -11
- package/dist/bin/cli.js +812 -486
- package/dist/bin/exe-assign.js +135 -7
- package/dist/bin/exe-boot.js +422 -113
- package/dist/bin/exe-cloud.js +160 -9
- package/dist/bin/exe-dispatch.js +136 -8
- package/dist/bin/exe-doctor.js +255 -13
- package/dist/bin/exe-export-behaviors.js +136 -8
- package/dist/bin/exe-forget.js +136 -8
- package/dist/bin/exe-gateway.js +171 -24
- package/dist/bin/exe-heartbeat.js +141 -13
- package/dist/bin/exe-kill.js +140 -12
- package/dist/bin/exe-launch-agent.js +143 -15
- package/dist/bin/exe-link.js +357 -48
- package/dist/bin/exe-pending-messages.js +136 -8
- package/dist/bin/exe-pending-notifications.js +136 -8
- package/dist/bin/exe-pending-reviews.js +138 -10
- package/dist/bin/exe-review.js +136 -8
- package/dist/bin/exe-search.js +155 -20
- package/dist/bin/exe-session-cleanup.js +166 -38
- package/dist/bin/exe-start-codex.js +142 -14
- package/dist/bin/exe-start-opencode.js +140 -12
- package/dist/bin/exe-status.js +148 -20
- package/dist/bin/exe-team.js +136 -8
- package/dist/bin/git-sweep.js +138 -10
- package/dist/bin/graph-backfill.js +135 -7
- package/dist/bin/graph-export.js +136 -8
- package/dist/bin/intercom-check.js +153 -25
- package/dist/bin/scan-tasks.js +138 -10
- package/dist/bin/setup.js +447 -121
- package/dist/bin/shard-migrate.js +135 -7
- package/dist/gateway/index.js +151 -23
- package/dist/hooks/bug-report-worker.js +151 -23
- package/dist/hooks/codex-stop-task-finalizer.js +145 -17
- package/dist/hooks/commit-complete.js +138 -10
- package/dist/hooks/error-recall.js +159 -24
- package/dist/hooks/ingest.js +142 -14
- package/dist/hooks/instructions-loaded.js +136 -8
- package/dist/hooks/notification.js +136 -8
- package/dist/hooks/post-compact.js +136 -8
- package/dist/hooks/post-tool-combined.js +159 -24
- package/dist/hooks/pre-compact.js +136 -8
- package/dist/hooks/pre-tool-use.js +144 -16
- package/dist/hooks/prompt-submit.js +195 -55
- package/dist/hooks/session-end.js +141 -13
- package/dist/hooks/session-start.js +165 -30
- package/dist/hooks/stop.js +136 -8
- package/dist/hooks/subagent-stop.js +136 -8
- package/dist/hooks/summary-worker.js +374 -65
- package/dist/index.js +136 -8
- package/dist/lib/cloud-sync.js +355 -46
- package/dist/lib/consolidation.js +1 -0
- package/dist/lib/exe-daemon.js +469 -127
- package/dist/lib/hybrid-search.js +155 -20
- package/dist/lib/keychain.js +191 -7
- package/dist/lib/schedules.js +138 -10
- package/dist/lib/store.js +135 -7
- package/dist/mcp/server.js +706 -213
- package/dist/runtime/index.js +136 -8
- package/dist/tui/App.js +208 -31
- package/package.json +1 -1
|
@@ -1156,8 +1156,8 @@ function findPackageRoot() {
|
|
|
1156
1156
|
function getAvailableMemoryGB() {
|
|
1157
1157
|
if (process.platform === "darwin") {
|
|
1158
1158
|
try {
|
|
1159
|
-
const { execSync:
|
|
1160
|
-
const vmstat =
|
|
1159
|
+
const { execSync: execSync6 } = __require("child_process");
|
|
1160
|
+
const vmstat = execSync6("vm_stat", { encoding: "utf8" });
|
|
1161
1161
|
const pageSize = 16384;
|
|
1162
1162
|
const pageSizeMatch = vmstat.match(/page size of (\d+) bytes/);
|
|
1163
1163
|
const actualPageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : pageSize;
|
|
@@ -2849,6 +2849,7 @@ __export(keychain_exports, {
|
|
|
2849
2849
|
});
|
|
2850
2850
|
import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
|
|
2851
2851
|
import { existsSync as existsSync6 } from "fs";
|
|
2852
|
+
import { execSync as execSync2 } from "child_process";
|
|
2852
2853
|
import path6 from "path";
|
|
2853
2854
|
import os5 from "os";
|
|
2854
2855
|
function getKeyDir() {
|
|
@@ -2857,6 +2858,83 @@ function getKeyDir() {
|
|
|
2857
2858
|
function getKeyPath() {
|
|
2858
2859
|
return path6.join(getKeyDir(), "master.key");
|
|
2859
2860
|
}
|
|
2861
|
+
function macKeychainGet() {
|
|
2862
|
+
if (process.platform !== "darwin") return null;
|
|
2863
|
+
try {
|
|
2864
|
+
return execSync2(
|
|
2865
|
+
`security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
|
|
2866
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
2867
|
+
).trim();
|
|
2868
|
+
} catch {
|
|
2869
|
+
return null;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
function macKeychainSet(value) {
|
|
2873
|
+
if (process.platform !== "darwin") return false;
|
|
2874
|
+
try {
|
|
2875
|
+
try {
|
|
2876
|
+
execSync2(
|
|
2877
|
+
`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
|
|
2878
|
+
{ timeout: 5e3 }
|
|
2879
|
+
);
|
|
2880
|
+
} catch {
|
|
2881
|
+
}
|
|
2882
|
+
execSync2(
|
|
2883
|
+
`security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
|
|
2884
|
+
{ timeout: 5e3 }
|
|
2885
|
+
);
|
|
2886
|
+
return true;
|
|
2887
|
+
} catch {
|
|
2888
|
+
return false;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
function macKeychainDelete() {
|
|
2892
|
+
if (process.platform !== "darwin") return false;
|
|
2893
|
+
try {
|
|
2894
|
+
execSync2(
|
|
2895
|
+
`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
|
|
2896
|
+
{ timeout: 5e3 }
|
|
2897
|
+
);
|
|
2898
|
+
return true;
|
|
2899
|
+
} catch {
|
|
2900
|
+
return false;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
function linuxSecretGet() {
|
|
2904
|
+
if (process.platform !== "linux") return null;
|
|
2905
|
+
try {
|
|
2906
|
+
return execSync2(
|
|
2907
|
+
`secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
2908
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
2909
|
+
).trim();
|
|
2910
|
+
} catch {
|
|
2911
|
+
return null;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function linuxSecretSet(value) {
|
|
2915
|
+
if (process.platform !== "linux") return false;
|
|
2916
|
+
try {
|
|
2917
|
+
execSync2(
|
|
2918
|
+
`echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
|
|
2919
|
+
{ timeout: 5e3 }
|
|
2920
|
+
);
|
|
2921
|
+
return true;
|
|
2922
|
+
} catch {
|
|
2923
|
+
return false;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
function linuxSecretDelete() {
|
|
2927
|
+
if (process.platform !== "linux") return false;
|
|
2928
|
+
try {
|
|
2929
|
+
execSync2(
|
|
2930
|
+
`secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
2931
|
+
{ timeout: 5e3 }
|
|
2932
|
+
);
|
|
2933
|
+
return true;
|
|
2934
|
+
} catch {
|
|
2935
|
+
return false;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2860
2938
|
async function tryKeytar() {
|
|
2861
2939
|
try {
|
|
2862
2940
|
return await import("keytar");
|
|
@@ -2864,13 +2942,72 @@ async function tryKeytar() {
|
|
|
2864
2942
|
return null;
|
|
2865
2943
|
}
|
|
2866
2944
|
}
|
|
2945
|
+
function deriveMachineKey() {
|
|
2946
|
+
try {
|
|
2947
|
+
const crypto5 = __require("crypto");
|
|
2948
|
+
const material = [
|
|
2949
|
+
os5.hostname(),
|
|
2950
|
+
os5.userInfo().username,
|
|
2951
|
+
os5.arch(),
|
|
2952
|
+
os5.platform(),
|
|
2953
|
+
// Machine ID on Linux (stable across reboots)
|
|
2954
|
+
process.platform === "linux" ? readMachineId() : ""
|
|
2955
|
+
].join("|");
|
|
2956
|
+
return crypto5.createHash("sha256").update(material).digest();
|
|
2957
|
+
} catch {
|
|
2958
|
+
return null;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
function readMachineId() {
|
|
2962
|
+
try {
|
|
2963
|
+
const { readFileSync: readFileSync14 } = __require("fs");
|
|
2964
|
+
return readFileSync14("/etc/machine-id", "utf-8").trim();
|
|
2965
|
+
} catch {
|
|
2966
|
+
return "";
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
function encryptWithMachineKey(plaintext, machineKey) {
|
|
2970
|
+
const crypto5 = __require("crypto");
|
|
2971
|
+
const iv = crypto5.randomBytes(12);
|
|
2972
|
+
const cipher = crypto5.createCipheriv("aes-256-gcm", machineKey, iv);
|
|
2973
|
+
let encrypted = cipher.update(plaintext, "utf-8", "base64");
|
|
2974
|
+
encrypted += cipher.final("base64");
|
|
2975
|
+
const authTag = cipher.getAuthTag().toString("base64");
|
|
2976
|
+
return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${authTag}:${encrypted}`;
|
|
2977
|
+
}
|
|
2978
|
+
function decryptWithMachineKey(encrypted, machineKey) {
|
|
2979
|
+
if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
|
|
2980
|
+
try {
|
|
2981
|
+
const crypto5 = __require("crypto");
|
|
2982
|
+
const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
|
|
2983
|
+
if (parts.length !== 3) return null;
|
|
2984
|
+
const [ivB64, tagB64, cipherB64] = parts;
|
|
2985
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
2986
|
+
const authTag = Buffer.from(tagB64, "base64");
|
|
2987
|
+
const decipher = crypto5.createDecipheriv("aes-256-gcm", machineKey, iv);
|
|
2988
|
+
decipher.setAuthTag(authTag);
|
|
2989
|
+
let decrypted = decipher.update(cipherB64, "base64", "utf-8");
|
|
2990
|
+
decrypted += decipher.final("utf-8");
|
|
2991
|
+
return decrypted;
|
|
2992
|
+
} catch {
|
|
2993
|
+
return null;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2867
2996
|
async function getMasterKey() {
|
|
2997
|
+
const nativeValue = macKeychainGet() ?? linuxSecretGet();
|
|
2998
|
+
if (nativeValue) {
|
|
2999
|
+
return Buffer.from(nativeValue, "base64");
|
|
3000
|
+
}
|
|
2868
3001
|
const keytar = await tryKeytar();
|
|
2869
3002
|
if (keytar) {
|
|
2870
3003
|
try {
|
|
2871
|
-
const
|
|
2872
|
-
if (
|
|
2873
|
-
|
|
3004
|
+
const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
3005
|
+
if (keytarValue) {
|
|
3006
|
+
const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
|
|
3007
|
+
if (migrated) {
|
|
3008
|
+
process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
|
|
3009
|
+
}
|
|
3010
|
+
return Buffer.from(keytarValue, "base64");
|
|
2874
3011
|
}
|
|
2875
3012
|
} catch {
|
|
2876
3013
|
}
|
|
@@ -2884,8 +3021,31 @@ async function getMasterKey() {
|
|
|
2884
3021
|
return null;
|
|
2885
3022
|
}
|
|
2886
3023
|
try {
|
|
2887
|
-
const content = await readFile3(keyPath, "utf-8");
|
|
2888
|
-
|
|
3024
|
+
const content = (await readFile3(keyPath, "utf-8")).trim();
|
|
3025
|
+
let b64Value;
|
|
3026
|
+
if (content.startsWith(ENCRYPTED_PREFIX)) {
|
|
3027
|
+
const machineKey = deriveMachineKey();
|
|
3028
|
+
if (!machineKey) {
|
|
3029
|
+
process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
|
|
3030
|
+
return null;
|
|
3031
|
+
}
|
|
3032
|
+
const decrypted = decryptWithMachineKey(content, machineKey);
|
|
3033
|
+
if (!decrypted) {
|
|
3034
|
+
process.stderr.write(
|
|
3035
|
+
"[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
|
|
3036
|
+
);
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
3039
|
+
b64Value = decrypted;
|
|
3040
|
+
} else {
|
|
3041
|
+
b64Value = content;
|
|
3042
|
+
}
|
|
3043
|
+
const key = Buffer.from(b64Value, "base64");
|
|
3044
|
+
const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
|
|
3045
|
+
if (migrated) {
|
|
3046
|
+
process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
|
|
3047
|
+
}
|
|
3048
|
+
return key;
|
|
2889
3049
|
} catch (err) {
|
|
2890
3050
|
process.stderr.write(
|
|
2891
3051
|
`[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -2896,6 +3056,9 @@ async function getMasterKey() {
|
|
|
2896
3056
|
}
|
|
2897
3057
|
async function setMasterKey(key) {
|
|
2898
3058
|
const b64 = key.toString("base64");
|
|
3059
|
+
if (macKeychainSet(b64) || linuxSecretSet(b64)) {
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
2899
3062
|
const keytar = await tryKeytar();
|
|
2900
3063
|
if (keytar) {
|
|
2901
3064
|
try {
|
|
@@ -2907,10 +3070,23 @@ async function setMasterKey(key) {
|
|
|
2907
3070
|
const dir = getKeyDir();
|
|
2908
3071
|
await mkdir3(dir, { recursive: true });
|
|
2909
3072
|
const keyPath = getKeyPath();
|
|
2910
|
-
|
|
2911
|
-
|
|
3073
|
+
const machineKey = deriveMachineKey();
|
|
3074
|
+
if (machineKey) {
|
|
3075
|
+
const encrypted = encryptWithMachineKey(b64, machineKey);
|
|
3076
|
+
await writeFile3(keyPath, encrypted + "\n", "utf-8");
|
|
3077
|
+
await chmod2(keyPath, 384);
|
|
3078
|
+
process.stderr.write("[keychain] Key stored encrypted (machine-bound).\n");
|
|
3079
|
+
} else {
|
|
3080
|
+
await writeFile3(keyPath, b64 + "\n", "utf-8");
|
|
3081
|
+
await chmod2(keyPath, 384);
|
|
3082
|
+
process.stderr.write(
|
|
3083
|
+
"[keychain] WARNING: Key stored in plaintext file \u2014 no OS keychain available.\n"
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
2912
3086
|
}
|
|
2913
3087
|
async function deleteMasterKey() {
|
|
3088
|
+
macKeychainDelete();
|
|
3089
|
+
linuxSecretDelete();
|
|
2914
3090
|
const keytar = await tryKeytar();
|
|
2915
3091
|
if (keytar) {
|
|
2916
3092
|
try {
|
|
@@ -2952,12 +3128,13 @@ async function importMnemonic(mnemonic) {
|
|
|
2952
3128
|
const entropy = mnemonicToEntropy(trimmed);
|
|
2953
3129
|
return Buffer.from(entropy, "hex");
|
|
2954
3130
|
}
|
|
2955
|
-
var SERVICE, ACCOUNT;
|
|
3131
|
+
var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
|
|
2956
3132
|
var init_keychain = __esm({
|
|
2957
3133
|
"src/lib/keychain.ts"() {
|
|
2958
3134
|
"use strict";
|
|
2959
3135
|
SERVICE = "exe-mem";
|
|
2960
3136
|
ACCOUNT = "master-key";
|
|
3137
|
+
ENCRYPTED_PREFIX = "enc:";
|
|
2961
3138
|
}
|
|
2962
3139
|
});
|
|
2963
3140
|
|
|
@@ -3599,7 +3776,7 @@ var init_session_registry = __esm({
|
|
|
3599
3776
|
});
|
|
3600
3777
|
|
|
3601
3778
|
// src/lib/session-key.ts
|
|
3602
|
-
import { execSync as
|
|
3779
|
+
import { execSync as execSync3 } from "child_process";
|
|
3603
3780
|
function normalizeCommand(command) {
|
|
3604
3781
|
const trimmed = command.trim().toLowerCase();
|
|
3605
3782
|
const parts = trimmed.split(/[\\/]/);
|
|
@@ -3618,7 +3795,7 @@ function resolveRuntimeProcess() {
|
|
|
3618
3795
|
let pid = process.ppid;
|
|
3619
3796
|
for (let i = 0; i < 10; i++) {
|
|
3620
3797
|
try {
|
|
3621
|
-
const info =
|
|
3798
|
+
const info = execSync3(`ps -p ${pid} -o ppid=,comm=`, {
|
|
3622
3799
|
encoding: "utf8",
|
|
3623
3800
|
timeout: 2e3
|
|
3624
3801
|
}).trim();
|
|
@@ -3784,7 +3961,7 @@ var init_transport = __esm({
|
|
|
3784
3961
|
});
|
|
3785
3962
|
|
|
3786
3963
|
// src/lib/cc-agent-support.ts
|
|
3787
|
-
import { execSync as
|
|
3964
|
+
import { execSync as execSync4 } from "child_process";
|
|
3788
3965
|
var init_cc_agent_support = __esm({
|
|
3789
3966
|
"src/lib/cc-agent-support.ts"() {
|
|
3790
3967
|
"use strict";
|
|
@@ -4132,8 +4309,8 @@ async function validateLicense(apiKey, deviceId) {
|
|
|
4132
4309
|
}
|
|
4133
4310
|
function getCacheAgeMs() {
|
|
4134
4311
|
try {
|
|
4135
|
-
const { statSync:
|
|
4136
|
-
const s =
|
|
4312
|
+
const { statSync: statSync4 } = __require("fs");
|
|
4313
|
+
const s = statSync4(CACHE_PATH);
|
|
4137
4314
|
return Date.now() - s.mtimeMs;
|
|
4138
4315
|
} catch {
|
|
4139
4316
|
return Infinity;
|
|
@@ -4635,10 +4812,10 @@ async function disposeEmbedder() {
|
|
|
4635
4812
|
async function embedDirect(text) {
|
|
4636
4813
|
const llamaCpp = await import("node-llama-cpp");
|
|
4637
4814
|
const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
4638
|
-
const { existsSync:
|
|
4639
|
-
const
|
|
4640
|
-
const modelPath =
|
|
4641
|
-
if (!
|
|
4815
|
+
const { existsSync: existsSync20 } = await import("fs");
|
|
4816
|
+
const path21 = await import("path");
|
|
4817
|
+
const modelPath = path21.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
|
|
4818
|
+
if (!existsSync20(modelPath)) {
|
|
4642
4819
|
throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
|
|
4643
4820
|
}
|
|
4644
4821
|
const llama = await llamaCpp.getLlama();
|
|
@@ -5081,6 +5258,109 @@ var init_crdt_sync = __esm({
|
|
|
5081
5258
|
}
|
|
5082
5259
|
});
|
|
5083
5260
|
|
|
5261
|
+
// src/lib/db-backup.ts
|
|
5262
|
+
var db_backup_exports = {};
|
|
5263
|
+
__export(db_backup_exports, {
|
|
5264
|
+
createBackup: () => createBackup,
|
|
5265
|
+
findActiveDb: () => findActiveDb,
|
|
5266
|
+
getBackupDir: () => getBackupDir,
|
|
5267
|
+
getLatestBackup: () => getLatestBackup,
|
|
5268
|
+
hasBackupToday: () => hasBackupToday,
|
|
5269
|
+
listBackups: () => listBackups,
|
|
5270
|
+
rotateBackups: () => rotateBackups
|
|
5271
|
+
});
|
|
5272
|
+
import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync5, unlinkSync as unlinkSync7, statSync as statSync3 } from "fs";
|
|
5273
|
+
import path18 from "path";
|
|
5274
|
+
function findActiveDb() {
|
|
5275
|
+
for (const name of DB_NAMES) {
|
|
5276
|
+
const p = path18.join(EXE_AI_DIR, name);
|
|
5277
|
+
if (existsSync17(p)) return p;
|
|
5278
|
+
}
|
|
5279
|
+
return null;
|
|
5280
|
+
}
|
|
5281
|
+
function createBackup(reason = "manual") {
|
|
5282
|
+
const dbPath = findActiveDb();
|
|
5283
|
+
if (!dbPath) return null;
|
|
5284
|
+
mkdirSync8(BACKUP_DIR, { recursive: true });
|
|
5285
|
+
const dbName = path18.basename(dbPath, ".db");
|
|
5286
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
5287
|
+
const backupName = `${dbName}-${reason}-${timestamp}.db`;
|
|
5288
|
+
const backupPath = path18.join(BACKUP_DIR, backupName);
|
|
5289
|
+
copyFileSync(dbPath, backupPath);
|
|
5290
|
+
const walPath = dbPath + "-wal";
|
|
5291
|
+
if (existsSync17(walPath)) {
|
|
5292
|
+
try {
|
|
5293
|
+
copyFileSync(walPath, backupPath + "-wal");
|
|
5294
|
+
} catch {
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
const shmPath = dbPath + "-shm";
|
|
5298
|
+
if (existsSync17(shmPath)) {
|
|
5299
|
+
try {
|
|
5300
|
+
copyFileSync(shmPath, backupPath + "-shm");
|
|
5301
|
+
} catch {
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
return backupPath;
|
|
5305
|
+
}
|
|
5306
|
+
function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
|
|
5307
|
+
if (!existsSync17(BACKUP_DIR)) return 0;
|
|
5308
|
+
const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1e3;
|
|
5309
|
+
let deleted = 0;
|
|
5310
|
+
try {
|
|
5311
|
+
const files = readdirSync5(BACKUP_DIR);
|
|
5312
|
+
for (const file of files) {
|
|
5313
|
+
if (!file.endsWith(".db") && !file.endsWith(".db-wal") && !file.endsWith(".db-shm")) continue;
|
|
5314
|
+
const filePath = path18.join(BACKUP_DIR, file);
|
|
5315
|
+
try {
|
|
5316
|
+
const stat = statSync3(filePath);
|
|
5317
|
+
if (stat.mtimeMs < cutoff) {
|
|
5318
|
+
unlinkSync7(filePath);
|
|
5319
|
+
deleted++;
|
|
5320
|
+
}
|
|
5321
|
+
} catch {
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
} catch {
|
|
5325
|
+
}
|
|
5326
|
+
return deleted;
|
|
5327
|
+
}
|
|
5328
|
+
function listBackups() {
|
|
5329
|
+
if (!existsSync17(BACKUP_DIR)) return [];
|
|
5330
|
+
try {
|
|
5331
|
+
const files = readdirSync5(BACKUP_DIR).filter((f) => f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm"));
|
|
5332
|
+
return files.map((name) => {
|
|
5333
|
+
const p = path18.join(BACKUP_DIR, name);
|
|
5334
|
+
const stat = statSync3(p);
|
|
5335
|
+
return { path: p, name, size: stat.size, date: stat.mtime };
|
|
5336
|
+
}).sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
5337
|
+
} catch {
|
|
5338
|
+
return [];
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
function hasBackupToday(reason) {
|
|
5342
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5343
|
+
const backups = listBackups();
|
|
5344
|
+
return backups.some((b) => b.name.includes(reason) && b.name.includes(today.replace(/-/g, "-")));
|
|
5345
|
+
}
|
|
5346
|
+
function getLatestBackup() {
|
|
5347
|
+
const backups = listBackups();
|
|
5348
|
+
return backups.length > 0 ? backups[0].path : null;
|
|
5349
|
+
}
|
|
5350
|
+
function getBackupDir() {
|
|
5351
|
+
return BACKUP_DIR;
|
|
5352
|
+
}
|
|
5353
|
+
var BACKUP_DIR, DEFAULT_KEEP_DAYS, DB_NAMES;
|
|
5354
|
+
var init_db_backup = __esm({
|
|
5355
|
+
"src/lib/db-backup.ts"() {
|
|
5356
|
+
"use strict";
|
|
5357
|
+
init_config();
|
|
5358
|
+
BACKUP_DIR = path18.join(EXE_AI_DIR, "backups");
|
|
5359
|
+
DEFAULT_KEEP_DAYS = 3;
|
|
5360
|
+
DB_NAMES = ["memories.db", "exe-mem.db", "exe-os.db", "exe.db"];
|
|
5361
|
+
}
|
|
5362
|
+
});
|
|
5363
|
+
|
|
5084
5364
|
// src/lib/cloud-sync.ts
|
|
5085
5365
|
var cloud_sync_exports = {};
|
|
5086
5366
|
__export(cloud_sync_exports, {
|
|
@@ -5110,16 +5390,16 @@ __export(cloud_sync_exports, {
|
|
|
5110
5390
|
pushToPostgres: () => pushToPostgres,
|
|
5111
5391
|
recordRosterDeletion: () => recordRosterDeletion
|
|
5112
5392
|
});
|
|
5113
|
-
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as
|
|
5393
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync18, readdirSync as readdirSync6, mkdirSync as mkdirSync9, appendFileSync as appendFileSync3, unlinkSync as unlinkSync8, openSync as openSync2, closeSync as closeSync2 } from "fs";
|
|
5114
5394
|
import crypto4 from "crypto";
|
|
5115
|
-
import
|
|
5395
|
+
import path19 from "path";
|
|
5116
5396
|
import { homedir as homedir2 } from "os";
|
|
5117
5397
|
function sqlSafe(v) {
|
|
5118
5398
|
return v === void 0 ? null : v;
|
|
5119
5399
|
}
|
|
5120
5400
|
function logError(msg) {
|
|
5121
5401
|
try {
|
|
5122
|
-
const logPath =
|
|
5402
|
+
const logPath = path19.join(homedir2(), ".exe-os", "workers.log");
|
|
5123
5403
|
appendFileSync3(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
5124
5404
|
`);
|
|
5125
5405
|
} catch {
|
|
@@ -5128,10 +5408,10 @@ function logError(msg) {
|
|
|
5128
5408
|
function loadPgClient() {
|
|
5129
5409
|
if (_pgFailed) return null;
|
|
5130
5410
|
const postgresUrl = process.env.DATABASE_URL;
|
|
5131
|
-
const configPath =
|
|
5411
|
+
const configPath = path19.join(EXE_AI_DIR, "config.json");
|
|
5132
5412
|
let cloudPostgresUrl;
|
|
5133
5413
|
try {
|
|
5134
|
-
if (
|
|
5414
|
+
if (existsSync18(configPath)) {
|
|
5135
5415
|
const cfg = JSON.parse(readFileSync13(configPath, "utf8"));
|
|
5136
5416
|
cloudPostgresUrl = cfg.cloud?.postgresUrl;
|
|
5137
5417
|
if (cfg.cloud?.syncToPostgres === false) {
|
|
@@ -5150,8 +5430,8 @@ function loadPgClient() {
|
|
|
5150
5430
|
_pgPromise = (async () => {
|
|
5151
5431
|
const { createRequire: createRequire3 } = await import("module");
|
|
5152
5432
|
const { pathToFileURL: pathToFileURL3 } = await import("url");
|
|
5153
|
-
const exeDbRoot = process.env.EXE_DB_ROOT ??
|
|
5154
|
-
const req = createRequire3(
|
|
5433
|
+
const exeDbRoot = process.env.EXE_DB_ROOT ?? path19.join(homedir2(), "exe-db");
|
|
5434
|
+
const req = createRequire3(path19.join(exeDbRoot, "package.json"));
|
|
5155
5435
|
const entry = req.resolve("@prisma/client");
|
|
5156
5436
|
const mod = await import(pathToFileURL3(entry).href);
|
|
5157
5437
|
const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
|
|
@@ -5204,7 +5484,7 @@ async function withRosterLock(fn) {
|
|
|
5204
5484
|
if (Date.now() - ts < LOCK_STALE_MS) {
|
|
5205
5485
|
throw new Error("Roster merge already in progress \u2014 another sync is running");
|
|
5206
5486
|
}
|
|
5207
|
-
|
|
5487
|
+
unlinkSync8(ROSTER_LOCK_PATH);
|
|
5208
5488
|
const fd = openSync2(ROSTER_LOCK_PATH, "wx");
|
|
5209
5489
|
closeSync2(fd);
|
|
5210
5490
|
writeFileSync9(ROSTER_LOCK_PATH, String(Date.now()));
|
|
@@ -5220,7 +5500,7 @@ async function withRosterLock(fn) {
|
|
|
5220
5500
|
return await fn();
|
|
5221
5501
|
} finally {
|
|
5222
5502
|
try {
|
|
5223
|
-
|
|
5503
|
+
unlinkSync8(ROSTER_LOCK_PATH);
|
|
5224
5504
|
} catch {
|
|
5225
5505
|
}
|
|
5226
5506
|
}
|
|
@@ -5591,13 +5871,42 @@ async function cloudSync(config) {
|
|
|
5591
5871
|
try {
|
|
5592
5872
|
const employees = await loadEmployees();
|
|
5593
5873
|
rosterResult.employees = employees.length;
|
|
5594
|
-
const idDir =
|
|
5595
|
-
if (
|
|
5596
|
-
rosterResult.identities =
|
|
5874
|
+
const idDir = path19.join(EXE_AI_DIR, "identity");
|
|
5875
|
+
if (existsSync18(idDir)) {
|
|
5876
|
+
rosterResult.identities = readdirSync6(idDir).filter((f) => f.endsWith(".md")).length;
|
|
5597
5877
|
}
|
|
5598
5878
|
} catch {
|
|
5599
5879
|
}
|
|
5600
5880
|
const totalMemories = await countRows("SELECT COUNT(*) as cnt FROM memories WHERE status = 'active' OR status IS NULL");
|
|
5881
|
+
try {
|
|
5882
|
+
const { getLatestBackup: getLatestBackup2 } = await Promise.resolve().then(() => (init_db_backup(), db_backup_exports));
|
|
5883
|
+
const { statSync: statFile } = await import("fs");
|
|
5884
|
+
const latestBackup = getLatestBackup2();
|
|
5885
|
+
if (latestBackup) {
|
|
5886
|
+
const backupSize = statFile(latestBackup).size;
|
|
5887
|
+
const MAX_CLOUD_BACKUP_BYTES = 50 * 1024 * 1024;
|
|
5888
|
+
if (backupSize <= MAX_CLOUD_BACKUP_BYTES) {
|
|
5889
|
+
const backupData = readFileSync13(latestBackup);
|
|
5890
|
+
const deviceId = loadDeviceId() ?? "unknown";
|
|
5891
|
+
const encrypted = encryptSyncBlob(backupData);
|
|
5892
|
+
const backupRes = await fetchWithRetry(`${config.endpoint}/sync/push-db-backup`, {
|
|
5893
|
+
method: "POST",
|
|
5894
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
|
|
5895
|
+
body: JSON.stringify({
|
|
5896
|
+
device_id: deviceId,
|
|
5897
|
+
filename: path19.basename(latestBackup),
|
|
5898
|
+
blob: encrypted,
|
|
5899
|
+
size: backupData.length
|
|
5900
|
+
})
|
|
5901
|
+
});
|
|
5902
|
+
if (backupRes && !backupRes.ok) {
|
|
5903
|
+
logError(`[cloud-sync] DB backup upload failed: ${backupRes.status}`);
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
} catch (err) {
|
|
5908
|
+
logError(`[cloud-sync] DB backup upload error: ${err instanceof Error ? err.message : String(err)}`);
|
|
5909
|
+
}
|
|
5601
5910
|
return {
|
|
5602
5911
|
pushed,
|
|
5603
5912
|
pulled,
|
|
@@ -5613,7 +5922,7 @@ async function cloudSync(config) {
|
|
|
5613
5922
|
function recordRosterDeletion(name) {
|
|
5614
5923
|
let deletions = [];
|
|
5615
5924
|
try {
|
|
5616
|
-
if (
|
|
5925
|
+
if (existsSync18(ROSTER_DELETIONS_PATH)) {
|
|
5617
5926
|
deletions = JSON.parse(readFileSync13(ROSTER_DELETIONS_PATH, "utf-8"));
|
|
5618
5927
|
}
|
|
5619
5928
|
} catch {
|
|
@@ -5623,7 +5932,7 @@ function recordRosterDeletion(name) {
|
|
|
5623
5932
|
}
|
|
5624
5933
|
function consumeRosterDeletions() {
|
|
5625
5934
|
try {
|
|
5626
|
-
if (!
|
|
5935
|
+
if (!existsSync18(ROSTER_DELETIONS_PATH)) return [];
|
|
5627
5936
|
const deletions = JSON.parse(readFileSync13(ROSTER_DELETIONS_PATH, "utf-8"));
|
|
5628
5937
|
writeFileSync9(ROSTER_DELETIONS_PATH, "[]");
|
|
5629
5938
|
return deletions;
|
|
@@ -5632,35 +5941,35 @@ function consumeRosterDeletions() {
|
|
|
5632
5941
|
}
|
|
5633
5942
|
}
|
|
5634
5943
|
function buildRosterBlob(paths) {
|
|
5635
|
-
const rosterPath = paths?.rosterPath ??
|
|
5636
|
-
const identityDir = paths?.identityDir ??
|
|
5637
|
-
const configPath = paths?.configPath ??
|
|
5944
|
+
const rosterPath = paths?.rosterPath ?? path19.join(EXE_AI_DIR, "exe-employees.json");
|
|
5945
|
+
const identityDir = paths?.identityDir ?? path19.join(EXE_AI_DIR, "identity");
|
|
5946
|
+
const configPath = paths?.configPath ?? path19.join(EXE_AI_DIR, "config.json");
|
|
5638
5947
|
let roster = [];
|
|
5639
|
-
if (
|
|
5948
|
+
if (existsSync18(rosterPath)) {
|
|
5640
5949
|
try {
|
|
5641
5950
|
roster = JSON.parse(readFileSync13(rosterPath, "utf-8"));
|
|
5642
5951
|
} catch {
|
|
5643
5952
|
}
|
|
5644
5953
|
}
|
|
5645
5954
|
const identities = {};
|
|
5646
|
-
if (
|
|
5647
|
-
for (const file of
|
|
5955
|
+
if (existsSync18(identityDir)) {
|
|
5956
|
+
for (const file of readdirSync6(identityDir).filter((f) => f.endsWith(".md"))) {
|
|
5648
5957
|
try {
|
|
5649
|
-
identities[file] = readFileSync13(
|
|
5958
|
+
identities[file] = readFileSync13(path19.join(identityDir, file), "utf-8");
|
|
5650
5959
|
} catch {
|
|
5651
5960
|
}
|
|
5652
5961
|
}
|
|
5653
5962
|
}
|
|
5654
5963
|
let config;
|
|
5655
|
-
if (
|
|
5964
|
+
if (existsSync18(configPath)) {
|
|
5656
5965
|
try {
|
|
5657
5966
|
config = JSON.parse(readFileSync13(configPath, "utf-8"));
|
|
5658
5967
|
} catch {
|
|
5659
5968
|
}
|
|
5660
5969
|
}
|
|
5661
5970
|
let agentConfig;
|
|
5662
|
-
const agentConfigPath =
|
|
5663
|
-
if (
|
|
5971
|
+
const agentConfigPath = path19.join(EXE_AI_DIR, "agent-config.json");
|
|
5972
|
+
if (existsSync18(agentConfigPath)) {
|
|
5664
5973
|
try {
|
|
5665
5974
|
agentConfig = JSON.parse(readFileSync13(agentConfigPath, "utf-8"));
|
|
5666
5975
|
} catch {
|
|
@@ -5738,16 +6047,16 @@ async function cloudPullRoster(config) {
|
|
|
5738
6047
|
}
|
|
5739
6048
|
}
|
|
5740
6049
|
function mergeConfig(remoteConfig, configPath) {
|
|
5741
|
-
const cfgPath = configPath ??
|
|
6050
|
+
const cfgPath = configPath ?? path19.join(EXE_AI_DIR, "config.json");
|
|
5742
6051
|
let local = {};
|
|
5743
|
-
if (
|
|
6052
|
+
if (existsSync18(cfgPath)) {
|
|
5744
6053
|
try {
|
|
5745
6054
|
local = JSON.parse(readFileSync13(cfgPath, "utf-8"));
|
|
5746
6055
|
} catch {
|
|
5747
6056
|
}
|
|
5748
6057
|
}
|
|
5749
6058
|
const merged = { ...remoteConfig, ...local };
|
|
5750
|
-
const dir =
|
|
6059
|
+
const dir = path19.dirname(cfgPath);
|
|
5751
6060
|
ensurePrivateDirSync(dir);
|
|
5752
6061
|
writeFileSync9(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
5753
6062
|
enforcePrivateFileSync(cfgPath);
|
|
@@ -5755,7 +6064,7 @@ function mergeConfig(remoteConfig, configPath) {
|
|
|
5755
6064
|
async function mergeRosterFromRemote(remote, paths) {
|
|
5756
6065
|
return withRosterLock(async () => {
|
|
5757
6066
|
const rosterPath = paths?.rosterPath ?? void 0;
|
|
5758
|
-
const identityDir = paths?.identityDir ??
|
|
6067
|
+
const identityDir = paths?.identityDir ?? path19.join(EXE_AI_DIR, "identity");
|
|
5759
6068
|
const localEmployees = await loadEmployees(rosterPath);
|
|
5760
6069
|
const localNames = new Set(localEmployees.map((e) => e.name));
|
|
5761
6070
|
let added = 0;
|
|
@@ -5776,11 +6085,11 @@ async function mergeRosterFromRemote(remote, paths) {
|
|
|
5776
6085
|
) ?? lookupKey;
|
|
5777
6086
|
const remoteIdentity = remote.identities[matchedKey];
|
|
5778
6087
|
if (remoteIdentity) {
|
|
5779
|
-
if (!
|
|
5780
|
-
const idPath =
|
|
6088
|
+
if (!existsSync18(identityDir)) mkdirSync9(identityDir, { recursive: true });
|
|
6089
|
+
const idPath = path19.join(identityDir, `${remoteEmp.name}.md`);
|
|
5781
6090
|
let localIdentity = null;
|
|
5782
6091
|
try {
|
|
5783
|
-
localIdentity =
|
|
6092
|
+
localIdentity = existsSync18(idPath) ? readFileSync13(idPath, "utf-8") : null;
|
|
5784
6093
|
} catch {
|
|
5785
6094
|
}
|
|
5786
6095
|
if (localIdentity !== remoteIdentity) {
|
|
@@ -5810,16 +6119,16 @@ async function mergeRosterFromRemote(remote, paths) {
|
|
|
5810
6119
|
}
|
|
5811
6120
|
if (remote.agentConfig && Object.keys(remote.agentConfig).length > 0) {
|
|
5812
6121
|
try {
|
|
5813
|
-
const agentConfigPath =
|
|
6122
|
+
const agentConfigPath = path19.join(EXE_AI_DIR, "agent-config.json");
|
|
5814
6123
|
let local = {};
|
|
5815
|
-
if (
|
|
6124
|
+
if (existsSync18(agentConfigPath)) {
|
|
5816
6125
|
try {
|
|
5817
6126
|
local = JSON.parse(readFileSync13(agentConfigPath, "utf-8"));
|
|
5818
6127
|
} catch {
|
|
5819
6128
|
}
|
|
5820
6129
|
}
|
|
5821
6130
|
const merged = { ...remote.agentConfig, ...local };
|
|
5822
|
-
ensurePrivateDirSync(
|
|
6131
|
+
ensurePrivateDirSync(path19.dirname(agentConfigPath));
|
|
5823
6132
|
writeFileSync9(agentConfigPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
5824
6133
|
enforcePrivateFileSync(agentConfigPath);
|
|
5825
6134
|
} catch {
|
|
@@ -6260,11 +6569,11 @@ var init_cloud_sync = __esm({
|
|
|
6260
6569
|
LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
|
|
6261
6570
|
FETCH_TIMEOUT_MS = 3e4;
|
|
6262
6571
|
PUSH_BATCH_SIZE = 5e3;
|
|
6263
|
-
ROSTER_LOCK_PATH =
|
|
6572
|
+
ROSTER_LOCK_PATH = path19.join(EXE_AI_DIR, "roster-merge.lock");
|
|
6264
6573
|
LOCK_STALE_MS = 3e4;
|
|
6265
6574
|
_pgPromise = null;
|
|
6266
6575
|
_pgFailed = false;
|
|
6267
|
-
ROSTER_DELETIONS_PATH =
|
|
6576
|
+
ROSTER_DELETIONS_PATH = path19.join(EXE_AI_DIR, "roster-deletions.json");
|
|
6268
6577
|
}
|
|
6269
6578
|
});
|
|
6270
6579
|
|
|
@@ -6408,9 +6717,9 @@ async function writeMemoryViaDaemon(entry) {
|
|
|
6408
6717
|
// src/adapters/claude/hooks/summary-worker.ts
|
|
6409
6718
|
init_task_scope();
|
|
6410
6719
|
init_employees();
|
|
6411
|
-
import { execSync as
|
|
6412
|
-
import { existsSync as
|
|
6413
|
-
import
|
|
6720
|
+
import { execSync as execSync5 } from "child_process";
|
|
6721
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, openSync as openSync3, closeSync as closeSync3 } from "fs";
|
|
6722
|
+
import path20 from "path";
|
|
6414
6723
|
async function main() {
|
|
6415
6724
|
const agentId = process.env.AGENT_ID ?? "default";
|
|
6416
6725
|
const agentRole = process.env.AGENT_ROLE ?? "employee";
|
|
@@ -6541,8 +6850,8 @@ async function main() {
|
|
|
6541
6850
|
}
|
|
6542
6851
|
try {
|
|
6543
6852
|
const { EXE_AI_DIR: EXE_AI_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
6544
|
-
const flagPath =
|
|
6545
|
-
if (
|
|
6853
|
+
const flagPath = path20.join(EXE_AI_DIR2, "session-cache", "needs-backfill");
|
|
6854
|
+
if (existsSync19(flagPath)) {
|
|
6546
6855
|
const { tryAcquireWorkerSlot: tryAcquireWorkerSlot2, registerWorkerPid: registerWorkerPid2 } = await Promise.resolve().then(() => (init_worker_gate(), worker_gate_exports));
|
|
6547
6856
|
if (!tryAcquireWorkerSlot2()) {
|
|
6548
6857
|
process.stderr.write("[summary-worker] Backfill needed but worker gate full \u2014 skipping\n");
|
|
@@ -6550,11 +6859,11 @@ async function main() {
|
|
|
6550
6859
|
const { spawn: spawn2 } = await import("child_process");
|
|
6551
6860
|
const { fileURLToPath: fileURLToPath3 } = await import("url");
|
|
6552
6861
|
const thisFile = fileURLToPath3(import.meta.url);
|
|
6553
|
-
const backfillPath =
|
|
6554
|
-
if (
|
|
6862
|
+
const backfillPath = path20.resolve(path20.dirname(thisFile), "backfill-vectors.js");
|
|
6863
|
+
if (existsSync19(backfillPath)) {
|
|
6555
6864
|
const { EXE_AI_DIR: exeDir2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
6556
|
-
const bLogPath =
|
|
6557
|
-
|
|
6865
|
+
const bLogPath = path20.join(exeDir2, "workers.log");
|
|
6866
|
+
mkdirSync10(path20.dirname(bLogPath), { recursive: true });
|
|
6558
6867
|
const bLogFd = openSync3(bLogPath, "a");
|
|
6559
6868
|
const child = spawn2(process.execPath, [backfillPath], {
|
|
6560
6869
|
detached: true,
|
|
@@ -6615,7 +6924,7 @@ async function main() {
|
|
|
6615
6924
|
const taskTitle = String(taskRow.title);
|
|
6616
6925
|
let lastCommit = "";
|
|
6617
6926
|
try {
|
|
6618
|
-
lastCommit =
|
|
6927
|
+
lastCommit = execSync5("git log --oneline -1 --since='30 minutes ago'", {
|
|
6619
6928
|
encoding: "utf8",
|
|
6620
6929
|
timeout: 5e3
|
|
6621
6930
|
}).trim();
|