project-cypher 0.11.0 → 0.13.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/cypher.js +74 -6
- package/package.json +1 -1
package/bin/cypher.js
CHANGED
|
@@ -52,6 +52,52 @@ const httpOf = s => s.replace(/\/$/, ''), wsOf = s => httpOf(s).replace(/^http/,
|
|
|
52
52
|
|
|
53
53
|
const ask = q => new Promise(r => { const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout }); rl.question(q, a => { rl.close(); r(a.trim()); }); });
|
|
54
54
|
const devName = () => rawCfg().name || os.hostname();
|
|
55
|
+
|
|
56
|
+
// ── Background service (auto-start at boot/login, restarts on crash, waits for internet) ──
|
|
57
|
+
const LOG = path.join(CFG_DIR, 'agent.log'), SELF = [process.execPath, path.resolve(__filename), 'connect'];
|
|
58
|
+
const sh = (cmd, args) => require('child_process').spawnSync(cmd, args, { encoding: 'utf8' });
|
|
59
|
+
const SVC = {
|
|
60
|
+
kind: process.platform === 'darwin' ? 'launchd' : process.platform === 'win32' ? 'task' : 'systemd',
|
|
61
|
+
unit: path.join(os.homedir(), '.config/systemd/user/cypher.service'),
|
|
62
|
+
plist: path.join(os.homedir(), 'Library/LaunchAgents/com.cypher.agent.plist'),
|
|
63
|
+
install() {
|
|
64
|
+
fs.mkdirSync(CFG_DIR, { recursive: true });
|
|
65
|
+
if (this.kind === 'systemd') {
|
|
66
|
+
fs.mkdirSync(path.dirname(this.unit), { recursive: true });
|
|
67
|
+
fs.writeFileSync(this.unit, `[Unit]\nDescription=Project Cypher agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nExecStart=${SELF.map(x => `"${x}"`).join(' ')}\nRestart=on-failure\nRestartSec=5\nStandardOutput=append:${LOG}\nStandardError=append:${LOG}\n\n[Install]\nWantedBy=default.target\n`);
|
|
68
|
+
sh('systemctl', ['--user', 'daemon-reload']); sh('loginctl', ['enable-linger', os.userInfo().username]); // linger = start at boot even before login
|
|
69
|
+
return sh('systemctl', ['--user', 'enable', 'cypher.service']).status === 0;
|
|
70
|
+
}
|
|
71
|
+
if (this.kind === 'launchd') {
|
|
72
|
+
fs.mkdirSync(path.dirname(this.plist), { recursive: true });
|
|
73
|
+
fs.writeFileSync(this.plist, `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>com.cypher.agent</string><key>ProgramArguments</key><array>${SELF.map(x => `<string>${x}</string>`).join('')}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>StandardOutPath</key><string>${LOG}</string><key>StandardErrorPath</key><string>${LOG}</string></dict></plist>\n`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return sh('schtasks', ['/create', '/f', '/tn', 'ProjectCypher', '/sc', 'onlogon', '/rl', 'limited', '/tr', `cmd /c ""${SELF[0]}" "${SELF[1]}" connect >> "${LOG}" 2>&1"`]).status === 0;
|
|
77
|
+
},
|
|
78
|
+
start() {
|
|
79
|
+
if (!this.install()) return false;
|
|
80
|
+
if (this.kind === 'systemd') return sh('systemctl', ['--user', 'restart', 'cypher.service']).status === 0;
|
|
81
|
+
if (this.kind === 'launchd') { sh('launchctl', ['unload', this.plist]); return sh('launchctl', ['load', '-w', this.plist]).status === 0; }
|
|
82
|
+
return sh('schtasks', ['/run', '/tn', 'ProjectCypher']).status === 0;
|
|
83
|
+
},
|
|
84
|
+
stop() {
|
|
85
|
+
if (this.kind === 'systemd') { sh('systemctl', ['--user', 'stop', 'cypher.service']); return sh('systemctl', ['--user', 'disable', 'cypher.service']).status === 0; }
|
|
86
|
+
if (this.kind === 'launchd') return sh('launchctl', ['unload', '-w', this.plist]).status === 0;
|
|
87
|
+
sh('schtasks', ['/end', '/tn', 'ProjectCypher']); return sh('schtasks', ['/change', '/tn', 'ProjectCypher', '/disable']).status === 0;
|
|
88
|
+
},
|
|
89
|
+
remove() { this.stop(); try { fs.rmSync(this.kind === 'systemd' ? this.unit : this.plist, { force: true }); } catch {} if (this.kind === 'task') sh('schtasks', ['/delete', '/f', '/tn', 'ProjectCypher']); },
|
|
90
|
+
running() {
|
|
91
|
+
if (this.kind === 'systemd') return sh('systemctl', ['--user', 'is-active', 'cypher.service']).stdout.trim() === 'active';
|
|
92
|
+
if (this.kind === 'launchd') return sh('launchctl', ['list', 'com.cypher.agent']).status === 0;
|
|
93
|
+
return /Running/i.test(sh('schtasks', ['/query', '/tn', 'ProjectCypher']).stdout || '');
|
|
94
|
+
},
|
|
95
|
+
enabled() {
|
|
96
|
+
if (this.kind === 'systemd') return sh('systemctl', ['--user', 'is-enabled', 'cypher.service']).stdout.trim() === 'enabled';
|
|
97
|
+
if (this.kind === 'launchd') return fs.existsSync(this.plist);
|
|
98
|
+
return sh('schtasks', ['/query', '/tn', 'ProjectCypher']).status === 0;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
55
101
|
const yes = (a, d) => (a === '' ? d : /^y/i.test(a));
|
|
56
102
|
const has = (cmd, args = ['--version']) => { const r = require('child_process').spawnSync(cmd, args, { encoding: 'utf8' }); return r.error ? null : (r.stdout || r.stderr || '').trim().split('\n')[0]; };
|
|
57
103
|
async function doLink(server) {
|
|
@@ -76,7 +122,7 @@ const cmds = {
|
|
|
76
122
|
async disconnect() {
|
|
77
123
|
const c = load(); if (!c.token) return console.log('already disconnected.');
|
|
78
124
|
await fetch(httpOf(c.server) + '/unlink', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: c.token }) }).catch(() => null);
|
|
79
|
-
dropToken('disconnected. This computer is no longer linked to your app. Run cypher init to link again.');
|
|
125
|
+
SVC.remove(); dropToken('disconnected. This computer is no longer linked to your app. Run cypher init to link again.');
|
|
80
126
|
},
|
|
81
127
|
// First-run setup: checks Claude Code, reaches the server, shows the QR, adds this folder, goes online.
|
|
82
128
|
async init(...argv) {
|
|
@@ -109,8 +155,8 @@ const cmds = {
|
|
|
109
155
|
ws = ensureRoot(ws);
|
|
110
156
|
console.log(`\nRoot is ready. New projects will be created in ${ws}`);
|
|
111
157
|
console.log('Open Project Cypher and chat with Root: add existing folders, create projects, ask for status.');
|
|
112
|
-
console.log('\
|
|
113
|
-
cmds.connect();
|
|
158
|
+
if (SVC.start()) console.log('\nCypher now runs in the background and starts automatically whenever this computer is on and online.\nManage it with: cypher stats | cypher stop | cypher start');
|
|
159
|
+
else { console.log('\nCould not install the background service. Running in this window (Ctrl+C to stop). Next time: cypher connect'); cmds.connect(); }
|
|
114
160
|
},
|
|
115
161
|
// One command to link this computer to the phone: shows a QR + code, waits for the app, adds the current folder, goes online.
|
|
116
162
|
async link(server) { await doLink(server); cmds.add('.'); cmds.connect(); },
|
|
@@ -148,8 +194,27 @@ const cmds = {
|
|
|
148
194
|
let id = slug(p), n = 2; if (id === 'root') id = 'root-project'; while (list.some(x => x.id === id)) id = slug(p) + '-' + n++;
|
|
149
195
|
list.push({ id, name: path.basename(p), path: p, summary: '' }); writeRegistry(list); console.log(`added ${id} (${p})`);
|
|
150
196
|
},
|
|
151
|
-
remove(id) {
|
|
197
|
+
async remove(id, ...rest) {
|
|
198
|
+
if (id && id !== '--yes') { writeRegistry(readRegistry().filter(x => x.id !== id)); return console.log('removed project ' + id); }
|
|
199
|
+
console.log(`This fully removes Cypher from this computer:\n - unlinks it from your phone app\n - stops and deletes the background service\n - deletes ${CFG_DIR} (login, project list, Root notes, chat sessions)\nYour actual project folders are NOT touched.`);
|
|
200
|
+
if (id !== '--yes' && !rest.includes('--yes') && !yes(await ask('Continue? [y/N] '), false)) return console.log('cancelled.');
|
|
201
|
+
const c = rawCfg();
|
|
202
|
+
if (c.token) await fetch(httpOf(c.server || 'http://140.238.248.30:3300') + '/unlink', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: c.token }) }).then(() => console.log('[ok] unlinked from the app')).catch(() => console.log('[!!] could not reach the server; remove this computer from the app Projects list'));
|
|
203
|
+
SVC.remove(); console.log('[ok] background service removed');
|
|
204
|
+
fs.rmSync(CFG_DIR, { recursive: true, force: true }); console.log('[ok] deleted ' + CFG_DIR);
|
|
205
|
+
console.log('\nClean. To reinstall: npm i -g project-cypher@latest && cypher init\nTo also remove the program itself: npm uninstall -g project-cypher');
|
|
206
|
+
},
|
|
207
|
+
removeProject(id) { writeRegistry(readRegistry().filter(x => x.id !== id)); console.log('removed ' + id); },
|
|
152
208
|
projects() { const c = load(); if (!c.projects.length) return console.log('none yet. cypher add <folder>'); c.projects.forEach(p => console.log(`${p.id.padEnd(20)} ${p.path}`)); },
|
|
209
|
+
// Service control
|
|
210
|
+
start() { const c = load(); if (!c.token) return console.log('Not linked yet. Run: cypher init'); console.log(SVC.start() ? 'Cypher is running in the background and will start automatically when this computer turns on.' : 'Could not start the background service. Run it in this window instead: cypher connect'); },
|
|
211
|
+
stop() { console.log(SVC.stop() ? 'Cypher stopped and auto-start turned off. Run cypher start to bring it back.' : 'Nothing to stop (or the service is not installed).'); },
|
|
212
|
+
async stats() {
|
|
213
|
+
const c = load(); const r = await fetch(httpOf(c.server) + '/health').then(r => r.json()).catch(() => null);
|
|
214
|
+
console.log(`service: ${SVC.running() ? 'running' : 'stopped'}${SVC.enabled() ? ' (starts automatically)' : ' (auto-start off)'}\ninternet/server: ${r?.ok ? 'reachable' : 'unreachable'} ${c.server}\nlinked: ${c.token ? 'yes, as "' + devName() + '"' : 'no (run cypher init)'}\nprojects: ${c.projects.length}\nlog: ${LOG}`);
|
|
215
|
+
try { const t = fs.readFileSync(LOG, 'utf8').trim().split('\n').slice(-3).join('\n'); if (t) console.log('\nrecent:\n' + t); } catch {}
|
|
216
|
+
},
|
|
217
|
+
logs() { try { console.log(fs.readFileSync(LOG, 'utf8').split('\n').slice(-40).join('\n')); } catch { console.log('no log yet'); } },
|
|
153
218
|
async status() { const c = load(); const r = await fetch(httpOf(c.server) + '/health').then(r => r.json()).catch(() => null); console.log(`server ${c.server}: ${r?.ok ? 'online' : 'unreachable'}\ntoken: ${c.token ? 'set' : 'missing (cypher login)'}\nprojects: ${c.projects.length}`); },
|
|
154
219
|
async latest() { const c = load(); const l = await (await fetch(httpOf(c.server) + '/latest')).json(); console.log(`${l.versionName} (code ${l.versionCode})\n${l.apkUrl}`); },
|
|
155
220
|
connect() {
|
|
@@ -211,9 +276,12 @@ const help = () => console.log(`Project Cypher
|
|
|
211
276
|
cypher link [server-url] link this computer to your phone (QR or code), add this folder, go online
|
|
212
277
|
cypher pair <server-url> <code> alternative: pair with a code made in the app
|
|
213
278
|
cypher add [folder] register a project folder (or just ask Root in the app)
|
|
214
|
-
cypher projects | remove <id> list / remove
|
|
279
|
+
cypher projects | remove <id> list projects / remove one project
|
|
280
|
+
cypher remove FULL removal (unlink, service, all local data) for a clean reinstall
|
|
215
281
|
cypher disconnect unlink this computer from the app
|
|
216
|
-
cypher connect go online
|
|
282
|
+
cypher connect go online in this window (no background service)
|
|
283
|
+
cypher start | stop start / stop the background service (auto-starts at boot)
|
|
284
|
+
cypher stats | logs service + connection status / recent log
|
|
217
285
|
cypher status | latest server check / latest app build
|
|
218
286
|
|
|
219
287
|
Config: ~/.cypher/config.json (permissionMode defaults to acceptEdits)`);
|