project-cypher 0.11.0 → 0.12.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 +61 -4
  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(); },
@@ -150,6 +196,15 @@ const cmds = {
150
196
  },
151
197
  remove(id) { writeRegistry(readRegistry().filter(x => x.id !== id)); console.log('removed ' + id); },
152
198
  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}`)); },
199
+ // Service control
200
+ 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'); },
201
+ 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).'); },
202
+ async stats() {
203
+ const c = load(); const r = await fetch(httpOf(c.server) + '/health').then(r => r.json()).catch(() => null);
204
+ 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}`);
205
+ try { const t = fs.readFileSync(LOG, 'utf8').trim().split('\n').slice(-3).join('\n'); if (t) console.log('\nrecent:\n' + t); } catch {}
206
+ },
207
+ logs() { try { console.log(fs.readFileSync(LOG, 'utf8').split('\n').slice(-40).join('\n')); } catch { console.log('no log yet'); } },
153
208
  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
209
  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
210
  connect() {
@@ -213,7 +268,9 @@ const help = () => console.log(`Project Cypher
213
268
  cypher add [folder] register a project folder (or just ask Root in the app)
214
269
  cypher projects | remove <id> list / remove projects
215
270
  cypher disconnect unlink this computer from the app
216
- cypher connect go online, run Claude Code when the app asks
271
+ cypher connect go online in this window (no background service)
272
+ cypher start | stop start / stop the background service (auto-starts at boot)
273
+ cypher stats | logs service + connection status / recent log
217
274
  cypher status | latest server check / latest app build
218
275
 
219
276
  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.12.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"