lattice-install 0.1.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/bin/lattice-install.js +491 -0
- package/package.json +9 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const LATTICE_BASE_URL = 'https://latticeproxy.io';
|
|
14
|
+
const LATTICE_PROXY_URL = `${LATTICE_BASE_URL}/v1`;
|
|
15
|
+
const REPORT_URL = `${LATTICE_BASE_URL}/install/report`;
|
|
16
|
+
const DOCS_URL = `${LATTICE_BASE_URL}/docs`;
|
|
17
|
+
|
|
18
|
+
const HOME = os.homedir();
|
|
19
|
+
|
|
20
|
+
const PATHS = {
|
|
21
|
+
openclaw: path.join(HOME, '.openclaw', 'openclaw.json'),
|
|
22
|
+
continue: path.join(HOME, '.continue', 'config.json'),
|
|
23
|
+
aider: path.join(HOME, '.config', 'aider', 'config.yml'),
|
|
24
|
+
cursor: path.join(HOME, '.cursor', 'config.json'),
|
|
25
|
+
envFile: path.join(process.cwd(), '.env'),
|
|
26
|
+
bashrc: path.join(HOME, '.bashrc'),
|
|
27
|
+
zshrc: path.join(HOME, '.zshrc'),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ─── Help ─────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
33
|
+
console.log(`
|
|
34
|
+
lattice-install — Onboard to Lattice Network in 60 seconds
|
|
35
|
+
|
|
36
|
+
USAGE
|
|
37
|
+
lattice-install [options]
|
|
38
|
+
|
|
39
|
+
OPTIONS
|
|
40
|
+
--help, -h Show this help message
|
|
41
|
+
|
|
42
|
+
WHAT IT DOES
|
|
43
|
+
1. Detects installed AI coding tools (OpenClaw, Continue, Cursor, Aider)
|
|
44
|
+
2. Asks which AI provider you use (Anthropic default)
|
|
45
|
+
3. Collects your API key
|
|
46
|
+
4. Configures OpenClaw to route through Lattice proxy
|
|
47
|
+
5. Adds ANTHROPIC_BASE_URL / OPENAI_BASE_URL to your shell profile
|
|
48
|
+
6. Tests the connection
|
|
49
|
+
7. Reports telemetry to latticeproxy.io (fire-and-forget)
|
|
50
|
+
|
|
51
|
+
SUPPORTED TOOLS
|
|
52
|
+
✓ OpenClaw — config written automatically
|
|
53
|
+
✓ Shell profile (.bashrc / .zshrc) — env var written automatically
|
|
54
|
+
⚠ Continue — detected, manual config required
|
|
55
|
+
⚠ Cursor — detected, manual config required
|
|
56
|
+
⚠ Aider — detected, manual config required
|
|
57
|
+
|
|
58
|
+
DOCS
|
|
59
|
+
${DOCS_URL}
|
|
60
|
+
`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function atomicWrite(filePath, content) {
|
|
67
|
+
const dir = path.dirname(filePath);
|
|
68
|
+
const tmp = path.join(dir, `.lattice-tmp-${process.pid}-${Date.now()}`);
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
fs.writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
|
|
71
|
+
fs.renameSync(tmp, filePath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readJsonSafe(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
77
|
+
return JSON.parse(raw);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (e.code === 'ENOENT') return null;
|
|
80
|
+
if (e instanceof SyntaxError) {
|
|
81
|
+
console.error(` ⚠ Could not parse ${filePath}: ${e.message}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fileExists(p) {
|
|
89
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ask(rl, question) {
|
|
93
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function spin(label) {
|
|
97
|
+
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
98
|
+
let i = 0;
|
|
99
|
+
const id = setInterval(() => {
|
|
100
|
+
process.stdout.write(`\r${frames[i++ % frames.length]} ${label}`);
|
|
101
|
+
}, 80);
|
|
102
|
+
return {
|
|
103
|
+
stop(result) {
|
|
104
|
+
clearInterval(id);
|
|
105
|
+
process.stdout.write(`\r${result}\n`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function httpsGet(url, timeoutMs = 8000) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const req = https.get(url, { timeout: timeoutMs }, res => {
|
|
113
|
+
let body = '';
|
|
114
|
+
res.on('data', d => body += d);
|
|
115
|
+
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
116
|
+
});
|
|
117
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
118
|
+
req.on('error', reject);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function httpsPost(url, payload, timeoutMs = 6000) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const data = JSON.stringify(payload);
|
|
125
|
+
const u = new URL(url);
|
|
126
|
+
const options = {
|
|
127
|
+
hostname: u.hostname,
|
|
128
|
+
port: u.port || 443,
|
|
129
|
+
path: u.pathname,
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'Content-Length': Buffer.byteLength(data),
|
|
134
|
+
'User-Agent': 'lattice-install/0.1.0',
|
|
135
|
+
},
|
|
136
|
+
timeout: timeoutMs,
|
|
137
|
+
};
|
|
138
|
+
const req = https.request(options, res => {
|
|
139
|
+
let body = '';
|
|
140
|
+
res.on('data', d => body += d);
|
|
141
|
+
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
142
|
+
});
|
|
143
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
144
|
+
req.on('error', reject);
|
|
145
|
+
req.write(data);
|
|
146
|
+
req.end();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Detection ────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
function detectTools() {
|
|
153
|
+
const found = {
|
|
154
|
+
openclaw: false,
|
|
155
|
+
continue: false,
|
|
156
|
+
aider: false,
|
|
157
|
+
cursor: false,
|
|
158
|
+
hasEnvFile: false,
|
|
159
|
+
hasShellProfile: [],
|
|
160
|
+
existingKeys: [],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
found.openclaw = fileExists(PATHS.openclaw);
|
|
164
|
+
found.continue = fileExists(PATHS.continue);
|
|
165
|
+
found.aider = fileExists(PATHS.aider);
|
|
166
|
+
found.cursor = fileExists(PATHS.cursor);
|
|
167
|
+
found.hasEnvFile = fileExists(PATHS.envFile);
|
|
168
|
+
|
|
169
|
+
if (fileExists(PATHS.bashrc)) found.hasShellProfile.push('.bashrc');
|
|
170
|
+
if (fileExists(PATHS.zshrc)) found.hasShellProfile.push('.zshrc');
|
|
171
|
+
|
|
172
|
+
// Sniff for existing keys in env/shell files
|
|
173
|
+
const sniffPaths = [PATHS.envFile, PATHS.bashrc, PATHS.zshrc];
|
|
174
|
+
for (const p of sniffPaths) {
|
|
175
|
+
if (!fileExists(p)) continue;
|
|
176
|
+
try {
|
|
177
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
178
|
+
if (/ANTHROPIC_API_KEY\s*=\s*sk-ant-/.test(content)) {
|
|
179
|
+
const m = content.match(/ANTHROPIC_API_KEY\s*=\s*(sk-ant-[^\s"']+)/);
|
|
180
|
+
if (m) found.existingKeys.push({ provider: 'anthropic', key: m[1], source: p });
|
|
181
|
+
}
|
|
182
|
+
if (/OPENAI_API_KEY\s*=\s*sk-/.test(content)) {
|
|
183
|
+
const m = content.match(/OPENAI_API_KEY\s*=\s*(sk-[^\s"']+)/);
|
|
184
|
+
if (m) found.existingKeys.push({ provider: 'openai', key: m[1], source: p });
|
|
185
|
+
}
|
|
186
|
+
} catch { /* ignore read errors */ }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return found;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Configure OpenClaw ───────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function configureOpenclaw(apiKey) {
|
|
195
|
+
let config = readJsonSafe(PATHS.openclaw) || {};
|
|
196
|
+
|
|
197
|
+
// Ensure nested structure exists
|
|
198
|
+
config.models = config.models || {};
|
|
199
|
+
config.models.providers = config.models.providers || {};
|
|
200
|
+
config.agents = config.agents || {};
|
|
201
|
+
config.agents.defaults = config.agents.defaults || {};
|
|
202
|
+
config.agents.defaults.model = config.agents.defaults.model || {};
|
|
203
|
+
|
|
204
|
+
// Set lattice provider
|
|
205
|
+
config.models.providers.lattice = {
|
|
206
|
+
baseUrl: 'https://latticeproxy.io/v1',
|
|
207
|
+
apiKey: apiKey,
|
|
208
|
+
api: 'openai-completions',
|
|
209
|
+
models: [
|
|
210
|
+
{ id: 'lattice-sonnet', name: 'Claude Sonnet (via Lattice)', api: 'openai-completions', contextWindow: 200000, maxTokens: 16000 },
|
|
211
|
+
{ id: 'lattice-opus', name: 'Claude Opus (via Lattice)', api: 'openai-completions', contextWindow: 200000, maxTokens: 32000 },
|
|
212
|
+
{ id: 'lattice-haiku', name: 'Claude Haiku (via Lattice)', api: 'openai-completions', contextWindow: 200000, maxTokens: 8000 },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Set default model
|
|
217
|
+
config.agents.defaults.model.primary = 'lattice/lattice-sonnet';
|
|
218
|
+
|
|
219
|
+
atomicWrite(PATHS.openclaw, JSON.stringify(config, null, 2) + '\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Configure shell profile ──────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function appendShellExport(profilePath, lines) {
|
|
225
|
+
const marker = '# Added by lattice-install';
|
|
226
|
+
let existing = '';
|
|
227
|
+
try { existing = fs.readFileSync(profilePath, 'utf8'); } catch { /* new file */ }
|
|
228
|
+
|
|
229
|
+
if (existing.includes(marker)) {
|
|
230
|
+
// Replace the block
|
|
231
|
+
const block = `${marker}\n${lines.join('\n')}\n# End lattice-install\n`;
|
|
232
|
+
const replaced = existing.replace(
|
|
233
|
+
/# Added by lattice-install[\s\S]*?# End lattice-install\n?/,
|
|
234
|
+
block,
|
|
235
|
+
);
|
|
236
|
+
atomicWrite(profilePath, replaced);
|
|
237
|
+
} else {
|
|
238
|
+
const block = `\n${marker}\n${lines.join('\n')}\n# End lattice-install\n`;
|
|
239
|
+
atomicWrite(profilePath, existing + block);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function configureShellProfiles(apiKey, provider, proxyUrl, profiles) {
|
|
244
|
+
const lines = provider === 'anthropic'
|
|
245
|
+
? [
|
|
246
|
+
`export ANTHROPIC_API_KEY="${apiKey}"`,
|
|
247
|
+
`export ANTHROPIC_BASE_URL="${proxyUrl}"`,
|
|
248
|
+
]
|
|
249
|
+
: [
|
|
250
|
+
`export OPENAI_API_KEY="${apiKey}"`,
|
|
251
|
+
`export OPENAI_BASE_URL="${proxyUrl}"`,
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
for (const profile of profiles) {
|
|
255
|
+
const fullPath = profile === '.bashrc' ? PATHS.bashrc : PATHS.zshrc;
|
|
256
|
+
try {
|
|
257
|
+
appendShellExport(fullPath, lines);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (e.code === 'EACCES') {
|
|
260
|
+
console.error(` ⚠ Permission denied writing to ~/${profile} — skipping`);
|
|
261
|
+
} else {
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Test connection ──────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
async function testConnection(apiKey, provider, proxyUrl) {
|
|
271
|
+
const spinner = spin('Testing connection to Lattice proxy…');
|
|
272
|
+
try {
|
|
273
|
+
// Hit the proxy health/models endpoint
|
|
274
|
+
const testUrl = `${proxyUrl}/models`;
|
|
275
|
+
const u = new URL(testUrl);
|
|
276
|
+
const options = {
|
|
277
|
+
hostname: u.hostname,
|
|
278
|
+
port: u.port || 443,
|
|
279
|
+
path: u.pathname,
|
|
280
|
+
method: 'GET',
|
|
281
|
+
headers: {
|
|
282
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
283
|
+
'User-Agent': 'lattice-install/0.1.0',
|
|
284
|
+
},
|
|
285
|
+
timeout: 10000,
|
|
286
|
+
};
|
|
287
|
+
const result = await new Promise((resolve, reject) => {
|
|
288
|
+
const req = https.request(options, res => {
|
|
289
|
+
let body = '';
|
|
290
|
+
res.on('data', d => body += d);
|
|
291
|
+
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
292
|
+
});
|
|
293
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
294
|
+
req.on('error', reject);
|
|
295
|
+
req.end();
|
|
296
|
+
});
|
|
297
|
+
// Accept 200 or 401 (proxy reachable, auth may vary)
|
|
298
|
+
if (result.status < 500) {
|
|
299
|
+
spinner.stop('✓ Connection successful');
|
|
300
|
+
return true;
|
|
301
|
+
} else {
|
|
302
|
+
spinner.stop(`⚠ Proxy returned HTTP ${result.status} — check your key`);
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
} catch (e) {
|
|
306
|
+
spinner.stop(`⚠ Could not reach ${proxyUrl} (${e.message}) — check network`);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── Report telemetry ─────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
function fireAndForgetReport(payload) {
|
|
314
|
+
httpsPost(REPORT_URL, payload, 6000).catch(() => { /* silently ignore */ });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
async function main() {
|
|
320
|
+
// Step 1: Banner
|
|
321
|
+
console.log(`
|
|
322
|
+
╔══════════════════════════════════════════════════════╗
|
|
323
|
+
║ Lattice Network — Install & Onboard ║
|
|
324
|
+
║ Route your AI traffic through BFT consensus ║
|
|
325
|
+
╚══════════════════════════════════════════════════════╝
|
|
326
|
+
`);
|
|
327
|
+
|
|
328
|
+
// Step 2: Detect tools
|
|
329
|
+
console.log('Scanning for AI tools…\n');
|
|
330
|
+
const detected = detectTools();
|
|
331
|
+
|
|
332
|
+
const detectedList = [];
|
|
333
|
+
if (detected.openclaw) detectedList.push('OpenClaw');
|
|
334
|
+
if (detected.continue) detectedList.push('Continue');
|
|
335
|
+
if (detected.cursor) detectedList.push('Cursor');
|
|
336
|
+
if (detected.aider) detectedList.push('Aider');
|
|
337
|
+
if (detected.hasShellProfile.length) detectedList.push(`shell (${detected.hasShellProfile.join(', ')})`);
|
|
338
|
+
|
|
339
|
+
if (detectedList.length) {
|
|
340
|
+
console.log(' Detected: ' + detectedList.join(', '));
|
|
341
|
+
} else {
|
|
342
|
+
console.log(' No known AI tools detected — will configure shell profile');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Warn about Option-A tools
|
|
346
|
+
const optionATools = [];
|
|
347
|
+
if (detected.continue) {
|
|
348
|
+
console.log(`\n ⚠ Continue detected — manual config needed, see ${DOCS_URL}`);
|
|
349
|
+
optionATools.push('continue');
|
|
350
|
+
}
|
|
351
|
+
if (detected.cursor) {
|
|
352
|
+
console.log(`\n ⚠ Cursor detected — manual config needed, see ${DOCS_URL}`);
|
|
353
|
+
optionATools.push('cursor');
|
|
354
|
+
}
|
|
355
|
+
if (detected.aider) {
|
|
356
|
+
console.log(`\n ⚠ Aider detected — manual config needed, see ${DOCS_URL}`);
|
|
357
|
+
optionATools.push('aider');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log('');
|
|
361
|
+
|
|
362
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
// Step 3: Ask provider
|
|
366
|
+
const providerAnswer = await ask(rl,
|
|
367
|
+
'Which AI provider? [1] Anthropic (default) [2] OpenAI\n> '
|
|
368
|
+
);
|
|
369
|
+
const provider = providerAnswer.trim() === '2' ? 'openai' : 'anthropic';
|
|
370
|
+
const keyPrefix = provider === 'anthropic' ? 'sk-ant-' : 'sk-';
|
|
371
|
+
const keyHint = provider === 'anthropic' ? 'sk-ant-…' : 'sk-…';
|
|
372
|
+
const envVar = provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
|
|
373
|
+
const baseEnvVar = provider === 'anthropic' ? 'ANTHROPIC_BASE_URL' : 'OPENAI_BASE_URL';
|
|
374
|
+
const proxyUrl = LATTICE_PROXY_URL;
|
|
375
|
+
|
|
376
|
+
console.log(`\n Provider: ${provider === 'anthropic' ? 'Anthropic' : 'OpenAI'}`);
|
|
377
|
+
|
|
378
|
+
// Step 4: Ask for API key
|
|
379
|
+
let apiKey = '';
|
|
380
|
+
while (true) {
|
|
381
|
+
apiKey = (await ask(rl, `\nEnter your ${envVar} (${keyHint}):\n> `)).trim();
|
|
382
|
+
|
|
383
|
+
if (!apiKey) {
|
|
384
|
+
console.log(' API key cannot be empty.');
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// Validate format
|
|
388
|
+
if (provider === 'anthropic' && !apiKey.startsWith('sk-ant-')) {
|
|
389
|
+
console.log(' Invalid key format. Anthropic keys start with sk-ant-');
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (provider === 'openai' && !apiKey.startsWith('sk-')) {
|
|
393
|
+
console.log(' Invalid key format. OpenAI keys start with sk-');
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
rl.close();
|
|
400
|
+
|
|
401
|
+
// Step 5: Write config
|
|
402
|
+
console.log('\nConfiguring…\n');
|
|
403
|
+
|
|
404
|
+
if (detected.openclaw) {
|
|
405
|
+
try {
|
|
406
|
+
configureOpenclaw(apiKey);
|
|
407
|
+
console.log(' ✓ OpenClaw configured');
|
|
408
|
+
} catch (e) {
|
|
409
|
+
if (e.code === 'EACCES') {
|
|
410
|
+
console.error(` ⚠ Permission denied writing OpenClaw config — skipping`);
|
|
411
|
+
} else {
|
|
412
|
+
throw e;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Always write shell profile if any exist; if none found, create .bashrc
|
|
418
|
+
const profiles = detected.hasShellProfile.length
|
|
419
|
+
? detected.hasShellProfile
|
|
420
|
+
: ['.bashrc'];
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
configureShellProfiles(apiKey, provider, proxyUrl, profiles);
|
|
424
|
+
console.log(` ✓ Shell profile updated (${profiles.join(', ')})`);
|
|
425
|
+
console.log(` Added: ${envVar}, ${baseEnvVar}`);
|
|
426
|
+
} catch (e) {
|
|
427
|
+
if (e.code === 'EACCES') {
|
|
428
|
+
console.error(' ⚠ Permission denied writing shell profile');
|
|
429
|
+
} else {
|
|
430
|
+
throw e;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Step 6: Test connection
|
|
435
|
+
console.log('');
|
|
436
|
+
const connected = await testConnection(apiKey, provider, proxyUrl);
|
|
437
|
+
|
|
438
|
+
// Step 7: Fire-and-forget telemetry report
|
|
439
|
+
const reportPayload = {
|
|
440
|
+
version: '0.1.0',
|
|
441
|
+
provider,
|
|
442
|
+
tools: {
|
|
443
|
+
openclaw: detected.openclaw,
|
|
444
|
+
continue: detected.continue,
|
|
445
|
+
cursor: detected.cursor,
|
|
446
|
+
aider: detected.aider,
|
|
447
|
+
shellProfiles: detected.hasShellProfile,
|
|
448
|
+
unknown: optionATools,
|
|
449
|
+
},
|
|
450
|
+
connected,
|
|
451
|
+
platform: process.platform,
|
|
452
|
+
nodeVersion: process.version,
|
|
453
|
+
timestamp: new Date().toISOString(),
|
|
454
|
+
};
|
|
455
|
+
fireAndForgetReport(reportPayload);
|
|
456
|
+
|
|
457
|
+
// Step 8: Success message
|
|
458
|
+
console.log(`
|
|
459
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
460
|
+
|
|
461
|
+
✓ Lattice Network configured!
|
|
462
|
+
|
|
463
|
+
Your AI requests now route through Lattice's BFT
|
|
464
|
+
consensus network for verifiable, auditable inference.
|
|
465
|
+
|
|
466
|
+
Reload your shell to apply profile changes:
|
|
467
|
+
source ~/${profiles[0]}
|
|
468
|
+
|
|
469
|
+
Docs & dashboard: ${LATTICE_BASE_URL}
|
|
470
|
+
|
|
471
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
472
|
+
`);
|
|
473
|
+
|
|
474
|
+
process.exit(0);
|
|
475
|
+
|
|
476
|
+
} catch (e) {
|
|
477
|
+
rl.close();
|
|
478
|
+
if (e.code === 'ERR_USE_AFTER_CLOSE' || e.message === 'readline was closed') {
|
|
479
|
+
// User hit Ctrl+C
|
|
480
|
+
console.log('\n\nAborted.');
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
console.error(`\nFatal error: ${e.message}`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
main().catch(e => {
|
|
489
|
+
console.error(`\nFatal error: ${e.message}`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lattice-install",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Onboard to Lattice Network in 60 seconds",
|
|
5
|
+
"bin": {"lattice-install": "bin/lattice-install.js"},
|
|
6
|
+
"engines": {"node": ">=18"},
|
|
7
|
+
"keywords": ["lattice", "ai", "proxy", "anthropic", "openai", "bft"],
|
|
8
|
+
"license": "MIT"
|
|
9
|
+
}
|