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.
Files changed (2) hide show
  1. package/bin/cypher.js +74 -6
  2. 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('\nOnline soon. Keep this window open (Ctrl+C to stop). Next time just run: cypher connect');
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) { writeRegistry(readRegistry().filter(x => x.id !== id)); console.log('removed ' + 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 projects
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, run Claude Code when the app asks
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)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-cypher",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Connect your machine's Claude Code projects to the Project Cypher phone app.",
5
5
  "bin": {
6
6
  "cypher": "bin/cypher.js"