cli-setu 0.1.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/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # cli-setu
2
+
3
+ Bridge your terminal to your phone. Run Claude Code, Codex, or any plain shell on your laptop, and approve prompts, type, and watch the live output from the [CLIसेतु](https://clisetu.com) app on your phone.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g cli-setu
9
+ ```
10
+
11
+ ## Usage
12
+
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
17
+ ```
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.
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.
22
+
23
+ ## Other commands
24
+
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
32
+ ```
33
+
34
+ ## How it works
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.
37
+
38
+ ## License
39
+
40
+ MIT
package/bin/setu.js ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ // Command dispatcher for the `setu` CLI. Kept deliberately thin - each
3
+ // real subcommand's logic lives in src/, this file only parses argv and
4
+ // calls the right one.
5
+ const AUTH_SERVER = process.env.CLI_SETU_SERVER || 'https://api.clisetu.com';
6
+ const AUTH_SERVER_PUBLIC = process.env.CLI_SETU_SERVER_PUBLIC || AUTH_SERVER;
7
+
8
+ // Real bug found by testing: this used to switch on process.argv[2]
9
+ // directly against the subcommand list (pair/status/stop), with any
10
+ // other value (including a real --flag, e.g. `setu --cli claude`)
11
+ // falling through to "Unknown command" and exiting immediately - the
12
+ // whole multi-CLI feature (claude/codex/shell adapters) was unreachable
13
+ // through the real `setu` command this entire time, only ever testable
14
+ // by calling node src/supervisor.js --cli claude directly. Flags
15
+ // (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)
18
+ // falls through to the same "watch this session" path as before, which
19
+ // still parses --cli on its own further down the line (supervisor.js) -
20
+ // this dispatcher doesn't need to understand every flag supervisor.js
21
+ // accepts, only recognize that a leading "-" means "not a subcommand."
22
+ const args = process.argv.slice(2);
23
+ const firstArg = args[0];
24
+ const isSubcommand = firstArg && !firstArg.startsWith('-');
25
+
26
+ async function main() {
27
+ if (args.includes('--version') || args.includes('-v')) {
28
+ console.log(require('../package.json').version);
29
+ return;
30
+ }
31
+
32
+ if (args.includes('--help') || args.includes('-h')) {
33
+ printHelp();
34
+ return;
35
+ }
36
+
37
+ if (args.includes('--update')) {
38
+ const { update } = require('../src/update');
39
+ await update();
40
+ return;
41
+ }
42
+
43
+ if (isSubcommand) {
44
+ switch (firstArg) {
45
+ case 'pair': {
46
+ const { pair } = require('../src/pair');
47
+ await pair(AUTH_SERVER, AUTH_SERVER_PUBLIC);
48
+ return;
49
+ }
50
+ case 'status': {
51
+ const { status } = require('../src/status');
52
+ await status(AUTH_SERVER);
53
+ return;
54
+ }
55
+ case 'stop': {
56
+ const { stop } = require('../src/stop');
57
+ await stop();
58
+ return;
59
+ }
60
+ default:
61
+ console.error(`[setu] Unknown command: ${firstArg}`);
62
+ printHelp();
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ // Bare `setu` (or `setu --cli <name>`, `setu --cli <name> <workingDir>`,
68
+ // etc. - real flags meant for the bridged session itself, parsed by
69
+ // supervisor.js) - watch the current directory as a session. Real
70
+ // 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
72
+ // QR/8-digit code immediately, the same "first run pairs AND starts
73
+ // watching this session in one step" behavior the original
74
+ // proof-of-concept had. Only pairs if not already paired - an
75
+ // already-paired computer skips straight to watching, unchanged.
76
+ const { readConfig } = require('../src/config');
77
+ const config = readConfig();
78
+ if (!config || !config.deviceToken) {
79
+ const { pair } = require('../src/pair');
80
+ await pair(AUTH_SERVER, AUTH_SERVER_PUBLIC);
81
+ }
82
+ require('../src/supervisor');
83
+ }
84
+
85
+ function printHelp() {
86
+ console.log(`[setu] Bridge your terminal to your phone.
87
+
88
+ 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
91
+ session instead of the default plain shell
92
+ (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`);
99
+ }
100
+
101
+ main().catch((err) => {
102
+ console.error('[setu] Error:', err.message);
103
+ process.exit(1);
104
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "cli-setu",
3
+ "version": "0.1.0",
4
+ "description": "Bridge your terminal to your phone — run and approve Claude Code, Codex CLI and other AI coding agents from anywhere.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "setu": "bin/setu.js"
8
+ },
9
+ "main": "src/config.js",
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "license": "MIT",
14
+ "keywords": [
15
+ "cli",
16
+ "claude-code",
17
+ "claude",
18
+ "codex",
19
+ "terminal",
20
+ "remote",
21
+ "mobile",
22
+ "pty",
23
+ "ssh"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Evyasys-Software-Solutions/clisetuapp.git",
28
+ "directory": "cli"
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "src"
33
+ ],
34
+ "dependencies": {
35
+ "@xterm/headless": "^6.0.0",
36
+ "node-pty": "^1.1.0",
37
+ "qrcode-terminal": "^0.12.0",
38
+ "ws": "^8.21.3"
39
+ }
40
+ }
@@ -0,0 +1,74 @@
1
+ # CLI adapters
2
+
3
+ Every CLI this app can supervise (Claude Code, Codex, or one nobody's
4
+ written an adapter for yet) implements this same shape. supervisor-qr.js
5
+ never has CLI-specific logic in it directly - it only ever calls through
6
+ whichever adapter `--cli <name>` selected (see cli-adapters/index.js).
7
+
8
+ The only thing every adapter MUST provide is enough to spawn the CLI in a
9
+ PTY and mirror its raw output to a phone - that's what makes an unknown,
10
+ never-integrated CLI still usable (via `generic.js`) instead of refusing
11
+ to run at all. Everything past that (structured approval prompts, real
12
+ token/cost numbers) is optional per adapter, because different CLIs
13
+ genuinely expose different amounts of machine-readable surface:
14
+
15
+ - Claude Code: no live token API for an interactive session, but DOES
16
+ have a hook system (PermissionRequest) that reports an approval
17
+ BEFORE it's shown, and a `/cost` slash command whose rendered text
18
+ can be scraped for a cost/token estimate.
19
+ - Codex: no hook system for an interactive PTY session, but `codex exec
20
+ --json` emits clean structured JSONL (including a real `usage` object
21
+ per turn) - a completely different mechanism, not scraped text.
22
+ - Some future CLI: maybe neither - `generic.js` is what such a CLI gets,
23
+ and that's a deliberate, permanent, first-class outcome, not a
24
+ placeholder waiting to be filled in.
25
+
26
+ ## Shape
27
+
28
+ ```js
29
+ {
30
+ id: 'claude', // matches devices.cli_type in Postgres
31
+ displayName: 'Claude Code',
32
+
33
+ // Required - how supervisor-qr.js spawns this CLI in a PTY.
34
+ spawn: { command, args: (workingDir) => [...] },
35
+
36
+ // Optional - null means "this CLI has no known way to report an
37
+ // approval prompt before it happens; the human sees it live in the
38
+ // mirrored terminal instead, with no structured Confirmations card."
39
+ //
40
+ // cwd -> the queue-file/event-source location this adapter should
41
+ // watch (adapter-specific meaning - Claude Code's is a hook queue
42
+ // file keyed by cwd; another adapter might not need cwd at all).
43
+ approvals: null | {
44
+ // Called on supervisor-qr.js's existing poll loop. Returns an array
45
+ // of NEW raw events since last call (adapter decides what "raw"
46
+ // means - Claude Code's is one parsed hook-queue JSON line).
47
+ pollNewEvents(cwd) -> event[],
48
+ // Turns one raw event into what the phone actually shows.
49
+ describePrompt(event) -> string,
50
+ getDialogOptions(event) -> [{ label, keystroke, style }],
51
+ },
52
+
53
+ // Optional - null means "no usage/cost reporting available for a live
54
+ // interactive session with this CLI."
55
+ usage: null | {
56
+ // How to ask the running session for a fresh usage report (e.g.
57
+ // injecting '/cost\r' into the PTY) - supervisor-qr.js calls this on
58
+ // a timer, then feeds subsequent PTY output through parseFromOutput.
59
+ triggerKeystrokes: string | null,
60
+ // Given a chunk of raw PTY output (after triggerKeystrokes was sent),
61
+ // try to pull a usage snapshot out of it. Returns null if this chunk
62
+ // doesn't contain one (adapter's own regex decides).
63
+ parseFromOutput(chunk) -> { totalTokens, contextUsedPct, raw } | null,
64
+ },
65
+ }
66
+ ```
67
+
68
+ Codex's real usage numbers don't come from PTY scraping at all (see
69
+ codex.js) - its adapter's `usage.parseFromOutput` is unused, and instead
70
+ supervisor-qr.js special-cases adapters that set `usage.mode: 'exec-json'`
71
+ to run `codex exec resume` as a side process instead of scraping the PTY.
72
+ This is the one place the interface leaks a CLI-specific detail, because
73
+ Codex's actual model (spawn-per-turn, not a persistent PTY at all) has no
74
+ honest way to look identical to Claude Code's from the outside.
@@ -0,0 +1,304 @@
1
+ // Claude Code adapter. Ported straight out of supervisor-qr.js's old
2
+ // hardcoded describePrompt/getDialogOptions - behavior unchanged, just
3
+ // moved behind the adapter interface so supervisor-qr.js can treat this
4
+ // as one of N CLIs instead of the only one it knows about.
5
+ //
6
+ // Approval detection: Claude Code's PermissionRequest hook
7
+ // (onPermissionRequest.js, wired via .claude/settings.json) writes one
8
+ // JSON line per prompt to a queue file THIS adapter knows how to read -
9
+ // not a generic mechanism, a Claude-Code-specific one. No other CLI here
10
+ // is expected to share this exact file-based queue approach; it's an
11
+ // implementation detail of the hook wiring already built for Claude Code,
12
+ // preserved as-is rather than generalized prematurely.
13
+ //
14
+ // Usage: Claude Code has no live token API for an interactive session
15
+ // (see cli-adapters/README.md) - the closest available signal is its own
16
+ // `/cost` slash command's rendered TUI output, which this adapter parses
17
+ // on a best-effort basis. This is fragile by nature (breaks if a future
18
+ // Claude Code version changes that output's wording) - see parseFromOutput's
19
+ // regex for exactly what shape it expects today.
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ function queueFileFor(baseDir, cwd) {
24
+ const safeName = cwd.replace(/[\\/:]/g, '_');
25
+ return path.join(baseDir, `pending-requests.${safeName}.json`);
26
+ }
27
+
28
+ // Real bug found by testing: this only ever described Write/Edit/Bash -
29
+ // AskUserQuestion (Claude's own "let me ask you something" tool, a REAL,
30
+ // commonly-used tool_name, not an edge case) fell through to the generic
31
+ // `${tool_name}: ${JSON.stringify(tool_input)}` branch, showing raw JSON
32
+ // instead of the actual question text Claude Code itself renders.
33
+ // PowerShell is Windows Claude Code's own tool_name for shell commands
34
+ // (distinct from Bash, which is what WSL/macOS/Linux sessions use for the
35
+ // exact same kind of prompt) - previously undetected here entirely, so it
36
+ // fell through to the same raw-JSON fallback Bash used to.
37
+ function describePrompt(event) {
38
+ const { tool_name, tool_input } = event;
39
+ if (tool_name === 'Write' && tool_input?.file_path) return `Create ${tool_input.file_path.split(/[\\/]/).pop()}?`;
40
+ if (tool_name === 'Edit' && tool_input?.file_path) return `Edit ${tool_input.file_path.split(/[\\/]/).pop()}?`;
41
+ if ((tool_name === 'Bash' || tool_name === 'PowerShell') && tool_input?.command) {
42
+ return tool_input.description ? `${tool_input.description}?\n${tool_input.command}` : `Run: ${tool_input.command}`;
43
+ }
44
+ if (tool_name === 'AskUserQuestion' && tool_input?.questions?.[0]) {
45
+ return tool_input.questions[0].question || tool_input.questions[0].header || 'Claude has a question';
46
+ }
47
+ return `${tool_name}: ${JSON.stringify(tool_input)}`;
48
+ }
49
+
50
+ // Real bug found by testing: every option set here was a HARDCODED guess
51
+ // keyed off tool_name, never the real options Claude Code actually
52
+ // offered - correct by coincidence for Write/Edit/Bash's own fixed
53
+ // 3-choice menu (verified against real captured PermissionRequest
54
+ // payloads), but silently WRONG for AskUserQuestion, whose real options
55
+ // (with real, meaningful labels like "Just delete existing file" or
56
+ // "Overwrite then delete") are sitting right there in
57
+ // tool_input.questions[0].options and were never read at all - the app
58
+ // showed a generic "Approve/Reject" for a prompt that actually offered 4
59
+ // distinct real choices. keystroke is the exact digit Claude Code's own
60
+ // numbered menu expects (1-indexed, matching the real on-screen "1.", "2."
61
+ // list) - this MUST match what's actually rendered, since supervisor.js
62
+ // injects this keystroke directly with no translation.
63
+ function getDialogOptions(event) {
64
+ if (event.tool_name === 'AskUserQuestion' && event.tool_input?.questions?.[0]?.options?.length) {
65
+ // Real payload verified live: tool_input.questions[0].options only
66
+ // ever lists the REAL answer choices Claude proposed (e.g. "Just
67
+ // delete existing file", "Overwrite then delete") - none of them is a
68
+ // "reject". The on-screen "N. Type something." and "N+1. Chat about
69
+ // this" the user actually sees are UI chrome this tool always appends
70
+ // itself, at fixed positions right after the real options, and are
71
+ // NOT present in this payload at all - added here explicitly (with
72
+ // their own real, correct keystrokes) rather than silently missing
73
+ // from the phone's choices, since without them there'd be no reject-
74
+ // equivalent option at all for this tool.
75
+ const opts = event.tool_input.questions[0].options;
76
+ const real = opts.map((o, i) => ({ label: o.label, keystroke: String(i + 1), style: i === 0 ? 'approve' : 'approve-alt' }));
77
+ return [
78
+ ...real,
79
+ { label: 'Type something', keystroke: String(opts.length + 1), style: 'reject' },
80
+ ];
81
+ }
82
+ if (event.tool_name === 'Write' || event.tool_name === 'Edit') {
83
+ return [
84
+ { label: 'Yes', keystroke: '1', style: 'approve' },
85
+ { label: 'Yes, and accept edits automatically for this session', keystroke: '2', style: 'approve-alt' },
86
+ { label: 'No', keystroke: '3', style: 'reject' },
87
+ ];
88
+ }
89
+ if (event.tool_name === 'Bash' || event.tool_name === 'PowerShell') {
90
+ return [
91
+ { label: 'Yes', keystroke: '1', style: 'approve' },
92
+ { label: "Yes, don't ask again for this command in this project", keystroke: '2', style: 'approve-alt' },
93
+ { label: 'No', keystroke: '3', style: 'reject' },
94
+ ];
95
+ }
96
+ return [
97
+ { label: 'Yes', keystroke: '1', style: 'approve' },
98
+ { label: 'No', keystroke: '3', style: 'reject' },
99
+ ];
100
+ }
101
+
102
+ // Real bug found by testing: getDialogOptions' Write/Edit/Bash/PowerShell
103
+ // branches above are a HARDCODED GUESS at Claude Code's own menu, since
104
+ // the PermissionRequest hook payload never includes the real options for
105
+ // those tool types (confirmed live - only AskUserQuestion's payload
106
+ // carries real options at all). That guess drifted the moment Claude Code
107
+ // added a 4th choice ("switch to auto mode") to its Bash/Write menu - the
108
+ // phone showed 3 buttons for a real 4-option prompt. This reads the ACTUAL
109
+ // rendered menu directly from the terminal instead of guessing - the one
110
+ // source of truth for what the user can really pick, immune to Claude
111
+ // Code changing its wording or option COUNT in a future version.
112
+ const ANSI_RE = /\x1b\[[0-9;?]*[a-zA-Z]/g;
113
+ function stripAnsi(text) {
114
+ return text.replace(ANSI_RE, '');
115
+ }
116
+
117
+ const PROCEED_RE = /do you want to (?:proceed|create|make this edit)\??/i;
118
+ const ESC_FOOTER_RE = /esc to cancel/i;
119
+ // A real numbered option line: "1. Yes", "❯ 2. Yes, and always allow...".
120
+ // Real bug found by testing: the currently-highlighted option's pointer
121
+ // glyph (❯) is NOT wrapped in its own ANSI color code the way the
122
+ // original comment here assumed - it's a plain literal character sitting
123
+ // before the digit, confirmed against a real captured "❯ 1. Yes" line -
124
+ // so \s* alone (whitespace only) never matched it, silently dropping
125
+ // EXACTLY the currently-selected option (always index 0) out of the
126
+ // parsed list every single time. [^\d\n]* explicitly allows that glyph
127
+ // (or any other non-digit marker Claude Code might use) before the
128
+ // number, without allowing it to swallow a real digit from a different
129
+ // line entirely (multiline mode - ^ anchors per line, not per string).
130
+ const OPTION_LINE_RE = /^[^\d\n]*(\d+)\.\s+(.+)$/;
131
+
132
+ // Reads the real, live menu straight from a trailing scrollback window.
133
+ // Returns null (not a guess) if the window doesn't contain a complete
134
+ // menu yet - supervisor.js retries a few times as the PTY catches up
135
+ // before ever falling back to the static guess above.
136
+ function parseLiveOptions(scrollbackWindow) {
137
+ const clean = stripAnsi(scrollbackWindow);
138
+ const startMatch = clean.match(PROCEED_RE);
139
+ if (!startMatch) return null;
140
+ const endMatch = clean.slice(startMatch.index).match(ESC_FOOTER_RE);
141
+ if (!endMatch) return null; // menu still rendering - not a complete capture yet
142
+
143
+ const section = clean.slice(startMatch.index, startMatch.index + endMatch.index);
144
+ const options = [];
145
+ // Real bug found by testing: Claude Code's real terminal output uses
146
+ // \r\n line endings, and splitting on '\n' alone leaves a trailing \r
147
+ // on every line. JavaScript's `.` does NOT match \r (confirmed - only
148
+ // \n is excluded from `.` in most regex engines, but V8 excludes \r
149
+ // too), so (.+)$ could never reach real end-of-line and OPTION_LINE_RE
150
+ // silently matched ZERO option lines, not just the pointer-marked one -
151
+ // this is what actually caused parseLiveOptions to return null on every
152
+ // real menu, not just a pointer-glyph edge case. Stripping stray \r
153
+ // before splitting is the fix that generalizes to any future capture,
154
+ // not just this one.
155
+ for (const line of section.replace(/\r/g, '').split('\n')) {
156
+ const m = line.match(OPTION_LINE_RE);
157
+ if (m) options.push({ number: Number(m[1]), label: m[2].trim() });
158
+ }
159
+ if (options.length === 0) return null;
160
+
161
+ // Real, observed shape: the LAST numbered option is always the decline
162
+ // ("No") - every live capture so far (3-option AND the newer 4-option
163
+ // menu) confirms this holds regardless of how many "Yes, and also..."
164
+ // variants sit in between. keystroke is the option's own real number as
165
+ // rendered, not a re-numbered index - this is what makes this immune to
166
+ // Claude Code adding/removing/reordering middle options.
167
+ return options.map((o, i) => ({
168
+ label: o.label,
169
+ keystroke: String(o.number),
170
+ style: i === options.length - 1 ? 'reject' : i === 0 ? 'approve' : 'approve-alt',
171
+ }));
172
+ }
173
+
174
+ // Corrected against REAL `/cost` output (Claude Code CLI, captured live -
175
+ // see project chat history for the exact screenshot), not guessed - the
176
+ // actual shape is:
177
+ // Total cost: $0.0000
178
+ // ...
179
+ // Usage: 0 input, 0 output, 0 cache read, 0 cache write
180
+ // ...
181
+ // Current session
182
+ // ████████████████████████████▉ 58% used
183
+ // Resets 8:20pm (Asia/Kolkata)
184
+ // Two real surprises the first version of this regex got wrong: (1) the
185
+ // line is literally "Usage:", not "Total tokens used:" - a guess that
186
+ // never matched; (2) input/output here reflects only recent API calls,
187
+ // not cumulative session usage - it read 0/0 on a hours-old session in
188
+ // testing. The "Current session ... % used" context-window bar is the
189
+ // actually-meaningful, reliably-nonzero signal for "how much of this
190
+ // session have I used", so that's captured too and is what Devices/
191
+ // Profile should lead with - raw input/output tokens are kept as a
192
+ // secondary field since they're real numbers, just not the useful ones
193
+ // for "how much have I used up" the user actually asked for.
194
+ //
195
+ // Wording/format has changed across Claude Code versions before and will
196
+ // again - these regexes are intentionally loose (case-insensitive,
197
+ // tolerant of box-drawing/ANSI noise around the numbers) but there's no
198
+ // guarantee they survive the next release. If they stop matching,
199
+ // parseFromOutput just keeps returning null (see supervisor-qr.js's
200
+ // caller) - the rest of the app degrades to "no usage data," not a crash.
201
+ const USAGE_LINE_RE = /usage:\s*([\d,]+)\s*input,\s*([\d,]+)\s*output,\s*([\d,]+)\s*cache read,\s*([\d,]+)\s*cache write/i;
202
+ const COST_RE = /total cost:\s*\$([\d.]+)/i;
203
+ // "Current session" and "Current week (all models)" each have their own
204
+ // "NN% used" + "Resets ..." pair right below them - captured as separate
205
+ // regexes (not one generic loop) since the two sections' reset-time
206
+ // format actually differs (session: bare time "8:20pm (TZ)"; week: date +
207
+ // time "Sep 19, 2:30am (TZ)") - confirmed against the real captured
208
+ // output, not assumed to be the same shape.
209
+ const SESSION_PCT_RE = /current session[\s\S]{0,200}?(\d{1,3})%\s*used/i;
210
+ const SESSION_RESET_RE = /current session[\s\S]{0,300}?resets\s+([^\n(]+(?:\([^)]+\))?)/i;
211
+ const WEEK_PCT_RE = /current week[^\n]*[\s\S]{0,200}?(\d{1,3})%\s*used/i;
212
+ const WEEK_RESET_RE = /current week[^\n]*[\s\S]{0,300}?resets\s+([^\n(]+(?:\([^)]+\))?)/i;
213
+
214
+ function parseFromOutput(chunk) {
215
+ const usageMatch = chunk.match(USAGE_LINE_RE);
216
+ const costMatch = chunk.match(COST_RE);
217
+ const pctMatch = chunk.match(SESSION_PCT_RE);
218
+ const sessionResetMatch = chunk.match(SESSION_RESET_RE);
219
+ const weekPctMatch = chunk.match(WEEK_PCT_RE);
220
+ const weekResetMatch = chunk.match(WEEK_RESET_RE);
221
+ if (!usageMatch && !costMatch && !pctMatch && !weekPctMatch) return null;
222
+
223
+ const inputTokens = usageMatch ? Number(usageMatch[1].replace(/,/g, '')) : null;
224
+ const outputTokens = usageMatch ? Number(usageMatch[2].replace(/,/g, '')) : null;
225
+ return {
226
+ totalTokens: inputTokens != null && outputTokens != null ? inputTokens + outputTokens : null,
227
+ inputTokens,
228
+ outputTokens,
229
+ cacheReadTokens: usageMatch ? Number(usageMatch[3].replace(/,/g, '')) : null,
230
+ cacheWriteTokens: usageMatch ? Number(usageMatch[4].replace(/,/g, '')) : null,
231
+ costUsd: costMatch ? Number(costMatch[1]) : null,
232
+ sessionContextUsedPct: pctMatch ? Number(pctMatch[1]) : null,
233
+ sessionResetsAt: sessionResetMatch ? sessionResetMatch[1].trim() : null,
234
+ weekUsedPct: weekPctMatch ? Number(weekPctMatch[1]) : null,
235
+ weekResetsAt: weekResetMatch ? weekResetMatch[1].trim() : null,
236
+ raw: chunk,
237
+ };
238
+ }
239
+
240
+ module.exports = {
241
+ id: 'claude',
242
+ displayName: 'Claude Code',
243
+ // Adapter-declared icon (see README.md) - the app's Devices tab reads
244
+ // this instead of hardcoding "if cliType === 'claude'" anywhere in the
245
+ // UI, so a future third adapter just declares its own icon here too,
246
+ // with no UI changes needed. No official Anthropic logo asset is
247
+ // bundled (real brand marks need to be properly sourced by whoever
248
+ // owns this app, not fabricated) - a distinct, well-chosen generic
249
+ // icon + color stands in until a real logo image is supplied.
250
+ icon: { name: 'sparkles', color: '#D97757' }, // Anthropic's own public brand accent is a terracotta/orange - approximated here, not an official value
251
+ spawn: {
252
+ command:
253
+ process.platform === 'win32'
254
+ ? 'C:\\Users\\hp\\AppData\\Roaming\\npm\\claude.cmd'
255
+ : 'claude',
256
+ args: () => ['--permission-mode', 'default'],
257
+ },
258
+ approvals: {
259
+ queueFileFor,
260
+ describePrompt,
261
+ getDialogOptions,
262
+ parseLiveOptions,
263
+ // Real gap found by testing: a PTY-text-scan for Claude Code's own
264
+ // "Interrupted..." wording (resolvedLocallyPattern) was tried and
265
+ // removed - real captured terminal output across several live
266
+ // rejection tests never actually contained that literal text in a
267
+ // form a regex could reliably match. This is a different, more
268
+ // robust signal: it doesn't care WHAT replaced the menu (an
269
+ // "Interrupted" message, a follow-up question, plain output - Claude
270
+ // Code's exact wording can drift freely) - only whether the SAME
271
+ // "Do you want to proceed?" style menu PROCEED_RE matched is still
272
+ // physically on screen. supervisor.js polls this for a few seconds
273
+ // right after relaying a prompt; once it stops matching, the prompt
274
+ // is gone - approved, rejected, or interrupted, any of those means a
275
+ // decision happened, so this is a fully reliable resolution signal
276
+ // for the Bash/Write/PowerShell prompts that only ever get options
277
+ // via parseLiveOptions in the first place (AskUserQuestion already
278
+ // resolves correctly via PostToolUse+resolveIfPending's exact
279
+ // question-text match - this is not used for that tool_name at all).
280
+ isMenuStillOnScreen: (scrollbackWindow) => PROCEED_RE.test(stripAnsi(scrollbackWindow)),
281
+ },
282
+ usage: {
283
+ // '/cost' + Enter - a real, visible command in the user's own
284
+ // terminal (see supervisor-qr.js's usage-polling comment for why this
285
+ // is opt-in-by-default rather than silent).
286
+ triggerKeystrokes: '/cost\r',
287
+ parseFromOutput,
288
+ },
289
+ // Remote slash-command palette (Section 15 roadmap item #6). Each entry
290
+ // is just a label + the literal keystrokes to write into the PTY - the
291
+ // SAME primitive terminal.input already uses for free-typed text (see
292
+ // supervisor-qr.js's ws.on('message') handler), just named and one tap
293
+ // instead of typing it out. Deliberately NOT a structured remote
294
+ // sub-menu: /model opens Claude Code's own interactive picker on the
295
+ // laptop's real screen, which the phone then sees via the existing
296
+ // live terminal mirror (Phase 6) and can navigate with the same
297
+ // quick-action arrow/Enter keys already in the Terminal tab - no new
298
+ // blind multi-step remote flow being invented here.
299
+ commands: [
300
+ { id: 'cost', label: 'Check usage', keystrokes: '/cost\r' },
301
+ { id: 'model', label: 'Switch model', keystrokes: '/model\r' },
302
+ { id: 'compact', label: 'Compact context', keystrokes: '/compact\r' },
303
+ ],
304
+ };