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 +40 -0
- package/bin/setu.js +104 -0
- package/package.json +40 -0
- package/src/cli-adapters/README.md +74 -0
- package/src/cli-adapters/claude.js +304 -0
- package/src/cli-adapters/codex.js +261 -0
- package/src/cli-adapters/generic.js +32 -0
- package/src/cli-adapters/index.js +27 -0
- package/src/cli-adapters/shell.js +52 -0
- package/src/config.js +54 -0
- package/src/hooks.js +80 -0
- package/src/onPermissionRequest.js +89 -0
- package/src/pair.js +83 -0
- package/src/status.js +65 -0
- package/src/stop.js +51 -0
- package/src/supervisor.js +1264 -0
- package/src/update.js +73 -0
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
|
+
};
|