@cryptiklemur/lattice 5.10.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{angular-html-BDIcxkJq.js → angular-html-BoFzmWT8.js} +1 -1
- package/dist/client/assets/{angular-ts-Bt22ouNH.js → angular-ts-DZnI8rKE.js} +1 -1
- package/dist/client/assets/{apl-p8qkxzEK.js → apl-DstVmncE.js} +1 -1
- package/dist/client/assets/{astro-CIaMc49M.js → astro-DTPCjzEx.js} +1 -1
- package/dist/client/assets/{blade-BR56EAMD.js → blade-6q42Ss3F.js} +1 -1
- package/dist/client/assets/{c-Dli0HzAh.js → c-BQDGJ-nQ.js} +1 -1
- package/dist/client/assets/{cobol-Cad15ECy.js → cobol-Dlh0WvsZ.js} +1 -1
- package/dist/client/assets/{coffee-DpyATEbF.js → coffee-DdQv129j.js} +1 -1
- package/dist/client/assets/{cpp-KN8_NFsf.js → cpp-DhbQJIv4.js} +1 -1
- package/dist/client/assets/{crystal-CuyGv0kh.js → crystal-C22kERUB.js} +1 -1
- package/dist/client/assets/{css-Cm3q4bxn.js → css-n31O5kHj.js} +1 -1
- package/dist/client/assets/{dist-BjxsMc4u.js → dist-D8okl7lw.js} +2 -2
- package/dist/client/assets/{edge-B6S7CSbx.js → edge-Cgwx-o_7.js} +1 -1
- package/dist/client/assets/{elixir-CNUy9H8T.js → elixir-DAGM2WKD.js} +1 -1
- package/dist/client/assets/{elm-CNfcWmb9.js → elm-BLw_7oO9.js} +1 -1
- package/dist/client/assets/{erb-DWebzDaI.js → erb-DCaNhYa7.js} +1 -1
- package/dist/client/assets/{git-rebase-B_Pt2ZBK.js → git-rebase-CNNhb8-g.js} +1 -1
- package/dist/client/assets/{glimmer-js-CVwoOd72.js → glimmer-js-BnZd88Wi.js} +1 -1
- package/dist/client/assets/{glimmer-ts-CjtFSxjz.js → glimmer-ts-DvFNbZu-.js} +1 -1
- package/dist/client/assets/{glsl-CP4rggAA.js → glsl-Dnrk_Jnx.js} +1 -1
- package/dist/client/assets/{graphql-Dbm6sAtp.js → graphql-DlWTPvCG.js} +1 -1
- package/dist/client/assets/{hack-Bj9y3SGf.js → hack-DQg1Ek33.js} +1 -1
- package/dist/client/assets/{haml-DRGrdf3f.js → haml-DSk45qIE.js} +1 -1
- package/dist/client/assets/{handlebars-CFKjcBMg.js → handlebars-DuLvATB2.js} +1 -1
- package/dist/client/assets/{html-Vcd4eHHg.js → html-D4DiUnLg.js} +1 -1
- package/dist/client/assets/{html-derivative-BF0YbD4L.js → html-derivative-CS5MZ6d9.js} +1 -1
- package/dist/client/assets/{http-CGVTa2NT.js → http-CkDncfer.js} +1 -1
- package/dist/client/assets/{hurl-B0GrsGqd.js → hurl-DU39oO3U.js} +1 -1
- package/dist/client/assets/{index-CX1tudsF.js → index-CHPfE1Zl.js} +129 -129
- package/dist/client/assets/index-DHUKmLLC.css +2 -0
- package/dist/client/assets/{java-BJHQqHsm.js → java-lntACKEu.js} +1 -1
- package/dist/client/assets/{javascript-CmuMsKrc.js → javascript-CxkFc6nV.js} +1 -1
- package/dist/client/assets/{jinja-JxCLeq1j.js → jinja-DolO2zO7.js} +1 -1
- package/dist/client/assets/{jison-BdgAUhei.js → jison-Cok5FPev.js} +1 -1
- package/dist/client/assets/{json-DtPissHL.js → json-BebuQPrq.js} +1 -1
- package/dist/client/assets/{jsx-DUAxxDkP.js → jsx-iLBaUyXr.js} +1 -1
- package/dist/client/assets/{julia-DxDlbL6e.js → julia-C5Dsc7cH.js} +1 -1
- package/dist/client/assets/{just-CVmAAx2R.js → just-DJYqq_9R.js} +1 -1
- package/dist/client/assets/{latex-uwxggTWA.js → latex-BTTYiKj1.js} +1 -1
- package/dist/client/assets/{liquid-xsETAJJy.js → liquid-DpAKCrOB.js} +1 -1
- package/dist/client/assets/{lua-B2Hh8PgD.js → lua-BZ6b1hko.js} +1 -1
- package/dist/client/assets/{marko-yDeGxD87.js → marko-D8VK6iGt.js} +1 -1
- package/dist/client/assets/{mdc-QMp4ieYR.js → mdc-Paa3XzwY.js} +1 -1
- package/dist/client/assets/{nginx-7gmRmcqz.js → nginx-C5k9mWtJ.js} +1 -1
- package/dist/client/assets/{nim-CA8SNY_7.js → nim-Dst6YSnE.js} +1 -1
- package/dist/client/assets/{perl-lx5nW4VC.js → perl-XhiCjgBp.js} +1 -1
- package/dist/client/assets/{php-DgHiW953.js → php-BcsPLnLU.js} +1 -1
- package/dist/client/assets/{pug-CbbB1vwb.js → pug-GLH9-eAJ.js} +1 -1
- package/dist/client/assets/{qml-COrzwCIh.js → qml-Cj_lJioE.js} +1 -1
- package/dist/client/assets/{r-Dv7pZJDH.js → r-B70aGYK5.js} +1 -1
- package/dist/client/assets/{razor-D2m8EDP5.js → razor-R3gub_zy.js} +1 -1
- package/dist/client/assets/{regexp-BXLT-jPc.js → regexp-itC0dIUJ.js} +1 -1
- package/dist/client/assets/{rst-_S6rrUYh.js → rst-DdyoV8E2.js} +1 -1
- package/dist/client/assets/{ruby-C3XO7tYY.js → ruby-BYBZsv66.js} +1 -1
- package/dist/client/assets/{sas-DP2k4iuN.js → sas-fqfqXqj1.js} +1 -1
- package/dist/client/assets/{scss-lhLFMXGn.js → scss-B-ELv6mu.js} +1 -1
- package/dist/client/assets/{shellscript-BYlBPHen.js → shellscript-BgB8TNw6.js} +1 -1
- package/dist/client/assets/{shellsession-CbVyQKWZ.js → shellsession-BLK2Dgkm.js} +1 -1
- package/dist/client/assets/{soy-Be8a0lHq.js → soy-C7_RmNrp.js} +1 -1
- package/dist/client/assets/{sql-2KxvU9YS.js → sql-AUgbUJq4.js} +1 -1
- package/dist/client/assets/{stata-BxlWftTS.js → stata-CIVqSIOr.js} +1 -1
- package/dist/client/assets/{surrealql-CJ-q86nR.js → surrealql-BzRQzc5S.js} +1 -1
- package/dist/client/assets/{svelte-Q1ml0OiY.js → svelte-BCIwEwtb.js} +1 -1
- package/dist/client/assets/{templ-BbfPZhtu.js → templ-C1hbwe4u.js} +1 -1
- package/dist/client/assets/{tex-Dcth4Gi6.js → tex-CI4tIsaP.js} +1 -1
- package/dist/client/assets/{ts-tags-BKhSOXI3.js → ts-tags-SUeikhEp.js} +1 -1
- package/dist/client/assets/{tsx-CS6iQ0XH.js → tsx-xkp7aIZs.js} +1 -1
- package/dist/client/assets/{twig-BHp31ZxS.js → twig-CGgBSAyc.js} +1 -1
- package/dist/client/assets/{typescript-16YJBTaO.js → typescript-O2YMTl_s.js} +1 -1
- package/dist/client/assets/{vue-CMKwTi4r.js → vue-DsNRxos1.js} +1 -1
- package/dist/client/assets/{vue-html-Dr8VUA2G.js → vue-html-CuY3t7bs.js} +1 -1
- package/dist/client/assets/{vue-vine-DZUqDerl.js → vue-vine-C6kSCKwY.js} +1 -1
- package/dist/client/assets/{xml-CBbBKKDC.js → xml-DafwzOLY.js} +1 -1
- package/dist/client/assets/{xsl-DWEX6PKX.js → xsl-1SGGZibr.js} +1 -1
- package/dist/client/assets/{yaml-DvKvvh3X.js → yaml-DSVhzmhr.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/analytics/engine.js +241 -241
- package/dist/server/assets.js +4 -4
- package/dist/server/auth/passphrase.js +13 -13
- package/dist/server/config.js +7 -7
- package/dist/server/daemon.js +93 -93
- package/dist/server/features/brainstorm.js +42 -42
- package/dist/server/features/ralph-loop.js +33 -33
- package/dist/server/features/scheduler.js +53 -53
- package/dist/server/features/specs.js +54 -54
- package/dist/server/features/sticky-notes.js +17 -17
- package/dist/server/features/superpowers.js +24 -24
- package/dist/server/handlers/analytics.js +1 -1
- package/dist/server/handlers/attachment.js +32 -32
- package/dist/server/handlers/bookmarks.js +4 -4
- package/dist/server/handlers/brainstorm.js +4 -4
- package/dist/server/handlers/chat.js +54 -54
- package/dist/server/handlers/editor.js +13 -13
- package/dist/server/handlers/fs.js +51 -51
- package/dist/server/handlers/hooks.js +20 -20
- package/dist/server/handlers/loop.js +6 -6
- package/dist/server/handlers/memory.js +44 -44
- package/dist/server/handlers/mesh.js +60 -60
- package/dist/server/handlers/notes.js +7 -7
- package/dist/server/handlers/plugins.js +174 -174
- package/dist/server/handlers/project-settings.js +26 -26
- package/dist/server/handlers/scheduler.js +6 -6
- package/dist/server/handlers/session.js +24 -24
- package/dist/server/handlers/settings.js +21 -21
- package/dist/server/handlers/skills.js +91 -91
- package/dist/server/handlers/specs.js +51 -28
- package/dist/server/handlers/terminal.js +13 -13
- package/dist/server/handlers/themes.js +21 -21
- package/dist/server/handlers/update.js +17 -17
- package/dist/server/hooks/event_forward.sh +34 -0
- package/dist/server/hooks/post_tool_use.sh +26 -0
- package/dist/server/hooks/statusline.sh +26 -0
- package/dist/server/identity.js +6 -6
- package/dist/server/index.js +111 -111
- package/dist/server/logger.js +1 -1
- package/dist/server/mesh/connector.js +78 -78
- package/dist/server/mesh/crypto.js +20 -20
- package/dist/server/mesh/discovery.js +14 -14
- package/dist/server/mesh/pairing.js +30 -30
- package/dist/server/mesh/peers.js +10 -10
- package/dist/server/mesh/proxy.js +14 -14
- package/dist/server/mesh/session-sync.js +23 -23
- package/dist/server/project/bookmarks.js +11 -11
- package/dist/server/project/context-breakdown.js +70 -70
- package/dist/server/project/file-browser.js +17 -17
- package/dist/server/project/project-files.js +68 -68
- package/dist/server/project/registry.js +10 -10
- package/dist/server/project/sdk-bridge.js +157 -157
- package/dist/server/project/session.js +201 -199
- package/dist/server/project/terminal.js +15 -15
- package/dist/server/project/warmup.js +37 -37
- package/dist/server/push.js +11 -11
- package/dist/server/runtime.js +1 -1
- package/dist/server/tls.js +15 -15
- package/dist/server/tui.js +15 -15
- package/dist/server/update-checker.js +21 -21
- package/dist/server/ws/broadcast.js +18 -18
- package/dist/server/ws/router.js +17 -17
- package/dist/shared/constants.js +8 -8
- package/package.json +2 -2
- package/dist/client/assets/index-DlfI20Gn.css +0 -2
|
@@ -2,13 +2,13 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const __dirname_local = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const terminals = new Map();
|
|
7
|
+
const WORKER_PATH = join(__dirname_local, "pty-worker.cjs");
|
|
8
|
+
const NODE_MODULES_PATH = (function () {
|
|
9
9
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const resolved = require.resolve("node-pty");
|
|
11
|
+
const parts = resolved.split("/node_modules/");
|
|
12
12
|
parts.pop();
|
|
13
13
|
return parts.join("/node_modules/") + "/node_modules";
|
|
14
14
|
}
|
|
@@ -17,23 +17,23 @@ var NODE_MODULES_PATH = (function () {
|
|
|
17
17
|
}
|
|
18
18
|
})();
|
|
19
19
|
export function createTerminal(cwd, onData, onExit) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const termId = randomUUID();
|
|
21
|
+
const child = spawn("node", [WORKER_PATH], {
|
|
22
22
|
stdio: ["pipe", "pipe", "ignore"],
|
|
23
23
|
cwd: cwd,
|
|
24
24
|
env: { ...process.env, NODE_PATH: NODE_MODULES_PATH },
|
|
25
25
|
});
|
|
26
|
-
|
|
26
|
+
let buffer = "";
|
|
27
27
|
child.stdout.setEncoding("utf-8");
|
|
28
28
|
child.stdout.on("data", function (chunk) {
|
|
29
29
|
buffer += chunk;
|
|
30
|
-
|
|
30
|
+
const lines = buffer.split("\n");
|
|
31
31
|
buffer = lines.pop() || "";
|
|
32
|
-
for (
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
33
|
if (!lines[i].trim())
|
|
34
34
|
continue;
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
const msg = JSON.parse(lines[i]);
|
|
37
37
|
if (msg.type === "data") {
|
|
38
38
|
onData(msg.data);
|
|
39
39
|
}
|
|
@@ -64,19 +64,19 @@ export function createTerminal(cwd, onData, onExit) {
|
|
|
64
64
|
return termId;
|
|
65
65
|
}
|
|
66
66
|
export function writeToTerminal(termId, data) {
|
|
67
|
-
|
|
67
|
+
const worker = terminals.get(termId);
|
|
68
68
|
if (worker) {
|
|
69
69
|
worker.send({ type: "input", data: data });
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
export function resizeTerminal(termId, cols, rows) {
|
|
73
|
-
|
|
73
|
+
const worker = terminals.get(termId);
|
|
74
74
|
if (worker) {
|
|
75
75
|
worker.send({ type: "resize", cols: cols, rows: rows });
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
export function destroyTerminal(termId) {
|
|
79
|
-
|
|
79
|
+
const worker = terminals.get(termId);
|
|
80
80
|
if (worker) {
|
|
81
81
|
worker.send({ type: "kill" });
|
|
82
82
|
terminals.delete(termId);
|
|
@@ -7,7 +7,7 @@ import { execSync } from "node:child_process";
|
|
|
7
7
|
import { existsSync, unlinkSync, rmSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
|
|
10
|
+
let claudeExePath = null;
|
|
11
11
|
function getClaudeExecutablePath() {
|
|
12
12
|
if (claudeExePath)
|
|
13
13
|
return claudeExePath;
|
|
@@ -19,24 +19,24 @@ function getClaudeExecutablePath() {
|
|
|
19
19
|
}
|
|
20
20
|
return claudeExePath;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
const KNOWN_MODELS = [
|
|
23
23
|
{ value: "default", displayName: "Default" },
|
|
24
24
|
{ value: "opus", displayName: "Opus" },
|
|
25
25
|
{ value: "sonnet", displayName: "Sonnet" },
|
|
26
26
|
{ value: "haiku", displayName: "Haiku" },
|
|
27
27
|
];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
let warmupModels = [];
|
|
29
|
+
let warmupSlashCommands = [];
|
|
30
|
+
let warmupAccountInfo = null;
|
|
31
|
+
let warmupComplete = false;
|
|
32
|
+
const warmupRateLimits = new Map();
|
|
33
33
|
function ensureKnownModels(sdkModels) {
|
|
34
|
-
|
|
35
|
-
for (
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (let i = 0; i < sdkModels.length; i++) {
|
|
36
36
|
seen.add(sdkModels[i].value);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
for (
|
|
38
|
+
const result = sdkModels.slice();
|
|
39
|
+
for (let j = 0; j < KNOWN_MODELS.length; j++) {
|
|
40
40
|
if (!seen.has(KNOWN_MODELS[j].value)) {
|
|
41
41
|
result.push(KNOWN_MODELS[j]);
|
|
42
42
|
}
|
|
@@ -47,10 +47,10 @@ function deleteWarmupSession(cwd, sessionId) {
|
|
|
47
47
|
if (!sessionId)
|
|
48
48
|
return;
|
|
49
49
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const hash = cwd.replace(/\//g, "-");
|
|
51
|
+
const projectDir = join(homedir(), ".claude", "projects", hash);
|
|
52
|
+
const jsonlPath = join(projectDir, sessionId + ".jsonl");
|
|
53
|
+
const dirPath = join(projectDir, sessionId);
|
|
54
54
|
if (existsSync(jsonlPath)) {
|
|
55
55
|
unlinkSync(jsonlPath);
|
|
56
56
|
log.server("Deleted warmup session file: %s", sessionId);
|
|
@@ -66,10 +66,10 @@ function deleteWarmupSession(cwd, sessionId) {
|
|
|
66
66
|
export async function runWarmup(cwd) {
|
|
67
67
|
log.server("SDK warmup starting (cwd: %s)...", cwd);
|
|
68
68
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const ac = new AbortController();
|
|
70
|
+
let ended = false;
|
|
71
|
+
const WARMUP_SESSION_ID = "lattice-warmup";
|
|
72
|
+
const mq = {
|
|
73
73
|
[Symbol.asyncIterator]: function () {
|
|
74
74
|
return {
|
|
75
75
|
next: function () {
|
|
@@ -89,7 +89,7 @@ export async function runWarmup(cwd) {
|
|
|
89
89
|
};
|
|
90
90
|
},
|
|
91
91
|
};
|
|
92
|
-
|
|
92
|
+
const stream = query({
|
|
93
93
|
prompt: mq,
|
|
94
94
|
options: {
|
|
95
95
|
cwd,
|
|
@@ -103,15 +103,15 @@ export async function runWarmup(cwd) {
|
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
105
|
});
|
|
106
|
-
for await (
|
|
106
|
+
for await (const msg of stream) {
|
|
107
107
|
if (msg.type === "system") {
|
|
108
|
-
|
|
108
|
+
const sysMsg = msg;
|
|
109
109
|
if (sysMsg.subtype === "init") {
|
|
110
110
|
if (sysMsg.slash_commands) {
|
|
111
111
|
warmupSlashCommands = sysMsg.slash_commands;
|
|
112
112
|
}
|
|
113
113
|
try {
|
|
114
|
-
|
|
114
|
+
const models = await stream.supportedModels();
|
|
115
115
|
warmupModels = ensureKnownModels((models || []));
|
|
116
116
|
}
|
|
117
117
|
catch {
|
|
@@ -126,9 +126,9 @@ export async function runWarmup(cwd) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
if (msg.type === "rate_limit_event") {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
const rlMsg = msg;
|
|
130
|
+
const rli = rlMsg.rate_limit_info;
|
|
131
|
+
const cacheKey = rli.rateLimitType || "default";
|
|
132
132
|
warmupRateLimits.set(cacheKey, {
|
|
133
133
|
status: rli.status,
|
|
134
134
|
utilization: rli.utilization,
|
|
@@ -182,17 +182,17 @@ export async function runWarmup(cwd) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
async function warmupProjectData() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
const t0 = Date.now();
|
|
186
|
+
const config = loadConfig();
|
|
187
|
+
const projects = config.projects;
|
|
188
188
|
if (projects.length === 0)
|
|
189
189
|
return;
|
|
190
190
|
log.server("Data warmup: pre-caching %d project(s)...", projects.length);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
for (
|
|
191
|
+
let totalSessions = 0;
|
|
192
|
+
const recentSessionIds = [];
|
|
193
|
+
for (let i = 0; i < projects.length; i++) {
|
|
194
194
|
try {
|
|
195
|
-
|
|
195
|
+
const result = await listSessions(projects[i].slug, { limit: 40 });
|
|
196
196
|
totalSessions += result.sessions.length;
|
|
197
197
|
broadcast({
|
|
198
198
|
type: "session:list",
|
|
@@ -200,8 +200,8 @@ async function warmupProjectData() {
|
|
|
200
200
|
sessions: result.sessions,
|
|
201
201
|
totalCount: result.totalCount,
|
|
202
202
|
});
|
|
203
|
-
|
|
204
|
-
for (
|
|
203
|
+
const preWarmCount = Math.min(5, result.sessions.length);
|
|
204
|
+
for (let k = 0; k < preWarmCount; k++) {
|
|
205
205
|
recentSessionIds.push({
|
|
206
206
|
projectSlug: projects[i].slug,
|
|
207
207
|
sessionId: result.sessions[k].id,
|
|
@@ -211,7 +211,7 @@ async function warmupProjectData() {
|
|
|
211
211
|
catch { }
|
|
212
212
|
}
|
|
213
213
|
log.server("Data warmup: cached %d sessions across %d projects (%dms)", totalSessions, projects.length, Date.now() - t0);
|
|
214
|
-
for (
|
|
214
|
+
for (let j = 0; j < recentSessionIds.length; j++) {
|
|
215
215
|
try {
|
|
216
216
|
await loadSessionHistory(recentSessionIds[j].projectSlug, recentSessionIds[j].sessionId);
|
|
217
217
|
}
|
|
@@ -234,7 +234,7 @@ export function getWarmupRateLimits() {
|
|
|
234
234
|
return Array.from(warmupRateLimits.values());
|
|
235
235
|
}
|
|
236
236
|
export function cacheRateLimitEntry(entry) {
|
|
237
|
-
|
|
237
|
+
const key = entry.rateLimitType || "default";
|
|
238
238
|
warmupRateLimits.set(key, entry);
|
|
239
239
|
}
|
|
240
240
|
export function isWarmupComplete() {
|
package/dist/server/push.js
CHANGED
|
@@ -3,8 +3,8 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getLatticeHome } from "./config.js";
|
|
5
5
|
import { log } from "./logger.js";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
let vapidKeys = null;
|
|
7
|
+
const subscriptions = new Map();
|
|
8
8
|
function getVapidPath() {
|
|
9
9
|
return join(getLatticeHome(), "vapid.json");
|
|
10
10
|
}
|
|
@@ -14,7 +14,7 @@ function getSubscriptionsPath() {
|
|
|
14
14
|
function ensureVapidKeys() {
|
|
15
15
|
if (vapidKeys)
|
|
16
16
|
return vapidKeys;
|
|
17
|
-
|
|
17
|
+
const vapidPath = getVapidPath();
|
|
18
18
|
if (existsSync(vapidPath)) {
|
|
19
19
|
try {
|
|
20
20
|
vapidKeys = JSON.parse(readFileSync(vapidPath, "utf-8"));
|
|
@@ -23,12 +23,12 @@ function ensureVapidKeys() {
|
|
|
23
23
|
}
|
|
24
24
|
catch { }
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const generated = webpush.generateVAPIDKeys();
|
|
27
27
|
vapidKeys = {
|
|
28
28
|
publicKey: generated.publicKey,
|
|
29
29
|
privateKey: generated.privateKey,
|
|
30
30
|
};
|
|
31
|
-
|
|
31
|
+
const dir = getLatticeHome();
|
|
32
32
|
if (!existsSync(dir)) {
|
|
33
33
|
mkdirSync(dir, { recursive: true });
|
|
34
34
|
}
|
|
@@ -37,26 +37,26 @@ function ensureVapidKeys() {
|
|
|
37
37
|
return vapidKeys;
|
|
38
38
|
}
|
|
39
39
|
function loadSubscriptions() {
|
|
40
|
-
|
|
40
|
+
const subsPath = getSubscriptionsPath();
|
|
41
41
|
if (!existsSync(subsPath))
|
|
42
42
|
return;
|
|
43
43
|
try {
|
|
44
|
-
|
|
45
|
-
for (
|
|
44
|
+
const data = JSON.parse(readFileSync(subsPath, "utf-8"));
|
|
45
|
+
for (let i = 0; i < data.length; i++) {
|
|
46
46
|
subscriptions.set(data[i].endpoint, data[i]);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
catch { }
|
|
50
50
|
}
|
|
51
51
|
function saveSubscriptions() {
|
|
52
|
-
|
|
52
|
+
const arr = [];
|
|
53
53
|
subscriptions.forEach(function (sub) {
|
|
54
54
|
arr.push(sub);
|
|
55
55
|
});
|
|
56
56
|
writeFileSync(getSubscriptionsPath(), JSON.stringify(arr), "utf-8");
|
|
57
57
|
}
|
|
58
58
|
export function initPush() {
|
|
59
|
-
|
|
59
|
+
const keys = ensureVapidKeys();
|
|
60
60
|
webpush.setVapidDetails("mailto:lattice@localhost", keys.publicKey, keys.privateKey);
|
|
61
61
|
loadSubscriptions();
|
|
62
62
|
log.server("Push notifications ready (%d subscription(s))", subscriptions.size);
|
|
@@ -75,7 +75,7 @@ export function removePushSubscription(endpoint) {
|
|
|
75
75
|
export function sendPush(payload) {
|
|
76
76
|
if (subscriptions.size === 0)
|
|
77
77
|
return;
|
|
78
|
-
|
|
78
|
+
const json = JSON.stringify(payload);
|
|
79
79
|
subscriptions.forEach(function (sub, endpoint) {
|
|
80
80
|
webpush.sendNotification(sub, json).catch(function (err) {
|
|
81
81
|
if (err.statusCode === 410 || err.statusCode === 404 || err.statusCode === 403) {
|
package/dist/server/runtime.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const IS_NPM = true;
|
package/dist/server/tls.js
CHANGED
|
@@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { networkInterfaces } from "node:os";
|
|
5
5
|
import { getLatticeHome } from "./config.js";
|
|
6
6
|
export function getCertsDir() {
|
|
7
|
-
|
|
7
|
+
const certsDir = join(getLatticeHome(), "certs");
|
|
8
8
|
if (!existsSync(certsDir)) {
|
|
9
9
|
mkdirSync(certsDir, { recursive: true });
|
|
10
10
|
}
|
|
@@ -12,14 +12,14 @@ export function getCertsDir() {
|
|
|
12
12
|
}
|
|
13
13
|
function isCertExpiringSoon(certPath) {
|
|
14
14
|
try {
|
|
15
|
-
|
|
15
|
+
const result = spawnSync("openssl", ["x509", "-enddate", "-noout", "-in", certPath], { encoding: "utf-8" });
|
|
16
16
|
if (result.status !== 0)
|
|
17
17
|
return true;
|
|
18
|
-
|
|
18
|
+
const match = result.stdout.match(/notAfter=(.+)/);
|
|
19
19
|
if (!match)
|
|
20
20
|
return true;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const expiryDate = new Date(match[1]);
|
|
22
|
+
const daysLeft = (expiryDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24);
|
|
23
23
|
return daysLeft < 30;
|
|
24
24
|
}
|
|
25
25
|
catch {
|
|
@@ -27,28 +27,28 @@ function isCertExpiringSoon(certPath) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export function ensureCerts() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const certsDir = getCertsDir();
|
|
31
|
+
const certPath = join(certsDir, "cert.pem");
|
|
32
|
+
const keyPath = join(certsDir, "key.pem");
|
|
33
33
|
if (existsSync(certPath) && existsSync(keyPath) && !isCertExpiringSoon(certPath)) {
|
|
34
34
|
return { cert: certPath, key: keyPath };
|
|
35
35
|
}
|
|
36
36
|
console.log("[lattice] Generating self-signed TLS certificate...");
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (
|
|
40
|
-
|
|
37
|
+
const sans = ["DNS:lattice", "DNS:localhost", "IP:127.0.0.1", "IP:::1"];
|
|
38
|
+
const ifaces = networkInterfaces();
|
|
39
|
+
for (const name in ifaces) {
|
|
40
|
+
const addrs = ifaces[name];
|
|
41
41
|
if (!addrs)
|
|
42
42
|
continue;
|
|
43
|
-
for (
|
|
43
|
+
for (let i = 0; i < addrs.length; i++) {
|
|
44
44
|
if (!addrs[i].internal) {
|
|
45
45
|
sans.push("IP:" + addrs[i].address);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
const extFile = join(certsDir, "openssl-san.cnf");
|
|
50
50
|
writeFileSync(extFile, "[req]\ndistinguished_name=dn\nx509_extensions=v3\nprompt=no\n[dn]\nCN=lattice\n[v3]\nsubjectAltName=" + sans.join(",") + "\n");
|
|
51
|
-
|
|
51
|
+
const result = spawnSync("openssl", [
|
|
52
52
|
"req", "-x509",
|
|
53
53
|
"-newkey", "rsa:2048",
|
|
54
54
|
"-keyout", keyPath,
|
package/dist/server/tui.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getLatticeHome, loadConfig, saveConfig } from "./config.js";
|
|
5
5
|
import { DEFAULT_PORT } from "#shared";
|
|
6
|
-
|
|
6
|
+
const BANNER = `
|
|
7
7
|
\x1b[36m██╗\x1b[0m \x1b[36m█████╗\x1b[0m \x1b[36m████████╗████████╗██╗\x1b[0m \x1b[36m██████╗\x1b[0m \x1b[36m███████╗\x1b[0m
|
|
8
8
|
\x1b[36m██║\x1b[0m \x1b[36m██╔══██╗\x1b[0m\x1b[2m╚══\x1b[0m\x1b[36m██\x1b[0m\x1b[2m╔══╝╚══\x1b[0m\x1b[36m██\x1b[0m\x1b[2m╔══╝\x1b[0m\x1b[36m██║\x1b[0m\x1b[36m██╔════╝\x1b[0m\x1b[36m██╔════╝\x1b[0m
|
|
9
9
|
\x1b[36m██║\x1b[0m \x1b[36m███████║\x1b[0m \x1b[36m██║\x1b[0m \x1b[36m██║\x1b[0m \x1b[36m██║██║\x1b[0m \x1b[36m█████╗\x1b[0m
|
|
@@ -15,9 +15,9 @@ export function printBanner() {
|
|
|
15
15
|
console.log(BANNER);
|
|
16
16
|
}
|
|
17
17
|
export function printStatus(config, version, projectCount, sessionCount, tailscaleUrl) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const protocol = config.tls ? "https" : "http";
|
|
19
|
+
const url = protocol + "://localhost:" + config.port;
|
|
20
|
+
let lines = "lattice v" + version + " — " + url + "\n";
|
|
21
21
|
if (tailscaleUrl) {
|
|
22
22
|
lines += tailscaleUrl + "\n";
|
|
23
23
|
}
|
|
@@ -30,8 +30,8 @@ export function printStatus(config, version, projectCount, sessionCount, tailsca
|
|
|
30
30
|
}
|
|
31
31
|
export async function printQrCode(url) {
|
|
32
32
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const qrcode = await import("qrcode");
|
|
34
|
+
const qr = await qrcode.toString(url, {
|
|
35
35
|
type: "terminal",
|
|
36
36
|
small: true,
|
|
37
37
|
});
|
|
@@ -45,22 +45,22 @@ export async function printQrCode(url) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export async function runOnboarding() {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const configPath = join(getLatticeHome(), "config.json");
|
|
49
|
+
const isFirstRun = !existsSync(configPath);
|
|
50
50
|
if (!isFirstRun) {
|
|
51
|
-
|
|
51
|
+
const config = loadConfig();
|
|
52
52
|
return { port: config.port, passphrase: null };
|
|
53
53
|
}
|
|
54
54
|
printBanner();
|
|
55
55
|
p.intro("Welcome to Lattice");
|
|
56
56
|
p.note("Anyone with the URL gets full Claude Code access to this machine.\n" +
|
|
57
57
|
"Use a private network (Tailscale, VPN) or set a passphrase.", "Security");
|
|
58
|
-
|
|
58
|
+
const portResult = await p.text({
|
|
59
59
|
message: "Port",
|
|
60
60
|
placeholder: String(DEFAULT_PORT),
|
|
61
61
|
defaultValue: String(DEFAULT_PORT),
|
|
62
62
|
validate: function (val) {
|
|
63
|
-
|
|
63
|
+
const n = parseInt(val || "", 10);
|
|
64
64
|
if (isNaN(n) || n < 1 || n > 65535)
|
|
65
65
|
return "Enter a valid port (1-65535)";
|
|
66
66
|
return undefined;
|
|
@@ -70,17 +70,17 @@ export async function runOnboarding() {
|
|
|
70
70
|
p.cancel("Setup cancelled");
|
|
71
71
|
process.exit(0);
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const port = parseInt(portResult, 10);
|
|
74
|
+
const passphraseResult = await p.password({
|
|
75
75
|
message: "Passphrase (optional, press Enter to skip)",
|
|
76
76
|
});
|
|
77
77
|
if (p.isCancel(passphraseResult)) {
|
|
78
78
|
p.cancel("Setup cancelled");
|
|
79
79
|
process.exit(0);
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
const passphrase = passphraseResult || null;
|
|
82
82
|
p.outro("Setup complete");
|
|
83
|
-
|
|
83
|
+
const config = loadConfig();
|
|
84
84
|
config.port = port;
|
|
85
85
|
saveConfig(config);
|
|
86
86
|
return { port, passphrase };
|
|
@@ -2,17 +2,17 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { log } from "./logger.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const __dirname_local = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PKG_NAME = "@cryptiklemur/lattice";
|
|
7
|
+
const GITHUB_REPO = "cryptiklemur/lattice";
|
|
8
|
+
const CHECK_INTERVAL_MS = 3600000;
|
|
9
|
+
let cached = null;
|
|
10
|
+
let checking = false;
|
|
11
11
|
function getCurrentVersion() {
|
|
12
12
|
if (process.env.LATTICE_VERSION)
|
|
13
13
|
return process.env.LATTICE_VERSION;
|
|
14
14
|
try {
|
|
15
|
-
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(__dirname_local, "../../package.json"), "utf-8"));
|
|
16
16
|
return pkg.version || "0.0.0";
|
|
17
17
|
}
|
|
18
18
|
catch {
|
|
@@ -22,11 +22,11 @@ function getCurrentVersion() {
|
|
|
22
22
|
function compareVersions(a, b) {
|
|
23
23
|
if (!a || !b || typeof a !== "string" || typeof b !== "string")
|
|
24
24
|
return 0;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
for (
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
26
|
+
const pb = b.replace(/^v/, "").split(".").map(Number);
|
|
27
|
+
for (let i = 0; i < 3; i++) {
|
|
28
|
+
const va = pa[i] || 0;
|
|
29
|
+
const vb = pb[i] || 0;
|
|
30
30
|
if (va !== vb)
|
|
31
31
|
return va - vb;
|
|
32
32
|
}
|
|
@@ -36,7 +36,7 @@ export function getInstallMode() {
|
|
|
36
36
|
return "npm";
|
|
37
37
|
}
|
|
38
38
|
async function checkGitHub(currentVersion) {
|
|
39
|
-
|
|
39
|
+
const res = await fetch("https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest", {
|
|
40
40
|
headers: { "Accept": "application/vnd.github.v3+json" },
|
|
41
41
|
signal: AbortSignal.timeout(10000),
|
|
42
42
|
});
|
|
@@ -44,9 +44,9 @@ async function checkGitHub(currentVersion) {
|
|
|
44
44
|
console.error("[lattice] GitHub update check failed: HTTP " + res.status);
|
|
45
45
|
return { currentVersion, latestVersion: null, updateAvailable: false, lastCheckedAt: Date.now(), releaseUrl: null, installMode: "binary" };
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
const latestVersion = data.tag_name ? data.tag_name.replace(/^v/, "") : null;
|
|
49
|
+
const updateAvailable = latestVersion !== null && compareVersions(latestVersion, currentVersion) > 0;
|
|
50
50
|
return {
|
|
51
51
|
currentVersion,
|
|
52
52
|
latestVersion,
|
|
@@ -57,7 +57,7 @@ async function checkGitHub(currentVersion) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
async function checkNpm(currentVersion) {
|
|
60
|
-
|
|
60
|
+
const res = await fetch("https://registry.npmjs.org/" + PKG_NAME + "/latest", {
|
|
61
61
|
headers: { "Accept": "application/json" },
|
|
62
62
|
signal: AbortSignal.timeout(10000),
|
|
63
63
|
});
|
|
@@ -65,9 +65,9 @@ async function checkNpm(currentVersion) {
|
|
|
65
65
|
console.error("[lattice] npm update check failed: HTTP " + res.status);
|
|
66
66
|
return { currentVersion, latestVersion: null, updateAvailable: false, lastCheckedAt: Date.now(), releaseUrl: null, installMode: "npm" };
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const data = await res.json();
|
|
69
|
+
const latestVersion = data.version ?? null;
|
|
70
|
+
const updateAvailable = latestVersion !== null && compareVersions(latestVersion, currentVersion) > 0;
|
|
71
71
|
return {
|
|
72
72
|
currentVersion,
|
|
73
73
|
latestVersion,
|
|
@@ -78,7 +78,7 @@ async function checkNpm(currentVersion) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
export async function checkForUpdate(force = false) {
|
|
81
|
-
|
|
81
|
+
const currentVersion = getCurrentVersion();
|
|
82
82
|
if (!force && cached && Date.now() - cached.lastCheckedAt < CHECK_INTERVAL_MS) {
|
|
83
83
|
return cached;
|
|
84
84
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { log } from "../logger.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
const clients = new Map();
|
|
3
|
+
const clientAlive = new Map();
|
|
4
|
+
const clientProjects = new Map();
|
|
5
|
+
const virtualSendHandlers = new Map();
|
|
6
6
|
export function registerVirtualClient(id, handler) {
|
|
7
7
|
virtualSendHandlers.set(id, handler);
|
|
8
8
|
}
|
|
@@ -22,7 +22,7 @@ export function markClientAlive(id) {
|
|
|
22
22
|
clientAlive.set(id, true);
|
|
23
23
|
}
|
|
24
24
|
export function subscribeClientToProject(clientId, projectSlug) {
|
|
25
|
-
|
|
25
|
+
let projects = clientProjects.get(clientId);
|
|
26
26
|
if (!projects) {
|
|
27
27
|
projects = new Set();
|
|
28
28
|
clientProjects.set(clientId, projects);
|
|
@@ -30,10 +30,10 @@ export function subscribeClientToProject(clientId, projectSlug) {
|
|
|
30
30
|
projects.add(projectSlug);
|
|
31
31
|
}
|
|
32
32
|
export function broadcastToProject(projectSlug, message, excludeId) {
|
|
33
|
-
|
|
34
|
-
for (
|
|
33
|
+
const text = JSON.stringify(message);
|
|
34
|
+
for (const [id, ws] of clients) {
|
|
35
35
|
if (id !== excludeId && ws.readyState === ws.OPEN) {
|
|
36
|
-
|
|
36
|
+
const projects = clientProjects.get(id);
|
|
37
37
|
if (projects && projects.has(projectSlug)) {
|
|
38
38
|
ws.send(text);
|
|
39
39
|
}
|
|
@@ -41,20 +41,20 @@ export function broadcastToProject(projectSlug, message, excludeId) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
export function broadcast(message, excludeId) {
|
|
44
|
-
|
|
45
|
-
for (
|
|
44
|
+
const text = JSON.stringify(message);
|
|
45
|
+
for (const [id, ws] of clients) {
|
|
46
46
|
if (id !== excludeId && ws.readyState === ws.OPEN) {
|
|
47
47
|
ws.send(text);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
export function sendTo(id, message) {
|
|
52
|
-
|
|
52
|
+
const ws = clients.get(id);
|
|
53
53
|
if (ws && ws.readyState === ws.OPEN) {
|
|
54
54
|
ws.send(JSON.stringify(message));
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
const virtualHandler = virtualSendHandlers.get(id);
|
|
58
58
|
if (virtualHandler) {
|
|
59
59
|
virtualHandler(message);
|
|
60
60
|
return;
|
|
@@ -70,21 +70,21 @@ export function getClientCount() {
|
|
|
70
70
|
return clients.size;
|
|
71
71
|
}
|
|
72
72
|
export function closeAllClients() {
|
|
73
|
-
for (
|
|
73
|
+
for (const [, ws] of clients) {
|
|
74
74
|
ws.close();
|
|
75
75
|
}
|
|
76
76
|
clients.clear();
|
|
77
77
|
clientAlive.clear();
|
|
78
78
|
clientProjects.clear();
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
81
|
+
let heartbeatTimer = null;
|
|
82
82
|
export function startHeartbeat(onDead) {
|
|
83
83
|
if (heartbeatTimer)
|
|
84
84
|
return;
|
|
85
85
|
heartbeatTimer = setInterval(function () {
|
|
86
|
-
|
|
87
|
-
for (
|
|
86
|
+
const dead = [];
|
|
87
|
+
for (const [id, ws] of clients) {
|
|
88
88
|
if (!clientAlive.get(id)) {
|
|
89
89
|
dead.push([id, ws]);
|
|
90
90
|
continue;
|
|
@@ -94,7 +94,7 @@ export function startHeartbeat(onDead) {
|
|
|
94
94
|
ws.ping();
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
for (
|
|
97
|
+
for (let i = 0; i < dead.length; i++) {
|
|
98
98
|
log.ws("Heartbeat: client %s unresponsive, terminating", dead[i][0].slice(0, 8));
|
|
99
99
|
onDead(dead[i][0], dead[i][1]);
|
|
100
100
|
dead[i][1].terminate();
|