spect8-vault 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Spect8 + Vault
2
2
 
3
- Publication candidate — not yet published to npm. Requires Node 22.12 or newer.
3
+ Version 0.1.1 publication candidate. Requires Node 22.12 or newer.
4
4
 
5
5
  After publication:
6
6
 
@@ -9,7 +9,7 @@ npm install -g spect8-vault
9
9
  spect8-vault setup
10
10
  ```
11
11
 
12
- Setup runs Spect8's existing browser sign-in and IDE configuration, starts the bundled Vault engine, and asks for a device credential from https://www.spect8ai.tech/vault/workspace. Credentials stay in your local `.ctxlayer/spect8-vault.json` file. Do not share this file. Windows users should restrict access to their user profile; POSIX file permissions alone do not set Windows ACLs.
12
+ Setup opens Spect8 in your browser. Sign in once and choose **Authorize Spect8 + Vault**. Both local credentials are saved automatically, detected IDEs are configured, and the bundled Vault engine starts. No credential copying is needed. Setup requires the matching combined authorization page deployed on spect8ai.tech. Credentials stay in your local `.ctxlayer/spect8-vault.json` file. Do not share this file. Windows users should restrict access to their user profile; POSIX file permissions alone do not set Windows ACLs.
13
13
 
14
14
  Start a supported coding agent through the engine:
15
15
 
@@ -0,0 +1,100 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { execFile } from 'node:child_process';
4
+
5
+ // Credentials travel in a POST body, never in browser history or a callback URL.
6
+ export async function pair({ save, announce = console.log, open = openBrowser, timeoutMs = 300000 }) {
7
+ const state = randomBytes(32).toString('hex');
8
+ let busy = false;
9
+ let timer;
10
+ let server;
11
+ const result = new Promise((resolve, reject) => {
12
+ server = createServer(async (req, res) => {
13
+ res.setHeader('Cache-Control', 'no-store');
14
+ res.setHeader('Referrer-Policy', 'no-referrer');
15
+ // Browsers may redact Origin on HTTPS → loopback HTTP form navigation.
16
+ // The random one-time state is required even when Origin is trusted or null.
17
+ if (
18
+ req.method !== 'POST' ||
19
+ req.url !== '/callback' ||
20
+ !['https://www.spect8ai.tech', 'null'].includes(req.headers.origin)
21
+ ) {
22
+ res.writeHead(403);
23
+ res.end('Authorization rejected');
24
+ return;
25
+ }
26
+ if (busy) {
27
+ res.writeHead(409);
28
+ res.end('Already connecting');
29
+ return;
30
+ }
31
+ busy = true;
32
+ try {
33
+ let body = '';
34
+ for await (const chunk of req) {
35
+ body += chunk;
36
+ if (Buffer.byteLength(body) > 16384) throw new Error('Invalid authorization');
37
+ }
38
+ const fields = new URLSearchParams(body);
39
+ const key = fields.get('vaultKey');
40
+ const mcpKey = fields.get('key');
41
+ const email = fields.get('developerId');
42
+ if (
43
+ fields.get('state') !== state ||
44
+ !/^sv_[a-f0-9]{64}$/.test(key || '') ||
45
+ !mcpKey ||
46
+ mcpKey.length > 4096 ||
47
+ !email ||
48
+ email.length > 320
49
+ ) {
50
+ res.writeHead(400);
51
+ res.end('Invalid authorization');
52
+ busy = false;
53
+ return;
54
+ }
55
+ await save({ key, mcpKey, email, deviceId: fields.get('deviceId') });
56
+ res.writeHead(303, { Location: 'https://www.spect8ai.tech/vault/workspace' });
57
+ res.end();
58
+ clearTimeout(timer);
59
+ server.close();
60
+ resolve();
61
+ } catch {
62
+ res.writeHead(500);
63
+ res.end('Could not save device connection. Return to the terminal.');
64
+ clearTimeout(timer);
65
+ server.close();
66
+ reject(new Error('Could not save device authorization.'));
67
+ }
68
+ });
69
+ server.requestTimeout = 10000;
70
+ server.on('error', reject);
71
+ server.listen(0, '127.0.0.1', () => {
72
+ const url = new URL('https://www.spect8ai.tech/connect');
73
+ url.searchParams.set('integration', 'vault');
74
+ url.searchParams.set('state', state);
75
+ url.searchParams.set('callback', `http://127.0.0.1:${server.address().port}/callback`);
76
+ announce('Authorize Spect8 + Vault in your browser:\n' + url);
77
+ open(url.toString());
78
+ });
79
+ timer = setTimeout(() => {
80
+ server.closeAllConnections();
81
+ server.close();
82
+ reject(new Error('Authorization timed out. Run setup again.'));
83
+ }, timeoutMs);
84
+ });
85
+ try {
86
+ await result;
87
+ } finally {
88
+ clearTimeout(timer);
89
+ server.close();
90
+ }
91
+ }
92
+ function openBrowser(url) {
93
+ const [command, args] =
94
+ process.platform === 'darwin'
95
+ ? ['open', [url]]
96
+ : process.platform === 'win32'
97
+ ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
98
+ : ['xdg-open', [url]];
99
+ execFile(command, args, () => {}); // The terminal URL remains available if no browser launcher exists.
100
+ }
package/bin/spect8.mjs CHANGED
@@ -6,71 +6,123 @@ import { homedir } from 'node:os';
6
6
  import { join, dirname } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { createInterface } from 'node:readline/promises';
