cli-setu 0.2.0 → 0.2.2

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/setu.js CHANGED
@@ -63,6 +63,11 @@ async function main() {
63
63
  await stop();
64
64
  return;
65
65
  }
66
+ case 'unpair': {
67
+ const { unpair } = require('../src/unpair');
68
+ await unpair(AUTH_SERVER);
69
+ return;
70
+ }
66
71
  default:
67
72
  console.error(`[cli-setu] Unknown command: ${firstArg}`);
68
73
  printHelp();
@@ -99,6 +104,7 @@ Usage:
99
104
  cli-setu pair Pair this computer with your phone, nothing else
100
105
  cli-setu status Show pairing status and any sessions running now
101
106
  cli-setu stop Stop a running session
107
+ cli-setu unpair Unpair this computer (asks for confirmation)
102
108
  cli-setu --version, -v Print the installed version
103
109
  cli-setu --update Check for and install the latest version
104
110
  cli-setu --help, -h Show this help`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-setu",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Bridge your terminal to your phone — run and approve Claude Code, Codex CLI and other AI coding agents from anywhere.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/pair.js CHANGED
@@ -11,8 +11,36 @@ async function pair(authServer, authServerPublic) {
11
11
  const config = loadOrCreateConfig();
12
12
 
13
13
  if (config.deviceToken) {
14
- console.log(`[cli-setu] "${config.deviceName}" is already paired.`);
15
- return config;
14
+ // Real bug found by testing: unpairing from the phone deletes the
15
+ // device server-side, but the laptop's local config.json still has
16
+ // the old (now-dead) token - this used to trust that local file
17
+ // blindly and declare itself "already paired" even when the server
18
+ // had no record of it at all. Confirm the saved token is actually
19
+ // still valid before trusting it; a dead token (401 - the server
20
+ // genuinely doesn't recognize it) falls through to a real re-pair.
21
+ // A network error/unreachable server is a THIRD, distinct outcome -
22
+ // deliberately NOT treated as "unpaired," since that would force an
23
+ // unwanted re-pair on every flaky-wifi run; it proceeds as if still
24
+ // paired instead, same as this check not existing at all.
25
+ let serverSaysUnpaired = false;
26
+ try {
27
+ const res = await fetch(`${authServer}/device/whoami`, {
28
+ headers: { Authorization: `Bearer ${config.deviceToken}` },
29
+ });
30
+ serverSaysUnpaired = res.status === 401;
31
+ } catch {
32
+ // Network error/unreachable - unknown, not "unpaired". Fall through
33
+ // to the "already paired" branch below, unchanged from before this
34
+ // check existed.
35
+ }
36
+
37
+ if (!serverSaysUnpaired) {
38
+ console.log(`[cli-setu] "${config.deviceName}" is already paired.`);
39
+ return config;
40
+ }
41
+
42
+ console.log(`[cli-setu] This device's pairing is no longer valid on the server (likely unpaired from the app) - re-pairing...`);
43
+ config.deviceToken = null;
16
44
  }
17
45
 
18
46
  console.log(`[cli-setu] Pairing as "${config.deviceName}"...`);
package/src/status.js CHANGED
@@ -26,6 +26,24 @@ async function status(authServer) {
26
26
  }
27
27
  console.log(`[cli-setu] Paired as "${config.deviceName}".`);
28
28
 
29
+ // Real gap found by testing with multiple real accounts across multiple
30
+ // laptops: there was no way to confirm which Google account a laptop was
31
+ // paired to without checking the phone app directly - the device token
32
+ // itself carries no human-readable identity. Best-effort: a network
33
+ // failure here shouldn't block the rest of `status` from reporting what
34
+ // it already knows locally.
35
+ try {
36
+ const whoamiRes = await fetch(`${authServer}/device/whoami`, {
37
+ headers: { Authorization: `Bearer ${config.deviceToken}` },
38
+ });
39
+ if (whoamiRes.ok) {
40
+ const { userId } = await whoamiRes.json();
41
+ console.log(`[cli-setu] Paired to account: ${userId}`);
42
+ }
43
+ } catch {
44
+ // Offline, or the server's unreachable - status still works from local state alone.
45
+ }
46
+
29
47
  let files;
