cli-setu 0.1.0 → 0.2.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
@@ -11,29 +11,29 @@ npm install -g cli-setu
11
11
  ## Usage
12
12
 
13
13
  ```bash
14
- setu # bridges the current folder as a plain shell session
15
- setu --cli claude # same, but runs Claude Code inside the bridged session
16
- setu --cli codex # or Codex
14
+ cli-setu # bridges the current folder as a plain shell session
15
+ cli-setu --cli claude # same, but runs Claude Code inside the bridged session
16
+ cli-setu --cli codex # or Codex
17
17
  ```
18
18
 
19
- The first time you run it, `setu` prints a QR code (and an 8-digit fallback code) - scan it with the CLIसेतु app to pair this computer and start watching the session live.
19
+ The first time you run it, `cli-setu` prints a QR code (and an 8-digit fallback code) - scan it with the CLIसेतु app to pair this computer and start watching the session live.
20
20
 
21
- Once paired, running `setu` in any folder just works - no QR code needed again, unless this is a device you haven't paired before.
21
+ Once paired, running `cli-setu` in any folder just works - no QR code needed again, unless this is a device you haven't paired before.
22
22
 
23
23
  ## Other commands
24
24
 
25
25
  ```bash
26
- setu pair # pair this computer, without starting a session
27
- setu status # show pairing status and any sessions currently running
28
- setu stop # stop a running session
29
- setu --update # check for and install the latest version
30
- setu --version # print the installed version
31
- setu --help # show all commands
26
+ cli-setu pair # pair this computer, without starting a session
27
+ cli-setu status # show pairing status and any sessions currently running
28
+ cli-setu stop # stop a running session
29
+ cli-setu --update # check for and install the latest version
30
+ cli-setu --version # print the installed version
31
+ cli-setu --help # show all commands
32
32
  ```
33
33
 
34
34
  ## How it works
35
35
 
36
- `setu` spawns your real terminal (or Claude Code/Codex) inside a PTY, same as any terminal emulator. Every byte it produces streams live to the CLIसेतु app over a secure relay; anything you type on the phone writes back into that same PTY, exactly like a local keystroke. Permission prompts (file edits, shell commands, etc.) are detected and relayed as structured approve/reject cards instead of raw text, when the adapter for your CLI supports it.
36
+ `cli-setu` spawns your real terminal (or Claude Code/Codex) inside a PTY, same as any terminal emulator. Every byte it produces streams live to the CLIसेतु app over a secure relay; anything you type on the phone writes back into that same PTY, exactly like a local keystroke. Permission prompts (file edits, shell commands, etc.) are detected and relayed as structured approve/reject cards instead of raw text, when the adapter for your CLI supports it.
37
37
 
38
38
  ## License
39
39
 
package/bin/setu.js CHANGED
@@ -1,20 +1,26 @@
1
1
  #!/usr/bin/env node
2
- // Command dispatcher for the `setu` CLI. Kept deliberately thin - each
2
+ // Command dispatcher for the `cli-setu` CLI. Kept deliberately thin - each
3
3
  // real subcommand's logic lives in src/, this file only parses argv and
4
4
  // calls the right one.
5
- const AUTH_SERVER = process.env.CLI_SETU_SERVER || 'https://api.clisetu.com';
5
+ //
6
+ // TEMPORARY: defaults to the DEV server (dev-api.clisetu.com), not
7
+ // production, because only dev-api is live right now. MUST switch back to
8
+ // https://api.clisetu.com and republish before announcing this package to
9
+ // real users as "production ready" - see clisetuapp/backend's deployment
10
+ // guide for when production actually goes live.
11
+ const AUTH_SERVER = process.env.CLI_SETU_SERVER || 'https://dev-api.clisetu.com';
6
12
  const AUTH_SERVER_PUBLIC = process.env.CLI_SETU_SERVER_PUBLIC || AUTH_SERVER;
7
13
 
8
14
  // Real bug found by testing: this used to switch on process.argv[2]
9
15
  // directly against the subcommand list (pair/status/stop), with any