9
- import { Writable } from 'node:stream';
10
- const root=dirname(fileURLToPath(import.meta.url));
11
- const home=process.env.CTX_HOME || join(homedir(),'.ctxlayer');
12
- const configFile=join(home,'spect8-vault.json');
13
- const require=createRequire(import.meta.url);
14
- async function run(file,args=[]) {
15
- await new Promise((resolve,reject)=>{
16
- const child=spawn(process.execPath,[file,...args],{stdio:'inherit'});
17
- child.on('error',reject);child.on('exit',code=>code===0?resolve():reject(new Error(`Command exited with ${code}`)));
18
- });
9
+ import { pair } from './pairing.mjs';
10
+ import { pathToFileURL } from 'node:url';
11
+ const root = dirname(fileURLToPath(import.meta.url));
12
+ const home = process.env.CTX_HOME || join(homedir(), '.ctxlayer');
13
+ const configFile = join(home, 'spect8-vault.json');
14
+ const require = createRequire(import.meta.url);
15
+ async function run(file, args = []) {
16
+ await new Promise((resolve, reject) => {
17
+ const child = spawn(process.execPath, [file, ...args], { stdio: 'inherit' });
18
+ child.on('error', reject);
19
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`Command exited with ${code}`))));
20
+ });
19
21
  }
20
- const engine=(args)=>run(join(root,'ctx.js'),args);
22
+ const engine = (args) => run(join(root, 'ctx.js'), args);
21
23
  async function daemon() {
22
- const d=JSON.parse(await readFile(join(home,'daemon.json'),'utf8'));
23
- if(!Number.isInteger(d.port)||d.port<1||d.port>65535)throw new Error('Invalid local engine port');
24
- const r=await fetch(`http://127.0.0.1:${d.port}/api/threads`,{headers:{Authorization:`Bearer ${d.token}`},redirect:'error',signal:AbortSignal.timeout(5000)});
25
- if(!r.ok)throw new Error('Local engine unavailable. Run spect8-vault setup.');
26
- return r.json();
24
+ const d = JSON.parse(await readFile(join(home, 'daemon.json'), 'utf8'));
25
+ if (!Number.isInteger(d.port) || d.port < 1 || d.port > 65535) throw new Error('Invalid local engine port');
26
+ const r = await fetch(`http://127.0.0.1:${d.port}/api/threads`, {
27
+ headers: { Authorization: `Bearer ${d.token}` },
28
+ redirect: 'error',
29
+ signal: AbortSignal.timeout(5000),
30
+ });
31
+ if (!r.ok) throw new Error('Local engine unavailable. Run spect8-vault setup.');
32
+ return r.json();
27
33
  }