30
48
  try {
31
49
  files = fs.readdirSync(CONFIG_DIR).filter((f) => f.startsWith('session.') && f.endsWith('.pid'));
package/src/supervisor.js CHANGED
@@ -236,6 +236,19 @@ async function connectToAzure() {
236
236
  log(`[supervisor] /device/connect rejected (403): ${err.error || 'no reason given'}`);
237
237
  process.exit(1);
238
238
  }
239
+ // Real bug found by testing: a 401 here means the server genuinely
240
+ // doesn't recognize this device token anymore (e.g. unpaired from the
241
+ // phone app) - this used to fall into the generic retry-every-3s path
242
+ // below, silently looping forever on a token that will never become
243
+ // valid again, with no indication to the user of what's actually
244
+ // wrong or what to do about it. Treated the same as 403 now: stop,
245
+ // say so clearly, point at the real fix.
246
+ if (res.status === 401) {
247
+ console.error(`\n[supervisor] This device is no longer paired (it may have been unpaired from the app).`);
248
+ console.error('[supervisor] Run `cli-setu pair` to pair it again.');
249
+ log(`[supervisor] /device/connect rejected (401): ${err.error || 'no reason given'}`);
250
+ process.exit(1);
251
+ }
239
252
  log(`[supervisor] /device/connect failed: ${err.error || res.status}`);
240
253
  setTimeout(connectToAzure, 3000);
241
254
  return;
@@ -243,6 +256,13 @@ async function connectToAzure() {
243
256
  const conn = await res.json();
244
257
  deviceId = conn.deviceId;
245
258
  log(`[supervisor] Connecting to Azure as device:${deviceId} (owner: ${conn.userId})...`);
259
+ // Diagnostic found by testing: a session not appearing in the phone's
260
+ // Terminal tab had no visible cause from the laptop's own side - this
261
+ // surfaces whether the backend's session.started broadcast (what
262
+ // actually makes a session show up before its first approval prompt)
263
+ // was even attempted and whether it succeeded, right here, instead of
264
+ // needing server log access to find out.
265
+ console.log(`[supervisor] session.started broadcast: ${conn.sessionBroadcast || '(not reported by this server version)'}`);
246
266
 
247
267
  ws = new WebSocket(conn.wssUrl);
248
268
 
package/src/unpair.js ADDED
@@ -0,0 +1,65 @@
1
+ // `cli-setu unpair` - lets this laptop remove itself from its paired
2
+ // account, without needing the phone app. Real gap found by testing:
3
+ // unpairing only ever existed from the phone's Devices tab - there was no
4
+ // way to do this from the laptop side at all, and no way to recover from
5
+ // a stale local pairing except manually deleting ~/.cli-setu/config.json
6
+ // by hand.
7
+ const readline = require('readline');
8
+ const { readConfig, writeConfig } = require('./config');
9
+
10
+ function confirm(question) {
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ return new Promise((resolve) => {
13
+ rl.question(question, (answer) => {
14
+ rl.close();
15
+ resolve(answer.trim().toLowerCase() === 'y');
16
+ });
17
+ });
18
+ }
19
+
20
+ async function unpair(authServer) {
21
+ const config = readConfig();
22
+ if (!config || !config.deviceToken) {
23
+ console.log('[cli-setu] Not paired - nothing to unpair.');
24
+ return;
25
+ }
26
+
27
+ const confirmed = await confirm(
28
+ `[cli-setu] This will unpair "${config.deviceName}" and stop it from connecting. Continue? (y/N) `
29
+ );
30
+ if (!confirmed) {
31
+ console.log('[cli-setu] Cancelled.');
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const res = await fetch(`${authServer}/device/unpair-self`, {
37
+ method: 'POST',
38
+ headers: { Authorization: `Bearer ${config.deviceToken}` },
39
+ });
40
+ // A 404 here means the server already has no record of this device
41
+ // (e.g. it was unpaired from the phone app already) - that's still a
42
+ // successful outcome from the user's point of view, so the local
43
+ // config is cleared the same way as a real 200.
44
+ if (!res.ok && res.status !== 404) {
45
+ const err = await res.json().catch(() => ({}));
46
+ console.error(`[cli-setu] Unpair failed: ${err.error || res.status}`);
47
+ console.error('[cli-setu] Local pairing left untouched - try again once the server is reachable.');
48
+ return;
49
+ }
50
+ } catch (err) {
51
+ console.error(`[cli-setu] Could not reach the server: ${err.message}`);
52
+ console.error('[cli-setu] Local pairing left untouched - try again once you\'re back online.');
53
+ return;
54
+ }
55
+
56
+ // Only clear the local file once the server confirms - never unpair
57
+ // locally while leaving a real, still-active device row behind on the
58
+ // server (that would orphan it with no way to remove it via the CLI,
59
+ // since cli-setu pair would need a device token this file no longer has).
60
+ config.deviceToken = null;
61
+ writeConfig(config);
62
+ console.log(`[cli-setu] ✓ Unpaired. Run \`cli-setu pair\` to pair this computer again.`);
63
+ }
64
+
65
+ module.exports = { unpair };