10
- // other value (including a real --flag, e.g. `setu --cli claude`)
16
+ // other value (including a real --flag, e.g. `cli-setu --cli claude`)
11
17
  // falling through to "Unknown command" and exiting immediately - the
12
18
  // whole multi-CLI feature (claude/codex/shell adapters) was unreachable
13
- // through the real `setu` command this entire time, only ever testable
19
+ // through the real `cli-setu` command this entire time, only ever testable
14
20
  // by calling node src/supervisor.js --cli claude directly. Flags
15
21
  // (anything starting with "-") are no longer mistaken for subcommand
16
- // names - they're checked first, and anything left over (bare `setu`, or
17
- // `setu --cli claude`/other flags meant for the bridged session itself)
22
+ // names - they're checked first, and anything left over (bare `cli-setu`, or
23
+ // `cli-setu --cli claude`/other flags meant for the bridged session itself)
18
24
  // falls through to the same "watch this session" path as before, which
19
25
  // still parses --cli on its own further down the line (supervisor.js) -
20
26
  // this dispatcher doesn't need to understand every flag supervisor.js
@@ -57,18 +63,23 @@ async function main() {
57
63
  await stop();
58
64
  return;
59
65
  }
66
+ case 'unpair': {
67
+ const { unpair } = require('../src/unpair');
68
+ await unpair(AUTH_SERVER);
69
+ return;
70
+ }
60
71
  default:
61
- console.error(`[setu] Unknown command: ${firstArg}`);
72
+ console.error(`[cli-setu] Unknown command: ${firstArg}`);
62
73
  printHelp();
63
74
  process.exit(1);
64
75
  }
65
76
  }
66
77
 
67
- // Bare `setu` (or `setu --cli <name>`, `setu --cli <name> <workingDir>`,
78
+ // Bare `cli-setu` (or `cli-setu --cli <name>`, `cli-setu --cli <name> <workingDir>`,
68
79
  // etc. - real flags meant for the bridged session itself, parsed by
69
80
  // supervisor.js) - watch the current directory as a session. Real
70
81
  // product requirement: a brand-new user should be able to run just
71
- // `setu` (no separate `setu pair` step to discover first) and see the
82
+ // `cli-setu` (no separate `cli-setu pair` step to discover first) and see the
72
83
  // QR/8-digit code immediately, the same "first run pairs AND starts
73
84
  // watching this session in one step" behavior the original
74
85
  // proof-of-concept had. Only pairs if not already paired - an
@@ -83,22 +94,23 @@ async function main() {
83
94
  }
84
95
 
85
96
  function printHelp() {
86
- console.log(`[setu] Bridge your terminal to your phone.
97
+ console.log(`[cli-setu] Bridge your terminal to your phone.
87
98
 
88
99
  Usage:
89
- setu Watch this folder as a session (pairs first if needed)
90
- setu --cli <name> Same, but run a specific CLI inside the bridged
100
+ cli-setu Watch this folder as a session (pairs first if needed)
101
+ cli-setu --cli <name> Same, but run a specific CLI inside the bridged
91
102
  session instead of the default plain shell
92
103
  (e.g. --cli claude, --cli codex)
93
- setu pair Pair this computer with your phone, nothing else
94
- setu status Show pairing status and any sessions running now
95
- setu stop Stop a running session
96
- setu --version, -v Print the installed version
97
- setu --update Check for and install the latest version
98
- setu --help, -h Show this help`);
104
+ cli-setu pair Pair this computer with your phone, nothing else
105
+ cli-setu status Show pairing status and any sessions running now
106
+ cli-setu stop Stop a running session
107
+ cli-setu unpair Unpair this computer (asks for confirmation)
108
+ cli-setu --version, -v Print the installed version
109
+ cli-setu --update Check for and install the latest version
110
+ cli-setu --help, -h Show this help`);
99
111
  }
100
112
 
101
113
  main().catch((err) => {
102
- console.error('[setu] Error:', err.message);
114
+ console.error('[cli-setu] Error:', err.message);
103
115
  process.exit(1);
104
116
  });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "cli-setu",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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": {