28
- async function main(){
29
- const [cmd,...args]=process.argv.slice(2);
30
- if(!cmd||['help','--help'].includes(cmd)) {
31
- console.log('Spect8 + Vault\n\n setup Connect Spect8 and start the Vault engine\n connect Save your Vault device credential locally\n sync Pick a session and sync until Ctrl+C\n doctor Diagnose both components\n run <agent> Launch an agent through Vault (e.g. run claude)\n engine ... Advanced local engine commands\n\nRequires Node 22.12+ and your coding agent installed separately.');return;
32
- }
33
- if(cmd==='setup'){
34
- await run(require.resolve('spect8-mcp/dist/cli.js'),['setup',...args]);
35
- await engine(['up']);await connect();
36
- console.log('Ready. Run spect8-vault run claude (or your supported agent), then spect8-vault sync in another terminal.');return;
37
- }
38
- if(cmd==='connect'){await connect();return;}
39
- if(cmd==='run'){if(!args.length)throw new Error('Choose an agent: spect8-vault run claude');await engine(['run',...args]);return;}
40
- if(cmd==='engine'){await engine(args);return;}
41
- if(cmd==='doctor'){await run(require.resolve('spect8-mcp/dist/cli.js'),['doctor']);await engine(['doctor']);await daemon();console.log('Local engine reachable.');return;}
42
- if(cmd==='sync'){
43
- const config=JSON.parse(await readFile(configFile,'utf8'));
44
- await engine(['up']);
45
- const data=await daemon();const threads=data.threads||[];
46
- if(!threads.length)throw new Error('No sessions yet. Start your agent with spect8-vault run, send a prompt, then retry sync.');
47
- const rl=createInterface({input:process.stdin,output:process.stdout});
48
- try{
49
- threads.forEach((t,i)=>console.log(`${i+1}. ${t.title||t.id}`));
50
- const choice=Number(await rl.question('Session number to sync: '));
51
- if(!Number.isInteger(choice)||!threads[choice-1])throw new Error('Invalid session number');
52
- console.log('Sync uploads this session’s conversation previews and context to your Spect8 account.');
53
- if((await rl.question('Start uploading this session? Type yes: ')).trim().toLowerCase()!=='yes')return;
54
- process.env.VAULT_CLOUD_URL='https://spect8-backend-1d0b24fd600a.herokuapp.com/api/v1/vault';
55
- process.env.VAULT_DEVICE_KEY=config.key;process.env.VAULT_THREAD_ID=threads[choice-1].id;process.env.VAULT_UPLOAD_CONTEXT='1';
56
- }finally{rl.close();}
57
- await import('./bridge.mjs');return;
58
- }
59
- throw new Error('Unknown command. Run spect8-vault --help.');
34
+ async function main() {
35
+ const [cmd, ...args] = process.argv.slice(2);
36
+ if (!cmd || ['help', '--help'].includes(cmd)) {
37
+ console.log(
38
+ 'Spect8 + Vault\n\n setup Connect Spect8 and start the Vault engine\n connect Authorize this computer for Spect8 + Vault\n sync Pick a session and sync until Ctrl+C\n doctor Diagnose both components\n run <agent> Launch an agent through Vault (e.g. run claude)\n engine ... Advanced local engine commands\n\nRequires Node 22.12+ and your coding agent installed separately.',
39
+ );
40
+ return;
41
+ }
42
+ if (cmd === 'setup') {
43
+ const { saveConfig } = await import(pathToFileURL(require.resolve('spect8-mcp/dist/config.js')));
44
+ await pair({
45
+ save: async ({ key, mcpKey, email, deviceId }) => {
46
+ await mkdir(home, { recursive: true, mode: 0o700 });
47
+ await writeFile(configFile, JSON.stringify({ key, deviceId }) + '\n', { mode: 0o600 });
48
+ await chmod(configFile, 0o600);
49
+ saveConfig({
50
+ ingest_hmac_key: mcpKey,
51
+ developer_id: email,
52
+ ingest_url: 'https://spect8-backend-1d0b24fd600a.herokuapp.com',
53
+ ide_name: 'other',
54
+ });
55
+ await chmod(join(homedir(), '.spect8/config.json'), 0o600);
56
+ },
57
+ });
58
+ const { detectInstalled, defaultInstallPaths, installTarget } = await import(
59
+ pathToFileURL(require.resolve('spect8-mcp/dist/install.js'))
60
+ );
61
+ for (const adapter of detectInstalled(defaultInstallPaths())) {
62
+ const result = installTarget(adapter.id);
63
+ for (const warning of result.warnings) console.log(warning);
64
+ console.log('Configured ' + adapter.label);
65
+ }
66
+ await engine(['up']);
67
+ await run(require.resolve('spect8-mcp/dist/cli.js'), ['doctor']);
68
+ console.log(
69
+ 'Ready. Run spect8-vault run claude (or your supported agent), then spect8-vault sync in another terminal.',
70
+ );
71
+ return;
72
+ }
73
+ if (cmd === 'connect') {
74
+ process.argv[2] = 'setup';
75
+ await main();
76
+ return;
77
+ }
78
+ if (cmd === 'run') {
79
+ if (!args.length) throw new Error('Choose an agent: spect8-vault run claude');
80
+ await engine(['run', ...args]);
81
+ return;
82
+ }
83
+ if (cmd === 'engine') {
84
+ await engine(args);
85
+ return;
86
+ }
87
+ if (cmd === 'doctor') {
88
+ await run(require.resolve('spect8-mcp/dist/cli.js'), ['doctor']);
89
+ await engine(['doctor']);
90
+ await daemon();
91
+ console.log('Local engine reachable.');
92
+ return;
93
+ }
94
+ if (cmd === 'sync') {
95
+ const config = JSON.parse(await readFile(configFile, 'utf8'));
96
+ await engine(['up']);
97
+ const data = await daemon();
98
+ const threads = data.threads || [];
99
+ if (!threads.length)
100
+ throw new Error(
101
+ 'No sessions yet. Start your agent with spect8-vault run, send a prompt, then retry sync.',
102
+ );
103
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
104
+ try {
105
+ threads.forEach((t, i) => console.log(`${i + 1}. ${t.title || t.id}`));
106
+ const choice = Number(await rl.question('Session number to sync: '));
107
+ if (!Number.isInteger(choice) || !threads[choice - 1]) throw new Error('Invalid session number');
108
+ console.log('Sync uploads this session’s conversation previews and context to your Spect8 account.');
109
+ if ((await rl.question('Start uploading this session? Type yes: ')).trim().toLowerCase() !== 'yes')
110
+ return;
111
+ process.env.VAULT_CLOUD_URL = 'https://spect8-backend-1d0b24fd600a.herokuapp.com/api/v1/vault';
112
+ process.env.VAULT_DEVICE_KEY = config.key;
113
+ process.env.VAULT_THREAD_ID = threads[choice - 1].id;
114
+ process.env.VAULT_UPLOAD_CONTEXT = '1';
115
+ } finally {
116
+ rl.close();
117
+ }
118
+ await import('./bridge.mjs');
119
+ return;
120
+ }
121
+ throw new Error('Unknown command. Run spect8-vault --help.');
60
122
  }
61
- async function connect(){
62
- if(!process.stdin.isTTY)throw new Error('Connect requires an interactive terminal.');
63
- console.log('Open https://www.spect8ai.tech/vault/workspace and create a device credential.');
64
- console.log('The credential is saved only on this computer. Input is hidden.');
65
- const hidden=new Writable({write(_chunk,_encoding,done){done();}});
66
- const rl=createInterface({input:process.stdin,output:hidden,terminal:true});
67
- try{
68
- process.stdout.write('Device credential: ');
69
- const key=(await rl.question('')).trim();process.stdout.write('\n');
70
- if(!key)throw new Error('A device credential is required.');
71
- await mkdir(home,{recursive:true,mode:0o700});
72
- await writeFile(configFile,JSON.stringify({key})+'\n',{mode:0o600});await chmod(configFile,0o600);
73
- console.log('Connected locally. Sync starts only when you select a session with spect8-vault sync.');
74
- }finally{rl.close();}
75
- }
76
- main().catch(e=>{console.error(`Spect8: ${e.code==='ENOENT'?'Setup is incomplete. Run spect8-vault setup.':e.message}`);process.exitCode=1;});
123
+ main().catch((e) => {
124
+ console.error(
125
+ `Spect8: ${e.code === 'ENOENT' ? 'Setup is incomplete. Run spect8-vault setup.' : e.message}`,
126
+ );
127
+ process.exitCode = 1;
128
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spect8-vault",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Spect8 onboarding and Vault local context engine in one installer.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",