@askexenow/exe-os 0.8.55 → 0.8.56
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/cli.js +49 -1
- package/dist/bin/install.js +47 -3
- package/dist/bin/update.js +485 -0
- package/dist/hooks/summary-worker.js +662 -211
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -6335,6 +6335,24 @@ async function runUpdate(cliArgs) {
|
|
|
6335
6335
|
console.log(" Try: npm cache clean --force && npm install -g @askexenow/exe-os@latest");
|
|
6336
6336
|
}
|
|
6337
6337
|
console.log(" Hooks re-wired, daemon restarted automatically.");
|
|
6338
|
+
try {
|
|
6339
|
+
const { existsSync: exists, readFileSync: readFile6 } = await import("fs");
|
|
6340
|
+
const p = await import("path");
|
|
6341
|
+
const { homedir: home } = await import("os");
|
|
6342
|
+
const exeDir = p.default.join(home(), ".exe-os");
|
|
6343
|
+
const licKeyPath = p.default.join(exeDir, "license.key");
|
|
6344
|
+
const configPath = p.default.join(exeDir, "config.json");
|
|
6345
|
+
if (!exists(licKeyPath) && exists(configPath)) {
|
|
6346
|
+
const cfg = JSON.parse(readFile6(configPath, "utf8"));
|
|
6347
|
+
const cloud = cfg.cloud;
|
|
6348
|
+
if (cloud?.apiKey) {
|
|
6349
|
+
const { mirrorLicenseKey: mirrorLicenseKey2 } = await Promise.resolve().then(() => (init_license(), license_exports));
|
|
6350
|
+
mirrorLicenseKey2(cloud.apiKey);
|
|
6351
|
+
console.log(" \u{1F511} License key restored from cloud config.");
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
} catch {
|
|
6355
|
+
}
|
|
6338
6356
|
console.log("\n\u{1F680} Ready. Start your COO session to use the new version.\n");
|
|
6339
6357
|
}
|
|
6340
6358
|
var init_update = __esm({
|
|
@@ -23118,10 +23136,40 @@ if (args.includes("--global")) {
|
|
|
23118
23136
|
} else if (args[0] === "update") {
|
|
23119
23137
|
const { runUpdate: runUpdate2 } = await Promise.resolve().then(() => (init_update(), update_exports));
|
|
23120
23138
|
await runUpdate2(args.slice(1));
|
|
23121
|
-
} else {
|
|
23139
|
+
} else if (args.includes("--tui") || args.includes("--demo") || args[0] === "tui") {
|
|
23122
23140
|
checkForUpdateOnBoot().catch(() => {
|
|
23123
23141
|
});
|
|
23124
23142
|
await init_App2().then(() => App_exports);
|
|
23143
|
+
} else {
|
|
23144
|
+
const claudeDir = path33.join(os11.homedir(), ".claude");
|
|
23145
|
+
const settingsPath = path33.join(claudeDir, "settings.json");
|
|
23146
|
+
const hasClaudeCode = existsSync21(settingsPath) && (() => {
|
|
23147
|
+
try {
|
|
23148
|
+
const raw = readFileSync17(settingsPath, "utf8");
|
|
23149
|
+
return raw.includes("exe-os") || raw.includes("exe-mem");
|
|
23150
|
+
} catch {
|
|
23151
|
+
return false;
|
|
23152
|
+
}
|
|
23153
|
+
})();
|
|
23154
|
+
if (hasClaudeCode) {
|
|
23155
|
+
console.log(`
|
|
23156
|
+
\x1B[1mexe-os\x1B[0m \u2014 AI Employee Operating System
|
|
23157
|
+
|
|
23158
|
+
\x1B[33mMode 1 detected:\x1B[0m Claude Code integration is installed.
|
|
23159
|
+
Open Claude Code and run \x1B[1m/exe\x1B[0m to boot your COO.
|
|
23160
|
+
|
|
23161
|
+
\x1B[2mCommands:\x1B[0m
|
|
23162
|
+
exe-os update Check for and install updates
|
|
23163
|
+
exe-os setup Re-run setup wizard
|
|
23164
|
+
exe-os claude check Verify Claude Code integration
|
|
23165
|
+
exe-os tui Launch TUI dashboard (Mode 2)
|
|
23166
|
+
exe-os --help Show all commands
|
|
23167
|
+
`);
|
|
23168
|
+
} else {
|
|
23169
|
+
checkForUpdateOnBoot().catch(() => {
|
|
23170
|
+
});
|
|
23171
|
+
await init_App2().then(() => App_exports);
|
|
23172
|
+
}
|
|
23125
23173
|
}
|
|
23126
23174
|
async function runClaudeInstall() {
|
|
23127
23175
|
const { runInstaller: runInstaller2 } = await Promise.resolve().then(() => (init_installer(), installer_exports));
|
package/dist/bin/install.js
CHANGED
|
@@ -672,8 +672,8 @@ function summarizeSymlinkResults(results) {
|
|
|
672
672
|
}
|
|
673
673
|
|
|
674
674
|
// src/bin/install.ts
|
|
675
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync, openSync, closeSync } from "fs";
|
|
676
|
-
import { spawn } from "child_process";
|
|
675
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync, readdirSync, openSync, closeSync } from "fs";
|
|
676
|
+
import { spawn, execSync as execSync2 } from "child_process";
|
|
677
677
|
import path5 from "path";
|
|
678
678
|
import { homedir } from "os";
|
|
679
679
|
var EXE_DIR = path5.join(homedir(), ".exe-os");
|
|
@@ -685,7 +685,7 @@ function restartDaemon() {
|
|
|
685
685
|
const pid = parseInt(readFileSync4(pidPath, "utf8").trim(), 10);
|
|
686
686
|
if (!isNaN(pid) && pid > 0) {
|
|
687
687
|
try {
|
|
688
|
-
process.kill(pid, "
|
|
688
|
+
process.kill(pid, "SIGKILL");
|
|
689
689
|
} catch {
|
|
690
690
|
}
|
|
691
691
|
}
|
|
@@ -694,10 +694,54 @@ function restartDaemon() {
|
|
|
694
694
|
} catch {
|
|
695
695
|
}
|
|
696
696
|
}
|
|
697
|
+
try {
|
|
698
|
+
const pids = execSync2("pgrep -f 'exe-daemon.js' 2>/dev/null || true", { encoding: "utf8" }).trim().split("\n").filter(Boolean).map(Number).filter((n) => !isNaN(n) && n !== process.pid);
|
|
699
|
+
for (const pid of pids) {
|
|
700
|
+
try {
|
|
701
|
+
process.kill(pid, "SIGKILL");
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (pids.length > 0) {
|
|
706
|
+
process.stderr.write(`exe-os: killed ${pids.length} orphaned daemon(s)
|
|
707
|
+
`);
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
const pids = execSync2("pgrep -f 'backfill-vectors.js' 2>/dev/null || true", { encoding: "utf8" }).trim().split("\n").filter(Boolean).map(Number).filter((n) => !isNaN(n) && n !== process.pid);
|
|
713
|
+
for (const pid of pids) {
|
|
714
|
+
try {
|
|
715
|
+
process.kill(pid, "SIGKILL");
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (pids.length > 0) {
|
|
720
|
+
process.stderr.write(`exe-os: killed ${pids.length} orphaned backfill worker(s)
|
|
721
|
+
`);
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const wpDir = path5.join(EXE_DIR, "worker-pids");
|
|
727
|
+
if (existsSync5(wpDir)) {
|
|
728
|
+
for (const f of readdirSync(wpDir)) {
|
|
729
|
+
try {
|
|
730
|
+
unlinkSync(path5.join(wpDir, f));
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
697
737
|
try {
|
|
698
738
|
unlinkSync(sockPath);
|
|
699
739
|
} catch {
|
|
700
740
|
}
|
|
741
|
+
try {
|
|
742
|
+
execSync2("sleep 0.5");
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
701
745
|
} catch {
|
|
702
746
|
}
|
|
703
747
|
try {
|
package/dist/bin/update.js
CHANGED
|
@@ -1,4 +1,471 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/lib/config.ts
|
|
19
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
20
|
+
import { readFileSync as readFileSync2, existsSync, renameSync } from "fs";
|
|
21
|
+
import path2 from "path";
|
|
22
|
+
import os from "os";
|
|
23
|
+
function resolveDataDir() {
|
|
24
|
+
if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
|
|
25
|
+
if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
|
|
26
|
+
const newDir = path2.join(os.homedir(), ".exe-os");
|
|
27
|
+
const legacyDir = path2.join(os.homedir(), ".exe-mem");
|
|
28
|
+
if (!existsSync(newDir) && existsSync(legacyDir)) {
|
|
29
|
+
try {
|
|
30
|
+
renameSync(legacyDir, newDir);
|
|
31
|
+
process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
|
|
32
|
+
`);
|
|
33
|
+
} catch {
|
|
34
|
+
return legacyDir;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return newDir;
|
|
38
|
+
}
|
|
39
|
+
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG;
|
|
40
|
+
var init_config = __esm({
|
|
41
|
+
"src/lib/config.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
EXE_AI_DIR = resolveDataDir();
|
|
44
|
+
DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
|
|
45
|
+
MODELS_DIR = path2.join(EXE_AI_DIR, "models");
|
|
46
|
+
CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
|
|
47
|
+
LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
|
|
48
|
+
CURRENT_CONFIG_VERSION = 1;
|
|
49
|
+
DEFAULT_CONFIG = {
|
|
50
|
+
config_version: CURRENT_CONFIG_VERSION,
|
|
51
|
+
dbPath: DB_PATH,
|
|
52
|
+
modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
|
|
53
|
+
embeddingDim: 1024,
|
|
54
|
+
batchSize: 20,
|
|
55
|
+
flushIntervalMs: 1e4,
|
|
56
|
+
autoIngestion: true,
|
|
57
|
+
autoRetrieval: true,
|
|
58
|
+
searchMode: "hybrid",
|
|
59
|
+
hookSearchMode: "hybrid",
|
|
60
|
+
fileGrepEnabled: true,
|
|
61
|
+
splashEffect: true,
|
|
62
|
+
consolidationEnabled: true,
|
|
63
|
+
consolidationIntervalMs: 6 * 60 * 60 * 1e3,
|
|
64
|
+
consolidationModel: "claude-haiku-4-5-20251001",
|
|
65
|
+
consolidationMaxCallsPerRun: 20,
|
|
66
|
+
selfQueryRouter: true,
|
|
67
|
+
selfQueryModel: "claude-haiku-4-5-20251001",
|
|
68
|
+
rerankerEnabled: true,
|
|
69
|
+
scalingRoadmap: {
|
|
70
|
+
rerankerAutoTrigger: {
|
|
71
|
+
enabled: true,
|
|
72
|
+
broadQueryMinCardinality: 5e4,
|
|
73
|
+
fetchTopK: 150,
|
|
74
|
+
returnTopK: 5
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
graphRagEnabled: true,
|
|
78
|
+
wikiEnabled: false,
|
|
79
|
+
wikiUrl: "",
|
|
80
|
+
wikiApiKey: "",
|
|
81
|
+
wikiSyncIntervalMs: 30 * 60 * 1e3,
|
|
82
|
+
wikiWorkspaceMapping: {
|
|
83
|
+
exe: "Executive",
|
|
84
|
+
yoshi: "Engineering",
|
|
85
|
+
mari: "Marketing",
|
|
86
|
+
tom: "Engineering",
|
|
87
|
+
sasha: "Production"
|
|
88
|
+
},
|
|
89
|
+
wikiAutoUpdate: true,
|
|
90
|
+
wikiAutoUpdateThreshold: 0.5,
|
|
91
|
+
wikiAutoUpdateCreateNew: true,
|
|
92
|
+
skillLearning: true,
|
|
93
|
+
skillThreshold: 3,
|
|
94
|
+
skillModel: "claude-haiku-4-5-20251001",
|
|
95
|
+
exeHeartbeat: {
|
|
96
|
+
enabled: true,
|
|
97
|
+
intervalSeconds: 60,
|
|
98
|
+
staleInProgressThresholdHours: 2
|
|
99
|
+
},
|
|
100
|
+
sessionLifecycle: {
|
|
101
|
+
idleKillEnabled: true,
|
|
102
|
+
idleKillTicksRequired: 3,
|
|
103
|
+
idleKillIntercomAckWindowMs: 1e4,
|
|
104
|
+
maxAutoInstances: 10
|
|
105
|
+
},
|
|
106
|
+
autoUpdate: {
|
|
107
|
+
checkOnBoot: true,
|
|
108
|
+
autoInstall: false,
|
|
109
|
+
checkIntervalMs: 24 * 60 * 60 * 1e3
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// src/lib/license.ts
|
|
116
|
+
var license_exports = {};
|
|
117
|
+
__export(license_exports, {
|
|
118
|
+
LICENSE_PUBLIC_KEY_PEM: () => LICENSE_PUBLIC_KEY_PEM,
|
|
119
|
+
PLAN_LIMITS: () => PLAN_LIMITS,
|
|
120
|
+
assertVpsLicense: () => assertVpsLicense,
|
|
121
|
+
checkLicense: () => checkLicense,
|
|
122
|
+
getCachedLicense: () => getCachedLicense,
|
|
123
|
+
isFeatureAllowed: () => isFeatureAllowed,
|
|
124
|
+
loadDeviceId: () => loadDeviceId,
|
|
125
|
+
loadLicense: () => loadLicense,
|
|
126
|
+
mirrorLicenseKey: () => mirrorLicenseKey,
|
|
127
|
+
saveLicense: () => saveLicense,
|
|
128
|
+
startLicenseRevalidation: () => startLicenseRevalidation,
|
|
129
|
+
stopLicenseRevalidation: () => stopLicenseRevalidation,
|
|
130
|
+
validateLicense: () => validateLicense
|
|
131
|
+
});
|
|
132
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
133
|
+
import { randomUUID } from "crypto";
|
|
134
|
+
import path3 from "path";
|
|
135
|
+
import { jwtVerify, importSPKI } from "jose";
|
|
136
|
+
async function fetchRetry(url, init) {
|
|
137
|
+
try {
|
|
138
|
+
return await fetch(url, init);
|
|
139
|
+
} catch {
|
|
140
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
141
|
+
return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function loadDeviceId() {
|
|
145
|
+
const deviceJsonPath = path3.join(EXE_AI_DIR, "device.json");
|
|
146
|
+
try {
|
|
147
|
+
if (existsSync2(deviceJsonPath)) {
|
|
148
|
+
const data = JSON.parse(readFileSync3(deviceJsonPath, "utf8"));
|
|
149
|
+
if (data.deviceId) return data.deviceId;
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (existsSync2(DEVICE_ID_PATH)) {
|
|
155
|
+
const id2 = readFileSync3(DEVICE_ID_PATH, "utf8").trim();
|
|
156
|
+
if (id2) return id2;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
const id = randomUUID();
|
|
161
|
+
mkdirSync(EXE_AI_DIR, { recursive: true });
|
|
162
|
+
writeFileSync(DEVICE_ID_PATH, id, "utf8");
|
|
163
|
+
return id;
|
|
164
|
+
}
|
|
165
|
+
function loadLicense() {
|
|
166
|
+
try {
|
|
167
|
+
if (!existsSync2(LICENSE_PATH)) return null;
|
|
168
|
+
return readFileSync3(LICENSE_PATH, "utf8").trim();
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function saveLicense(apiKey) {
|
|
174
|
+
mkdirSync(EXE_AI_DIR, { recursive: true });
|
|
175
|
+
writeFileSync(LICENSE_PATH, apiKey.trim(), { encoding: "utf8", mode: 384 });
|
|
176
|
+
}
|
|
177
|
+
async function verifyLicenseJwt(token) {
|
|
178
|
+
try {
|
|
179
|
+
const key = await importSPKI(LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG);
|
|
180
|
+
const { payload } = await jwtVerify(token, key, {
|
|
181
|
+
algorithms: [LICENSE_JWT_ALG]
|
|
182
|
+
});
|
|
183
|
+
const plan = payload.plan ?? "free";
|
|
184
|
+
const email = payload.sub ?? "";
|
|
185
|
+
const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
|
|
186
|
+
return {
|
|
187
|
+
valid: true,
|
|
188
|
+
plan,
|
|
189
|
+
email,
|
|
190
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
|
|
191
|
+
deviceLimit: limits.devices,
|
|
192
|
+
employeeLimit: limits.employees,
|
|
193
|
+
memoryLimit: limits.memories
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function getCachedLicense() {
|
|
200
|
+
try {
|
|
201
|
+
if (!existsSync2(CACHE_PATH)) return null;
|
|
202
|
+
const raw = JSON.parse(readFileSync3(CACHE_PATH, "utf8"));
|
|
203
|
+
if (!raw.token || typeof raw.token !== "string") return null;
|
|
204
|
+
return await verifyLicenseJwt(raw.token);
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function readCachedToken() {
|
|
210
|
+
try {
|
|
211
|
+
if (!existsSync2(CACHE_PATH)) return null;
|
|
212
|
+
const raw = JSON.parse(readFileSync3(CACHE_PATH, "utf8"));
|
|
213
|
+
return typeof raw.token === "string" ? raw.token : null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function cacheResponse(token) {
|
|
219
|
+
try {
|
|
220
|
+
writeFileSync(CACHE_PATH, JSON.stringify({ token }), "utf8");
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function validateLicense(apiKey, deviceId) {
|
|
225
|
+
const did = deviceId ?? loadDeviceId();
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetchRetry(`${API_BASE}/auth/activate`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: { "Content-Type": "application/json" },
|
|
230
|
+
body: JSON.stringify({ apiKey, deviceId: did }),
|
|
231
|
+
signal: AbortSignal.timeout(1e4)
|
|
232
|
+
});
|
|
233
|
+
if (res.ok) {
|
|
234
|
+
const data = await res.json();
|
|
235
|
+
if (data.error === "device_limit_exceeded") {
|
|
236
|
+
const cached2 = await getCachedLicense();
|
|
237
|
+
if (cached2) return cached2;
|
|
238
|
+
return { ...FREE_LICENSE, valid: false, plan: "free" };
|
|
239
|
+
}
|
|
240
|
+
if (data.token) {
|
|
241
|
+
cacheResponse(data.token);
|
|
242
|
+
const verified = await verifyLicenseJwt(data.token);
|
|
243
|
+
if (verified) return verified;
|
|
244
|
+
}
|
|
245
|
+
const limits = PLAN_LIMITS[data.plan] ?? PLAN_LIMITS.free;
|
|
246
|
+
return {
|
|
247
|
+
valid: data.valid,
|
|
248
|
+
plan: data.plan,
|
|
249
|
+
email: data.email,
|
|
250
|
+
expiresAt: data.expiresAt,
|
|
251
|
+
deviceLimit: limits.devices,
|
|
252
|
+
employeeLimit: limits.employees,
|
|
253
|
+
memoryLimit: limits.memories
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const cached = await getCachedLicense();
|
|
257
|
+
if (cached) return cached;
|
|
258
|
+
return { ...FREE_LICENSE, valid: false, plan: "free" };
|
|
259
|
+
} catch {
|
|
260
|
+
const cached = await getCachedLicense();
|
|
261
|
+
if (cached) return cached;
|
|
262
|
+
return { ...FREE_LICENSE, valid: false, error: "offline" };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function getCacheAgeMs() {
|
|
266
|
+
try {
|
|
267
|
+
const { statSync } = __require("fs");
|
|
268
|
+
const s = statSync(CACHE_PATH);
|
|
269
|
+
return Date.now() - s.mtimeMs;
|
|
270
|
+
} catch {
|
|
271
|
+
return Infinity;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function checkLicense() {
|
|
275
|
+
let key = loadLicense();
|
|
276
|
+
if (!key) {
|
|
277
|
+
try {
|
|
278
|
+
const configPath = path3.join(EXE_AI_DIR, "config.json");
|
|
279
|
+
if (existsSync2(configPath)) {
|
|
280
|
+
const raw = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
281
|
+
const cloud = raw.cloud;
|
|
282
|
+
if (cloud?.apiKey) {
|
|
283
|
+
key = cloud.apiKey;
|
|
284
|
+
saveLicense(key);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!key) return FREE_LICENSE;
|
|
291
|
+
const cached = await getCachedLicense();
|
|
292
|
+
if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
|
|
293
|
+
const deviceId = loadDeviceId();
|
|
294
|
+
return validateLicense(key, deviceId);
|
|
295
|
+
}
|
|
296
|
+
function isFeatureAllowed(license, feature) {
|
|
297
|
+
switch (feature) {
|
|
298
|
+
case "cloud_sync":
|
|
299
|
+
case "external_agents":
|
|
300
|
+
case "wiki":
|
|
301
|
+
return license.plan !== "free";
|
|
302
|
+
case "unlimited_employees":
|
|
303
|
+
return license.plan === "team" || license.plan === "agency" || license.plan === "enterprise";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function mirrorLicenseKey(apiKey) {
|
|
307
|
+
const trimmed = apiKey.trim();
|
|
308
|
+
if (!trimmed) return;
|
|
309
|
+
saveLicense(trimmed);
|
|
310
|
+
}
|
|
311
|
+
async function assertVpsLicense(opts) {
|
|
312
|
+
const env = opts?.env ?? process.env;
|
|
313
|
+
const inProduction = env.NODE_ENV === "production";
|
|
314
|
+
if (!opts?.force && !inProduction) {
|
|
315
|
+
return { ...FREE_LICENSE, plan: "free" };
|
|
316
|
+
}
|
|
317
|
+
const envKey = env.EXE_LICENSE_KEY?.trim();
|
|
318
|
+
if (envKey) {
|
|
319
|
+
saveLicense(envKey);
|
|
320
|
+
}
|
|
321
|
+
const apiKey = envKey ?? loadLicense();
|
|
322
|
+
if (!apiKey) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
"License required: set EXE_LICENSE_KEY env var with your exe_sk_* key. Purchase at https://askexe.com. This VPS image refuses to boot without a valid license."
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const deviceId = loadDeviceId();
|
|
328
|
+
let backendResponse = null;
|
|
329
|
+
let explicitRejection = false;
|
|
330
|
+
let transientFailure = false;
|
|
331
|
+
try {
|
|
332
|
+
const res = await fetchRetry(`${API_BASE}/auth/activate`, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: { "Content-Type": "application/json" },
|
|
335
|
+
body: JSON.stringify({ apiKey, deviceId }),
|
|
336
|
+
signal: AbortSignal.timeout(1e4)
|
|
337
|
+
});
|
|
338
|
+
if (res.ok) {
|
|
339
|
+
backendResponse = await res.json();
|
|
340
|
+
if (!backendResponse.valid) explicitRejection = true;
|
|
341
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
342
|
+
explicitRejection = true;
|
|
343
|
+
} else {
|
|
344
|
+
transientFailure = true;
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
transientFailure = true;
|
|
348
|
+
}
|
|
349
|
+
if (backendResponse?.valid) {
|
|
350
|
+
if (backendResponse.token) {
|
|
351
|
+
cacheResponse(backendResponse.token);
|
|
352
|
+
const verified = await verifyLicenseJwt(backendResponse.token);
|
|
353
|
+
if (verified) return verified;
|
|
354
|
+
}
|
|
355
|
+
const limits = PLAN_LIMITS[backendResponse.plan] ?? PLAN_LIMITS.free;
|
|
356
|
+
return {
|
|
357
|
+
valid: true,
|
|
358
|
+
plan: backendResponse.plan,
|
|
359
|
+
email: backendResponse.email,
|
|
360
|
+
expiresAt: backendResponse.expiresAt,
|
|
361
|
+
deviceLimit: limits.devices,
|
|
362
|
+
employeeLimit: limits.employees,
|
|
363
|
+
memoryLimit: limits.memories
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (explicitRejection) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`License invalid or expired. Renew at https://askexe.com, then restart. Backend rejected key ending in ...${apiKey.slice(-4)}.`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (!transientFailure) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
"License validation failed: unknown backend state. Restore network connectivity to https://askexe.com/cloud and retry."
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const fresh = await getCachedLicense();
|
|
377
|
+
if (fresh && fresh.valid) return fresh;
|
|
378
|
+
const graceDays = opts?.offlineGraceDays ?? 7;
|
|
379
|
+
const graceMs = graceDays * 24 * 60 * 60 * 1e3;
|
|
380
|
+
try {
|
|
381
|
+
const token = readCachedToken();
|
|
382
|
+
if (token) {
|
|
383
|
+
const payloadB64 = token.split(".")[1];
|
|
384
|
+
if (payloadB64) {
|
|
385
|
+
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
|
|
386
|
+
const expMs = (payload.exp ?? 0) * 1e3;
|
|
387
|
+
if (Date.now() < expMs + graceMs) {
|
|
388
|
+
const key = await importSPKI(LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG);
|
|
389
|
+
const { payload: verified } = await jwtVerify(token, key, {
|
|
390
|
+
algorithms: [LICENSE_JWT_ALG],
|
|
391
|
+
clockTolerance: graceDays * 24 * 60 * 60
|
|
392
|
+
});
|
|
393
|
+
const plan = verified.plan ?? "free";
|
|
394
|
+
const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
|
|
395
|
+
return {
|
|
396
|
+
valid: true,
|
|
397
|
+
plan,
|
|
398
|
+
email: verified.sub ?? "",
|
|
399
|
+
expiresAt: verified.exp ? new Date(verified.exp * 1e3).toISOString() : null,
|
|
400
|
+
deviceLimit: limits.devices,
|
|
401
|
+
employeeLimit: limits.employees,
|
|
402
|
+
memoryLimit: limits.memories
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
throw new Error(
|
|
410
|
+
`License validation unreachable for more than ${graceDays} days. Restore network connectivity to https://askexe.com/cloud and retry. This VPS image refuses to boot after the offline grace window.`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
function startLicenseRevalidation(intervalMs = 36e5) {
|
|
414
|
+
if (_revalTimer) return;
|
|
415
|
+
_revalTimer = setInterval(async () => {
|
|
416
|
+
try {
|
|
417
|
+
const license = await checkLicense();
|
|
418
|
+
if (!license.valid) {
|
|
419
|
+
process.stderr.write("[exe-os] License expired or invalid \u2014 features may be restricted\n");
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}, intervalMs);
|
|
424
|
+
if (_revalTimer && typeof _revalTimer === "object" && "unref" in _revalTimer) {
|
|
425
|
+
_revalTimer.unref();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function stopLicenseRevalidation() {
|
|
429
|
+
if (_revalTimer) {
|
|
430
|
+
clearInterval(_revalTimer);
|
|
431
|
+
_revalTimer = null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, RETRY_DELAY_MS, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS, _revalTimer;
|
|
435
|
+
var init_license = __esm({
|
|
436
|
+
"src/lib/license.ts"() {
|
|
437
|
+
"use strict";
|
|
438
|
+
init_config();
|
|
439
|
+
LICENSE_PATH = path3.join(EXE_AI_DIR, "license.key");
|
|
440
|
+
CACHE_PATH = path3.join(EXE_AI_DIR, "license-cache.json");
|
|
441
|
+
DEVICE_ID_PATH = path3.join(EXE_AI_DIR, "device-id");
|
|
442
|
+
API_BASE = "https://askexe.com/cloud";
|
|
443
|
+
RETRY_DELAY_MS = 500;
|
|
444
|
+
LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
445
|
+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
|
|
446
|
+
4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
|
|
447
|
+
-----END PUBLIC KEY-----`;
|
|
448
|
+
LICENSE_JWT_ALG = "ES256";
|
|
449
|
+
PLAN_LIMITS = {
|
|
450
|
+
free: { devices: 1, employees: 1, memories: 5e3 },
|
|
451
|
+
pro: { devices: 2, employees: 5, memories: 1e5 },
|
|
452
|
+
team: { devices: 10, employees: 20, memories: 1e6 },
|
|
453
|
+
agency: { devices: 50, employees: 100, memories: 1e7 },
|
|
454
|
+
enterprise: { devices: -1, employees: -1, memories: -1 }
|
|
455
|
+
};
|
|
456
|
+
FREE_LICENSE = {
|
|
457
|
+
valid: true,
|
|
458
|
+
plan: "free",
|
|
459
|
+
email: "",
|
|
460
|
+
expiresAt: null,
|
|
461
|
+
deviceLimit: 1,
|
|
462
|
+
employeeLimit: 1,
|
|
463
|
+
memoryLimit: 5e3
|
|
464
|
+
};
|
|
465
|
+
CACHE_MAX_AGE_MS = 36e5;
|
|
466
|
+
_revalTimer = null;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
2
469
|
|
|
3
470
|
// src/bin/update.ts
|
|
4
471
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -140,6 +607,24 @@ async function runUpdate(cliArgs) {
|
|
|
140
607
|
console.log(" Try: npm cache clean --force && npm install -g @askexenow/exe-os@latest");
|
|
141
608
|
}
|
|
142
609
|
console.log(" Hooks re-wired, daemon restarted automatically.");
|
|
610
|
+
try {
|
|
611
|
+
const { existsSync: exists, readFileSync: readFile2 } = await import("fs");
|
|
612
|
+
const p = await import("path");
|
|
613
|
+
const { homedir: home } = await import("os");
|
|
614
|
+
const exeDir = p.default.join(home(), ".exe-os");
|
|
615
|
+
const licKeyPath = p.default.join(exeDir, "license.key");
|
|
616
|
+
const configPath = p.default.join(exeDir, "config.json");
|
|
617
|
+
if (!exists(licKeyPath) && exists(configPath)) {
|
|
618
|
+
const cfg = JSON.parse(readFile2(configPath, "utf8"));
|
|
619
|
+
const cloud = cfg.cloud;
|
|
620
|
+
if (cloud?.apiKey) {
|
|
621
|
+
const { mirrorLicenseKey: mirrorLicenseKey2 } = await Promise.resolve().then(() => (init_license(), license_exports));
|
|
622
|
+
mirrorLicenseKey2(cloud.apiKey);
|
|
623
|
+
console.log(" \u{1F511} License key restored from cloud config.");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
143
628
|
console.log("\n\u{1F680} Ready. Start your COO session to use the new version.\n");
|
|
144
629
|
}
|
|
145
630
|
if (isMainModule(import.meta.url)) {
|