7
- "setu": "bin/setu.js"
7
+ "cli-setu": "bin/setu.js"
8
8
  },
9
9
  "main": "src/config.js",
10
10
  "engines": {
@@ -13,11 +13,11 @@ const KNOWN_ADAPTERS = {
13
13
  shell,
14
14
  };
15
15
 
16
- // Real product decision (confirmed directly by the PM): bare `setu` with
16
+ // Real product decision (confirmed directly by the PM): bare `cli-setu` with
17
17
  // no --cli flag bridges the user's own plain terminal, not Claude Code
18
18
  // specifically - the user decides what to run once it's open. Defaulting
19
19
  // to 'claude' here silently assumed every user wanted an AI CLI running
20
- // the instant they typed `setu`, which isn't actually the product's real
20
+ // the instant they typed `cli-setu`, which isn't actually the product's real
21
21
  // intent (a genuine remote terminal, not an AI-agent-only bridge).
22
22
  function getAdapter(cliName) {
23
23
  const normalized = (cliName || 'shell').toLowerCase();
@@ -1,8 +1,8 @@
1
- // The default adapter when a user runs plain `setu` with no --cli flag at
2
- // all - a real gap in the earlier design: `setu` (bare) always launched
1
+ // The default adapter when a user runs plain `cli-setu` with no --cli flag at
2
+ // all - a real gap in the earlier design: `cli-setu` (bare) always launched
3
3
  // Claude Code specifically, silently assuming that's what every user
4
4
  // wants. The actual product intent (confirmed directly by the PM) is
5
- // simpler and more honest: `setu` bridges the user's OWN real terminal to
5
+ // simpler and more honest: `cli-setu` bridges the user's OWN real terminal to
6
6
  // their phone - a genuine remote shell, nothing AI-specific - and the
7
7
  // user decides what to run inside it once it's open (`claude`, `codex`,
8
8
  // `npm install`, `git status`, anything). Running an AI CLI inside this
@@ -43,7 +43,7 @@ module.exports = {
43
43
  // INSIDE this shell, that CLI's own real adapter (claude.js/codex.js)
44
44
  // still isn't active here, since supervisor.js picks exactly one
45
45
  // adapter per process/session at startup - a real user wanting
46
- // structured approvals for a specific CLI should run `setu --cli
46
+ // structured approvals for a specific CLI should run `cli-setu --cli
47
47
  // claude` (or codex) directly, same as always. This adapter is
48
48
  // specifically for "just give me my terminal, I'll decide what to run."
49
49
  approvals: null,
package/src/config.js CHANGED
@@ -15,7 +15,7 @@ function defaultDeviceName() {
15
15
  return os.hostname() || 'My Computer';
16
16
  }
17
17
 
18
- // Saved once, on the first `setu pair`, so a later OS-level hostname
18
+ // Saved once, on the first `cli-setu pair`, so a later OS-level hostname
19
19
  // change (renaming the machine, moving to a new user profile) doesn't
20
20
  // silently rename an already-paired device out from under the user - the
21
21
  // Devices tab's identity for this machine stays whatever it was when it
@@ -33,7 +33,7 @@ function writeConfig(config) {
33
33
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
34
34
  }
35
35
 
36
- // Called by `setu pair` - creates the config the FIRST time, using the
36
+ // Called by `cli-setu pair` - creates the config the FIRST time, using the
37
37
  // auto-detected hostname as the default device name. A config that
38
38
  // already exists (this machine has paired before) is left untouched;
39
39
  // re-pairing intentionally reuses the same device identity rather than
package/src/hooks.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // that - every user's global install lives at a different path (varies by
5
5
  // OS, by nvm/volta version manager, by whether it's a global or local
6
6
  // install), and expecting someone to hand-edit JSON before their first
7
- // `setu` run defeats the entire point of "npm install -g cli-setu" being
7
+ // `cli-setu` run defeats the entire point of "npm install -g cli-setu" being
8
8
  // a real, ready-to-use install. This ensures the CURRENT project's
9
9
  // .claude/settings.json has the right hook registered, pointing at
10
10
  // wherever THIS running copy of cli-setu actually lives on disk right now
@@ -34,7 +34,7 @@ function alreadyRegistered(existingHooksForEvent) {
34
34
 
35
35
  // Ensures the CURRENT working directory's .claude/settings.json has both
36
36
  // hooks pointed at this package's real install location. Called once at
37
- // the start of every `setu` (bare) run - cheap (a few small file reads/
37
+ // the start of every `cli-setu` (bare) run - cheap (a few small file reads/
38
38
  // writes), and self-healing if the package is ever moved/reinstalled at a
39
39
  // different path (a stale command string gets overwritten with today's
40
40
  // real path automatically, no manual fix needed).
@@ -48,8 +48,8 @@ function ensureHooksRegistered(projectDir) {
48
48
  } catch {
49
49
  // No settings.json yet, or it's not valid JSON - start fresh rather
50
50
  // than crash; an unreadable existing file is left untouched below by
51
- // simply not being parsed further, but we still want `setu` itself to
52
- // keep working for this session's approval detection.
51
+ // simply not being parsed further, but we still want `cli-setu` itself
52
+ // to keep working for this session's approval detection.
53
53
  }
54
54
 
55
55
  settings.hooks = settings.hooks || {};
@@ -73,7 +73,7 @@ function ensureHooksRegistered(projectDir) {
73
73
  if (changed) {
74
74
  fs.mkdirSync(claudeDir, { recursive: true });
75
75
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
76
- console.log('[setu] Registered the approval-detection hook in .claude/settings.json');
76
+ console.log('[cli-setu] Registered the approval-detection hook in .claude/settings.json');
77
77
  }
78
78
  }
79
79
 
package/src/pair.js CHANGED
@@ -11,18 +11,47 @@ async function pair(authServer, authServerPublic) {
11
11
  const config = loadOrCreateConfig();
12
12
 
13
13
  if (config.deviceToken) {
14
- console.log(`[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
- console.log(`[setu] Pairing as "${config.deviceName}"...`);
19
- console.log('[setu] (This is your computer\'s name. Rename it in ~/.cli-setu/config.json before pairing if you want something else.)');
46
+ console.log(`[cli-setu] Pairing as "${config.deviceName}"...`);
47
+ console.log('[cli-setu] (This is your computer\'s name. Rename it in ~/.cli-setu/config.json before pairing if you want something else.)');
20
48
 
21
49
  // qr-start also needs a sessionId up front (mirrors supervisor-qr.js's
22
50
  // "the QR both pairs it AND jumps the phone straight to watching THIS
23
- // session" behavior) - 'pairing' is a placeholder id here since `setu
24
- // pair` itself never streams a terminal; the real per-run session id is
25
- // generated fresh by supervisor.js on every `setu` (bare) invocation.
51
+ // session" behavior) - 'pairing' is a placeholder id here since
52
+ // `cli-setu pair` itself never streams a terminal; the real per-run
53
+ // session id is generated fresh by supervisor.js on every `cli-setu`
54
+ // (bare) invocation.
26
55
  const startRes = await fetch(`${authServer}/device/pair/qr-start`, {
27
56
  method: 'POST',
28
57
  headers: { 'Content-Type': 'application/json' },
@@ -34,8 +63,8 @@ async function pair(authServer, authServerPublic) {
34
63
 
35
64
  console.log('');
36
65
  qrcode.generate(scanUrl, { small: true });
37
- console.log('[setu] Scan this with your phone camera.');
38
- console.log(`[setu] (Testing on one PC? Paste this URL into a browser tab instead: ${scanUrl})`);
66
+ console.log('[cli-setu] Scan this with your phone camera.');
67
+ console.log(`[cli-setu] (Testing on one PC? Paste this URL into a browser tab instead: ${scanUrl})`);
39
68
 
40
69
  const codeRes = await fetch(`${authServer}/device/pair/start`, {
41
70
  method: 'POST',
@@ -43,8 +72,8 @@ async function pair(authServer, authServerPublic) {
43
72
  body: JSON.stringify({ deviceName: config.deviceName }),
44
73
  });
45
74
  const { code: typedCode, pairId: typedPairId } = await codeRes.json();
46
- console.log(`[setu] Or type this code in the app's Devices tab: ${typedCode}`);
47
- console.log('[setu] Waiting for scan or code entry...');
75
+ console.log(`[cli-setu] Or type this code in the app's Devices tab: ${typedCode}`);
76
+ console.log('[cli-setu] Waiting for scan or code entry...');
48
77
 
49
78
  let confirmed = false;
50
79
  while (!confirmed) {
@@ -56,13 +85,13 @@ async function pair(authServer, authServerPublic) {
56
85
  confirmed = true;
57
86
  config.deviceToken = status.deviceToken;
58
87
  writeConfig(config);
59
- console.log(`[setu] ✓ Paired via QR and saved to ~/.cli-setu/config.json`);
88
+ console.log(`[cli-setu] ✓ Paired via QR and saved to ~/.cli-setu/config.json`);
60
89
  break;
61
90
  } else if (status.status === 'EXPIRED') {
62
- console.error('[setu] QR code expired. Run `setu pair` again to get a new one.');
91
+ console.error('[cli-setu] QR code expired. Run `cli-setu pair` again to get a new one.');
63
92
  process.exit(1);
64
93
  } else if (status.status === 'REJECTED') {
65
- console.error(`[setu] Pairing rejected: ${status.rejectReason || 'the server declined this pairing.'}`);
94
+ console.error(`[cli-setu] Pairing rejected: ${status.rejectReason || 'the server declined this pairing.'}`);
66
95
  process.exit(1);
67
96
  }
68
97
 
@@ -72,7 +101,7 @@ async function pair(authServer, authServerPublic) {
72
101
  confirmed = true;
73
102
  config.deviceToken = typedStatus.deviceToken;
74
103
  writeConfig(config);
75
- console.log(`[setu] ✓ Paired via typed code and saved to ~/.cli-setu/config.json`);
104
+ console.log(`[cli-setu] ✓ Paired via typed code and saved to ~/.cli-setu/config.json`);
76
105
  break;
77
106
  }
78
107
  }
package/src/status.js CHANGED
@@ -1,5 +1,5 @@
1
- // `setu status` - reports whether this computer is paired, and which
2
- // sessions (if any) currently have a `setu` process running, using the
1
+ // `cli-setu status` - reports whether this computer is paired, and which
2
+ // sessions (if any) currently have a `cli-setu` process running, using the
3
3
  // PID files supervisor.js writes/removes around its own lifetime (see
4
4
  // its own comment on why - there was no way to answer this question at
5
5
  // all before those existed).
@@ -21,10 +21,28 @@ function isProcessAlive(pid) {
21
21
  async function status(authServer) {
22
22
  const config = readConfig();
23
23
  if (!config || !config.deviceToken) {
24
- console.log('[setu] Not paired. Run `setu pair` to get started.');
24
+ console.log('[cli-setu] Not paired. Run `cli-setu pair` to get started.');
25
25
  return;
26
26
  }
27
- console.log(`[setu] Paired as "${config.deviceName}".`);
27
+ console.log(`[cli-setu] Paired as "${config.deviceName}".`);
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
+ }
28
46
 
29
47
  let files;
30
48
  try {
@@ -52,11 +70,11 @@ async function status(authServer) {
52
70
  }
53
71
 
54
72
  if (running.length === 0) {
55
- console.log('[setu] No sessions currently running. Run `setu` in a project folder to start one.');
73
+ console.log('[cli-setu] No sessions currently running. Run `cli-setu` in a project folder to start one.');
56
74
  return;
57
75
  }
58
76
 
59
- console.log(`[setu] ${running.length} session(s) running:`);
77
+ console.log(`[cli-setu] ${running.length} session(s) running:`);
60
78
  for (const s of running) {
61
79
  console.log(` - ${s.sessionId} · ${s.cwd} · started ${s.startedAt} (pid ${s.pid})`);
62
80
  }
package/src/stop.js CHANGED
@@ -1,4 +1,4 @@
1
- // `setu stop` - cleanly disconnects every session currently running on
1
+ // `cli-setu stop` - cleanly disconnects every session currently running on
2
2
  // this computer. Real gap in the PoC this replaces: the only way to stop
3
3
  // a supervisor was closing its terminal window (or Ctrl+C, which only
4
4
  // reaches the one you're actually looking at) - this reaches every
@@ -15,7 +15,7 @@ async function stop() {
15
15
  }
16
16
 
17
17
  if (files.length === 0) {
18
- console.log('[setu] No sessions currently running.');
18
+ console.log('[cli-setu] No sessions currently running.');
19
19
  return;
20
20
  }
21
21
 
@@ -35,7 +35,7 @@ async function stop() {
35
35
  // PID file. A hard kill would skip all of that and leave the CLI
36
36
  // process and the Azure connection to time out on their own.
37
37
  process.kill(info.pid, 'SIGINT');
38
- console.log(`[setu] Stopped ${info.sessionId} (${info.cwd})`);
38
+ console.log(`[cli-setu] Stopped ${info.sessionId} (${info.cwd})`);
39
39
  stopped++;
40
40
  } catch {
41
41
  // Already dead - just clean up the stale file.
@@ -44,7 +44,7 @@ async function stop() {
44
44
  }
45
45
 
46
46
  if (stopped === 0) {
47
- console.log('[setu] Sessions were already stopped (stale state cleaned up).');
47
+ console.log('[cli-setu] Sessions were already stopped (stale state cleaned up).');
48
48
  }
49
49
  }
50
50
 
package/src/supervisor.js CHANGED
@@ -74,24 +74,27 @@ function log(...args) {
74
74
  }
75
75
 
76
76
  // Real config, from pairing (src/pair.js, either via bin/setu.js's own
77
- // bare-`setu` pairing check or the explicit `setu pair` command) - not
78
- // argv. bin/setu.js already guarantees this exists before requiring this
79
- // file for a normal `setu` run; this is a defensive fallback for anyone
80
- // requiring supervisor.js directly, so a null deviceToken fails with a
81
- // clear message instead of a confusing crash deep in connectToAzure().
77
+ // bare-`cli-setu` pairing check or the explicit `cli-setu pair` command) -
78
+ // not argv. bin/setu.js already guarantees this exists before requiring
79
+ // this file for a normal `cli-setu` run; this is a defensive fallback for
80
+ // anyone requiring supervisor.js directly, so a null deviceToken fails
81
+ // with a clear message instead of a confusing crash deep in connectToAzure().
82
82
  const config = readConfig();
83
83
  if (!config || !config.deviceToken) {
84
- console.error('[setu] This computer isn\'t paired yet. Run `setu` again, or run `setu pair` directly.');
84
+ console.error('[cli-setu] This computer isn\'t paired yet. Run `cli-setu` again, or run `cli-setu pair` directly.');
85
85
  process.exit(1);
86
86
  }
87
87
  const DEVICE_NAME = config.deviceName;
88
88
 
89
- const AUTH_SERVER = process.env.CLI_SETU_SERVER || 'https://api.clisetu.com';
89
+ // TEMPORARY: defaults to the DEV server, not production - see bin/setu.js's
90
+ // matching comment. Must switch back to https://api.clisetu.com before
91
+ // treating this package as production-ready for real users.
92
+ const AUTH_SERVER = process.env.CLI_SETU_SERVER || 'https://dev-api.clisetu.com';
90
93
 
91
- // --cli still supported, e.g. `setu --cli codex`, for the same reason it
94
+ // --cli still supported, e.g. `cli-setu --cli codex`, for the same reason it
92
95
  // existed before: one paired device can run more than one kind of CLI
93
96
  // across different sessions/folders. Real product decision (confirmed
94
- // directly by the PM): bare `setu` with no --cli flag bridges the user's
97
+ // directly by the PM): bare `cli-setu` with no --cli flag bridges the user's
95
98
  // own plain terminal (see cli-adapters/shell.js), not Claude Code by
96
99
  // default - the user decides what to run once it's open, same as
97
100
  // getAdapter's own default (index.js) - kept in sync here rather than
@@ -105,7 +108,7 @@ const WORKING_DIR = process.cwd();
105
108
  // plus the real working directory (not just the folder's basename, which
106
109
  // collides the moment two different projects share a folder name like
107
110
  // "app" on the same machine) plus a short random suffix so re-running
108
- // `setu` in the same folder twice at once (e.g. two terminal tabs) never
111
+ // `cli-setu` in the same folder twice at once (e.g. two terminal tabs) never
109
112
  // silently shares one session.
110
113
  const dirHash = crypto.createHash('sha1').update(WORKING_DIR).digest('hex').slice(0, 8);
111
114
  const SESSION_ID = `${os.hostname()}-${dirHash}-${crypto.randomBytes(3).toString('hex')}`;
@@ -121,7 +124,7 @@ console.log(`[supervisor] Using CLI adapter: ${adapter.displayName} (${adapter.i
121
124
  log(`Using CLI adapter: ${adapter.displayName} (${adapter.id})`);
122
125
 
123
126
  // Real bug found by testing: when the spawned adapter is 'shell' (the
124
- // bare-`setu` default - see cli-adapters/shell.js), the user is expected
127
+ // bare-`cli-setu` default - see cli-adapters/shell.js), the user is expected
125
128
  // to freely run `claude` INSIDE that bridged shell whenever they want to.
126
129
  // Gating hook registration on "is the spawn-time adapter Claude" (as this
127
130
  // used to) meant Claude Code's hooks never got installed into
@@ -141,21 +144,21 @@ const PROCESSED_FILE = path.join(CONFIG_DIR, `processed-requests.${SESSION_ID}.j
141
144
  const POLL_INTERVAL_MS = 300;
142
145
 
143
146
  // Always registered now (see QUEUE_FILE's comment above) - covers both
144
- // `setu --cli claude` (adapter itself is Claude) and bare `setu` /
145
- // `setu --cli shell` followed by the user manually running `claude`
147
+ // `cli-setu --cli claude` (adapter itself is Claude) and bare `cli-setu` /
148
+ // `cli-setu --cli shell` followed by the user manually running `claude`
146
149
  // inside the bridged shell.
147
150
  {
148
151
  const { ensureHooksRegistered } = require('./hooks');
149
152
  ensureHooksRegistered(WORKING_DIR);
150
153
  }
151
154
 
152
- // Real config, from `setu pair` (src/config.js) - not a per-device-name
153
- // token file. Pairing is now a separate, one-time command (`setu pair`)
155
+ // Real config, from `cli-setu pair` (src/config.js) - not a per-device-name
156
+ // token file. Pairing is now a separate, one-time command (`cli-setu pair`)
154
157
  // that must already have succeeded before this ever runs (checked above,
155
158
  // where the process exits early if config.deviceToken is missing) - so
156
159
  // unlike the PoC's ensurePaired(), there is no QR-pairing fallback here at
157
160
  // all. A session watcher's job is to watch a session, not to also be the
158
- // pairing flow; keeping them separate is what makes `setu` (bare) not
161
+ // pairing flow; keeping them separate is what makes `cli-setu` (bare) not
159
162
  // need a QR code/camera every single run.
160
163
  let deviceToken = config.deviceToken;
161
164
  let deviceId = null;
@@ -233,6 +236,19 @@ async function connectToAzure() {
233
236
  log(`[supervisor] /device/connect rejected (403): ${err.error || 'no reason given'}`);
234
237
  process.exit(1);
235
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
+ }
236
252
  log(`[supervisor] /device/connect failed: ${err.error || res.status}`);
237
253
  setTimeout(connectToAzure, 3000);
238
254
  return;
@@ -585,7 +601,7 @@ function handleAzureDecision(msg) {
585
601
  // right of the actual PTY content - not a phone-side rendering bug at
586
602
  // all, a genuine laptop-side geometry mismatch. process.stdout.columns/
587
603
  // rows reflect the REAL terminal's actual current size at the moment
588
- // `setu` starts (falling back to the old 120x30 only if those aren't
604
+ // `cli-setu` starts (falling back to the old 120x30 only if those aren't
589
605
  // available - e.g. output piped to a file, not a real TTY). Still a
590
606
  // FIXED size after this point, same reasoning as before (not read back
591
607
  // off ptyProcess after spawn, never resized again - see the
@@ -1236,8 +1252,8 @@ async function main() {
1236
1252
  process.stdin.resume();
1237
1253
  process.stdin.on('data', (data) => ptyProcess.write(data.toString()));
1238
1254
 
1239
- // Real gap in the PoC this replaces: there was no way for `setu status`
1240
- // or `setu stop` (both new - see src/status.js, src/stop.js) to know
1255
+ // Real gap in the PoC this replaces: there was no way for `cli-setu status`
1256
+ // or `cli-setu stop` (both new - see src/status.js, src/stop.js) to know
1241
1257
  // whether a session watcher was even running, or on which PID, since
1242
1258
  // supervisor-qr.js was always started and killed by hand in a visible
1243
1259
  // terminal. One small PID file per running session (keyed by
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 };
package/src/update.js CHANGED
@@ -1,4 +1,4 @@
1
- // `setu --update` - checks the real npm registry for a newer published
1
+ // `cli-setu --update` - checks the real npm registry for a newer published
2
2
  // version, and if one exists, runs the real upgrade command on the
3
3
  // user's behalf (confirmed directly: don't just tell them what to run,
4
4
  // actually run it) so they don't have to remember/type
@@ -35,24 +35,24 @@ function isNewer(latest, current) {
35
35
  }
36
36
 
37
37
  async function update() {
38
- console.log(`[setu] Current version: ${pkg.version}`);
39
- console.log('[setu] Checking for updates...');
38
+ console.log(`[cli-setu] Current version: ${pkg.version}`);
39
+ console.log('[cli-setu] Checking for updates...');
40
40
 
41
41
  let latest;
42
42
  try {
43
43
  latest = await fetchLatestVersion();
44
44
  } catch (err) {
45
- console.error(`[setu] Could not check for updates: ${err.message}`);
45
+ console.error(`[cli-setu] Could not check for updates: ${err.message}`);
46
46
  process.exit(1);
47
47
  }
48
48
 
49
49
  if (!isNewer(latest, pkg.version)) {
50
- console.log(`[setu] Already on the latest version (${pkg.version}).`);
50
+ console.log(`[cli-setu] Already on the latest version (${pkg.version}).`);
51
51
  return;
52
52
  }
53
53
 
54
- console.log(`[setu] A newer version is available: ${latest} (you have ${pkg.version}).`);
55
- console.log(`[setu] Installing ${pkg.name}@${latest}...`);
54
+ console.log(`[cli-setu] A newer version is available: ${latest} (you have ${pkg.version}).`);
55
+ console.log(`[cli-setu] Installing ${pkg.name}@${latest}...`);
56
56
  try {
57
57
  // execFile (not exec) - no shell involved, consistent with this
58
58
  // project's own established convention for anything that runs a
@@ -62,10 +62,10 @@ async function update() {
62
62
  shell: process.platform === 'win32', // npm.cmd on Windows needs a shell to resolve; execFile alone can't find it otherwise
63
63
  });
64
64
  if (stdout) console.log(stdout.trim());
65
- console.log(`[setu] ✓ Updated to ${latest}.`);
65
+ console.log(`[cli-setu] ✓ Updated to ${latest}.`);
66
66
  } catch (err) {
67
- console.error(`[setu] Update failed: ${err.message}`);
68
- console.error(`[setu] You can try running it yourself: npm install -g ${pkg.name}@${latest}`);
67
+ console.error(`[cli-setu] Update failed: ${err.message}`);
68
+ console.error(`[cli-setu] You can try running it yourself: npm install -g ${pkg.name}@${latest}`);
69
69
  process.exit(1);
70
70
  }
71
71
  }