copyhub-cli 1.0.6 → 1.0.9

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/.env.example CHANGED
@@ -16,7 +16,8 @@ COPYHUB_GOOGLE_CLIENT_ID=
16
16
  COPYHUB_GOOGLE_CLIENT_SECRET=
17
17
  COPYHUB_OAUTH_REDIRECT_PORT=19999
18
18
 
19
- # Overlay accelerator (optional): if set here it OVERRIDES the value saved after copyhub login (config overlayAccelerator).
19
+ # Overlay accelerator (optional): overrides config. Default app-wide is Control+Shift+H (⌃/Ctrl + Shift + H).
20
+ # Example Mac ⌘ shortcut: COPYHUB_OVERLAY_ACCELERATOR=CommandOrControl+Shift+H
20
21
  COPYHUB_OVERLAY_ACCELERATOR=
21
22
 
22
23
  # Set to 1 to NOT show the window when overlay starts (open via shortcut / tray only).
package/README.md CHANGED
@@ -1,229 +1,291 @@
1
1
  # CopyHub
2
2
 
3
- CopyHub watches your **clipboard**, keeps a **local history** under `~/.copyhub/history.jsonl`, optionally syncs copies to **Google Sheets** (one tab per day), and shows an **Electron overlay** so you can browse recent clips quickly.
3
+ CopyHub watches your **clipboard**, stores **local history** (`~/.copyhub/history.jsonl`), optionally syncs to **Google Sheets** (one tab per day), and opens an **Electron overlay** to browse and pick recent copies.
4
4
 
5
5
  Runs on **Windows**, **macOS**, and **Linux**.
6
6
 
7
- ## Requirements
7
+ ---
8
8
 
9
- - **Node.js** 18
10
- - A **Google Cloud** project with:
11
- - **Google Sheets API** enabled for the *same* project as your OAuth client
12
- - **OAuth 2.0 Client** (Desktop app type works well for localhost redirect)
9
+ ## Table of contents
13
10
 
14
- ## Installation
11
+ Sections below: **Features** · **Requirements** · **Installation** · **Environment files** · **Google Cloud & OAuth** · **OAuth config vs env** · **First run** · **CLI commands** · **Environment variables** · **Data directory** · **Google Sheets** · **Overlay** · **Clipboard & history** · **Updating** · **Troubleshooting** · **Security** · **License**.
15
12
 
16
- ### Install globally (npm)
13
+ ---
17
14
 
18
- After the package is published to npm:
15
+ ## Features
19
16
 
20
- ```bash
21
- npm install -g copyhub-cli
22
- ```
17
+ - Clipboard polling (tunable via `COPYHUB_POLL_MS`).
18
+ - Skips saving the **same content twice in a row** to `history.jsonl` / Sheets.
19
+ - Writes Sheets to tabs named **`COPYHUB-YYYY-MM-DD`** (machine timezone).
20
+ - Overlay: paginated history, incremental Sheet sync (not all tabs at once), hints while Sheet data loads.
23
21
 
24
- Then run `copyhub` from any directory (ensure Node.js ≥ 18 is on your `PATH`).
22
+ ---
25
23
 
26
- On Linux/macOS you may need elevated permissions or an npm prefix configured for your user; see [npm docs on global installs](https://docs.npmjs.com/cli/v10/commands/npm-install#global-installation).
24
+ ## Requirements
25
+
26
+ - **Node.js** ≥ 18
27
+ - A **Google Cloud project** with:
28
+ - **Google Sheets API** enabled **on the same project** as the OAuth client
29
+ - OAuth client type **Web application** and redirect URI configured correctly (see below)
27
30
 
28
- ### From source (this repository)
31
+ ---
29
32
 
30
- From the repository root:
33
+ ## Installation
34
+
35
+ ### Global install (npm)
31
36
 
32
37
  ```bash
33
- npm install
38
+ npm install -g copyhub-cli
34
39
  ```
35
40
 
36
- Register the CLI on your machine:
41
+ Ensure `node` and `copyhub` are on your `PATH`. On Linux/macOS you may need an npm global prefix for your user — see [npm global installation](https://docs.npmjs.com/cli/v10/commands/npm-install#global-installation).
42
+
43
+ ### From source (this repo)
37
44
 
38
45
  ```bash
46
+ npm install
39
47
  npm link
40
48
  ```
41
49
 
42
- Or run without linking:
50
+ Without linking:
43
51
 
44
52
  ```bash
45
53
  node src/cli.js <command>
46
54
  ```
47
55
 
48
- ## Updating
56
+ ---
49
57
 
50
- Your settings (`~/.copyhub/config.json`, tokens, history) are kept when you upgrade the CLI.
58
+ ## Environment files
51
59
 
52
- **Recommended:** stop the background daemon before upgrading so Electron can reinstall cleanly; after upgrading start again:
60
+ The CLI and Electron overlay call `loadCopyhubEnv()`: each `.env` file is parsed and merged into **one object** — **later files override keys** from earlier ones. Then each key is applied to `process.env` **only if that variable is not already set** in the process environment (values you `export` in the shell before starting Node always win).
53
61
 
54
- ```bash
55
- copyhub stop
56
- ```
62
+ File order:
57
63
 
58
- *(Skip `stop` if you only run `--foreground` or `overlay` and nothing is in the background.)*
64
+ 1. `<package>/.env` (installed package directory / repo when developing)
65
+ 2. `~/.copyhub/.env`
66
+ 3. `./.env` in the **current working directory** (`cwd`)
59
67
 
60
- ### Global install (`npm install -g copyhub-cli`)
68
+ So after `npm install -g`, variables in `~/.copyhub/.env` still load regardless of `cwd`.
61
69
 
62
- Check what npm considers latest vs what you have:
70
+ See the template: `.env.example`.
63
71
 
64
- ```bash
65
- npm view copyhub-cli version
66
- ```
72
+ ---
67
73
 
68
- Upgrade to the latest published release:
74
+ ## Google Cloud & OAuth
69
75
 
70
- ```bash
71
- npm install -g copyhub-cli@latest
72
- ```
76
+ 1. Enable **[Google Sheets API](https://console.cloud.google.com/apis/library/sheets.googleapis.com)** for your project.
77
+ 2. **Credentials** **Create credentials** → **OAuth client ID** → **Web application**.
78
+ 3. **Authorized redirect URIs** — add **exactly** (CopyHub uses `127.0.0.1`, not `localhost`, for the default redirect):
73
79
 
74
- Alternatively:
80
+ ```text
81
+ http://127.0.0.1:19999/oauth2callback
82
+ ```
75
83
 
76
- ```bash
77
- npm update -g copyhub-cli
78
- ```
84
+ If you change the port (`COPYHUB_OAUTH_REDIRECT_PORT` or `redirectPort` in config), the Console URI must use that port.
79
85
 
80
- Restart CopyHub:
86
+ 4. **Do not** mix Client ID from env with Secret from file (CopyHub refuses mixed half-pairs). Credential precedence — see the next section.
81
87
 
82
- ```bash
83
- copyhub start
84
- ```
88
+ ### How to supply Client ID / Secret
85
89
 
86
- To see the installed package version: `npm list -g copyhub-cli --depth=0`.
90
+ | Method | Notes |
91
+ |--------|--------|
92
+ | **`copyhub login`** | Recommended first time: localhost wizard for ID/Secret → saves `config.json` → Google sign-in → spreadsheet ID / shortcut setup. |
93
+ | **`copyhub config --client-id … --client-secret …`** | Writes `config.json` directly. |
94
+ | **`.env`** or shell | Use only when **`config.json` does not** contain a full ID+Secret pair, or you intentionally rely on env only (no OAuth in config). |
87
95
 
88
- ### From source
96
+ On the wizard (Mac/Safari): prefer **Download JSON** from the Console and paste `client_id` / `client_secret`; clear the fields before pasting to avoid Keychain filling an old secret.
89
97
 
90
- Pull latest commits and reinstall modules:
98
+ ---
99
+
100
+ ## OAuth: config vs env (important)
101
+
102
+ - If **`~/.copyhub/config.json` contains both** `clientId` **and** `clientSecret` → CopyHub **always uses the file pair** for OAuth; **`COPYHUB_GOOGLE_*` from env/.env are ignored** for those two fields.
103
+ - If the file **does not** have both → use **`COPYHUB_GOOGLE_CLIENT_ID`** + **`COPYHUB_GOOGLE_CLIENT_SECRET`** from env (merged from `.env`).
104
+ - Never combine ID from env with Secret from file (or the reverse).
105
+
106
+ ID/Secret values are **sanitized** on read/write (BOM, CRLF, NBSP, zero-width characters, stray brackets around strings).
107
+
108
+ Check which source is active:
91
109
 
92
110
  ```bash
93
- copyhub stop
94
- git pull
95
- npm install
111
+ copyhub status
96
112
  ```
97
113
 
98
- If you previously ran **`npm link`**, linking stays tied to this folder — after `npm install` you usually **do not** need to link again unless npm warns otherwise:
114
+ ---
115
+
116
+ ## First run
99
117
 
100
118
  ```bash
101
- npm link
119
+ copyhub login
102
120
  ```
103
121
 
104
- Then:
122
+ 1. If OAuth is not fully configured in config/env → the browser opens **`http://127.0.0.1:<port>/credentials`** to enter Client ID / Secret.
123
+ 2. Then Google sign-in; callback **`/oauth2callback`**.
124
+ 3. Setup page: **Spreadsheet ID** (from URL `…/d/<ID>/edit`), **platform**, **overlay shortcut** (optional).
125
+
126
+ Start the daemon (clipboard + Sheet + overlay by default):
105
127
 
106
128
  ```bash
107
129
  copyhub start
108
130
  ```
109
131
 
110
- ## Google Cloud setup
111
-
112
- 1. Enable **[Google Sheets API](https://console.cloud.google.com/apis/library/sheets.googleapis.com)** on your OAuth project.
113
- 2. Create **OAuth 2.0 credentials** and add this **Authorized redirect URI** (adjust the port if you change it):
114
-
115
- ```text
116
- http://127.0.0.1:19999/oauth2callback
117
- ```
132
+ You can close the terminal; the process runs in the background. Use `copyhub list`, stop with `copyhub stop`.
118
133
 
119
- 3. Provide OAuth credentials in **any one** of these ways:
134
+ After editing `config.json`, `~/.copyhub/.env`, or shell variables that affect the daemon/overlay, **reload** without manual stop/start:
120
135
 
121
- - **Recommended for first setup:** run **`copyhub login`** with no secrets configured — your browser opens a **localhost wizard** where you paste **Client ID** and **Client secret** (the same values as `COPYHUB_GOOGLE_CLIENT_ID` / `COPYHUB_GOOGLE_CLIENT_SECRET`); they are stored in `~/.copyhub/config.json`.
136
+ ```bash
137
+ copyhub restart
138
+ ```
122
139
 
123
- - Copy `.env.example` to **`~/.copyhub/.env`** and/or a `.env` in your project folder and set `COPYHUB_GOOGLE_CLIENT_ID`, `COPYHUB_GOOGLE_CLIENT_SECRET`, and optionally `COPYHUB_OAUTH_REDIRECT_PORT` (default **19999**). This works even with **`npm install -g`** when your shell is not in the repo directory.
140
+ (Same flags as `start`: `--no-sheet`, `--no-overlay`, `--foreground`.)
124
141
 
125
- - Or run **`copyhub config`**:
142
+ Foreground (Ctrl+C stops everything):
126
143
 
127
144
  ```bash
128
- copyhub config --client-id "<ID>" --client-secret "<SECRET>" [--sheet-id "<SPREADSHEET_ID>"] [--redirect-port 19999]
145
+ copyhub start --foreground
129
146
  ```
130
147
 
131
- ## First run
148
+ ---
132
149
 
133
- 1. **Login** — opens the browser:
150
+ ## CLI commands
134
151
 
135
- ```bash
136
- copyhub login
137
- ```
152
+ | Command | Description |
153
+ |---------|-------------|
154
+ | `copyhub config --client-id ID --client-secret SEC [--redirect-port P] [--sheet-id ID]` | Writes OAuth (and optional Sheet ID, port) to `config.json`. |
155
+ | `copyhub login` | OAuth flow + browser setup. |
156
+ | `copyhub logout` | Deletes `tokens.json` (config unchanged). |
157
+ | `copyhub status` | OAuth, Sheet, token, overlay, daemon. |
158
+ | `copyhub start [--no-sheet] [--no-overlay] [--foreground]` | Default **background**; single instance. |
159
+ | `copyhub restart [--no-sheet] [--no-overlay] [--foreground]` | Stops the daemon if running, then **`start`s again** — reloads config, `.env`, overlay shortcut. |
160
+ | `copyhub list` / `copyhub ls` | Daemon PID (if any). |
161
+ | `copyhub stop` | Stops daemon and child overlay. |
162
+ | `copyhub overlay` | Electron window only (no clipboard daemon). |
163
+ | `copyhub reset --yes` | **Deletes all** of `~/.copyhub` (config, tokens, history, run state). `.env` files outside that folder are untouched. |
164
+ | `copyhub commands` / `copyhub cmds` | Quick command list. |
165
+ | `copyhub --help` | Commander help. |
166
+
167
+ ---
168
+
169
+ ## Environment variables
170
+
171
+ | Variable | Meaning |
172
+ |----------|---------|
173
+ | `COPYHUB_GOOGLE_CLIENT_ID` | OAuth Client ID (only used when config **does not** contain a full ID+Secret pair). |
174
+ | `COPYHUB_GOOGLE_CLIENT_SECRET` | OAuth Client Secret (same rule). |
175
+ | `COPYHUB_OAUTH_REDIRECT_PORT` | Localhost port for OAuth (default `19999`). Must match redirect URI in Google Console. |
176
+ | `COPYHUB_OVERLAY_ACCELERATOR` | Electron shortcut ([Accelerator](https://www.electronjs.org/docs/latest/api/accelerator)); **overrides** config when set. |
177
+ | `COPYHUB_START_NO_OVERLAY` | `=1` → `copyhub start` does not spawn overlay. |
178
+ | `COPYHUB_OVERLAY_STICKY` | `=1` → overlay does not hide on blur (only Esc / picking a row). |
179
+ | `COPYHUB_OVERLAY_HIDE_ON_START` | `=1` → do not show window at overlay startup (open via shortcut/tray). |
180
+ | `COPYHUB_OVERLAY_SKIP_TASKBAR` | `=1` → hide from taskbar (Windows/Electron). |
181
+ | `COPYHUB_POLL_MS` | Clipboard poll interval (ms). |
182
+
183
+ Electron inherits `process.env` from the daemon/CLI parent, so these apply once present in that environment.
184
+
185
+ ---
186
+
187
+ ## Data directory
188
+
189
+ Everything lives under **`~/.copyhub/`** (Windows: **`%USERPROFILE%\.copyhub`**):
138
190
 
139
- - If Client ID / Secret are **not** already in `.env` or `~/.copyhub/config.json`, the first screen collects them (**Google OAuth credentials** page). Submit it to save into config.
140
- - After Google sign-in, another setup page asks for **Spreadsheet ID**, **platform**, and optional **overlay shortcut**.
191
+ | File | Contents |
192
+ |------|----------|
193
+ | `config.json` | OAuth (`clientId`, `clientSecret`, `redirectPort`), `googleSheetId`, `overlayAccelerator`, `overlayPlatform`, … |
194
+ | `tokens.json` | OAuth refresh / access tokens |
195
+ | `history.jsonl` | Clipboard history (JSON Lines) |
196
+ | `run.json` | PID and metadata when `copyhub start` runs in the background |
141
197
 
142
- 2. On that spreadsheet/setup page, enter your **Spreadsheet ID** (from the URL `…/d/<SPREADSHEET_ID>/edit`), choose **platform** (Windows / macOS / Linux) for shortcut hints, set the **overlay accelerator** if you want, and save.
198
+ ---
143
199
 
144
- 3. **Start** the background watcher (clipboard + Sheets + overlay by default):
200
+ ## Google Sheets
145
201
 
146
- ```bash
147
- copyhub start
148
- ```
202
+ - Appends rows when Sheets are enabled and tokens are valid.
203
+ - New tab per **calendar day**: `COPYHUB-YYYY-MM-DD`.
204
+ - The spreadsheet must be shared with the Google account used for OAuth (or owned by that account).
205
+ - If the API reports disabled / permission errors: check logs — some errors include Enable API links from formatted messages in code.
149
206
 
150
- You can close the terminal; the process keeps running. Check with `copyhub list` and stop with `copyhub stop`.
207
+ ---
151
208
 
152
- ### Useful flags and environment variables
209
+ ## Overlay (Electron)
153
210
 
154
- | Action | How |
155
- |--------|-----|
156
- | Run in terminal (Ctrl+C stops everything) | `copyhub start --foreground` |
157
- | No Google Sheets | `copyhub start --no-sheet` |
158
- | No Electron overlay | `copyhub start --no-overlay` or `COPYHUB_START_NO_OVERLAY=1` |
159
- | Override shortcut | `COPYHUB_OVERLAY_ACCELERATOR` in `.env` (overrides saved config) |
160
- | Overlay stays open when clicking outside | `COPYHUB_OVERLAY_STICKY=1` |
211
+ - Default shortcut (**all platforms**, including macOS): **`Control+Shift+H`** — **⌃ Control** (bottom-left on Apple keyboards) or **Ctrl** on PC layouts — **same physical position**, not ⌘ / Win.
212
+ - For **⌘ Command + Shift + H** on Mac: set `overlayAccelerator` to `Command+Shift+H` (avoid `CommandOrControl+…` — on macOS Electron maps that to ⌘, so **⌃ Control** will not trigger the overlay). Legacy preset `CommandOrControl+Shift+H` is migrated to ⌃+Shift+H when the overlay starts.
213
+ - **macOS**: you may need **Accessibility** (*Privacy & Security*) for the app that launches Electron (Terminal, iTerm, …). If shortcuts still fail with non-US layouts, try **Input Source QWERTY** (Electron `globalShortcut` limitation).
214
+ - Overlay paginates ~10 items; Sheet data loads incrementally (not all tabs at once).
215
+ - Click outside the window usually closes the overlay (unless `COPYHUB_OVERLAY_STICKY=1`). **Esc** closes.
161
216
 
162
- Run `copyhub --help` or `copyhub commands` for built-in help.
217
+ ---
163
218
 
164
- ## CLI commands
219
+ ## Clipboard & history
220
+
221
+ - Watcher skips consecutive clipboard duplicates (same hash).
222
+ - Before writing file/Sheet, if content **exactly matches the newest row** in `history.jsonl`, it is **skipped** (avoids re-saving the same string after clipboard churn).
223
+
224
+ ---
225
+
226
+ ## Updating
165
227
 
166
- Quick reference (same as `copyhub commands`):
228
+ `~/.copyhub` data is kept when upgrading the package.
167
229
 
168
230
  ```bash
169
- copyhub config --client-id "<ID>" --client-secret "<SECRET>" [--redirect-port 19999] [--sheet-id "<SPREADSHEET_ID>"]
170
- copyhub login
171
- copyhub logout
172
- copyhub status
173
- copyhub start [--no-sheet] [--no-overlay] [--foreground]
174
- copyhub list # alias: copyhub ls
175
231
  copyhub stop
176
- copyhub overlay
177
- copyhub commands # alias: copyhub cmds
178
- copyhub --help
232
+ npm install -g copyhub-cli@latest
233
+ copyhub start
179
234
  ```
180
235
 
181
- | Command | What it does |
182
- |---------|----------------|
183
- | `copyhub config` | Writes OAuth client ID/secret (and optional Sheet ID, redirect port) to `~/.copyhub/config.json`. `--client-id` and `--client-secret` are required. |
184
- | `copyhub login` | Opens browser: localhost wizard for Client ID/secret if missing, then Google OAuth, then spreadsheet/platform setup. |
185
- | `copyhub logout` | Deletes saved OAuth tokens (`~/.copyhub/tokens.json`). |
186
- | `copyhub status` | Prints OAuth config source, sheet target, tokens, overlay settings, and whether the background daemon is running. |
187
- | `copyhub start` | Starts clipboard watcher + optional Sheets sync + Electron overlay in the **background** (closing the terminal does not stop it). Only one instance at a time. |
188
- | `copyhub start --foreground` | Same as above but attached to the terminal; **Ctrl+C** stops everything. |
189
- | `copyhub start --no-sheet` | Local history only; no Google Sheets writes. |
190
- | `copyhub start --no-overlay` | No Electron window; use env `COPYHUB_START_NO_OVERLAY=1` for the same effect. |
191
- | `copyhub list` / `copyhub ls` | Shows PID and start time if the daemon from `copyhub start` is running. |
192
- | `copyhub stop` | Stops the background daemon and its overlay child process. |
193
- | `copyhub overlay` | Runs **only** the Electron overlay (no clipboard daemon). Useful if you run the daemon separately or for debugging. |
236
+ (If you only changed config / `.env`: `copyhub restart`.)
194
237
 
195
- ## Data locations
238
+ From source: `git pull`, `npm install`, then `copyhub start` (or `npm link` while developing).
196
239
 
197
- Everything lives under **`~/.copyhub/`** (or `%USERPROFILE%\.copyhub` on Windows):
240
+ ---
198
241
 
199
- | File | Contents |
200
- |------|----------|
201
- | `config.json` | OAuth credentials (if not only in `.env`), `googleSheetId`, `overlayAccelerator`, `overlayPlatform` |
202
- | `tokens.json` | OAuth refresh/access tokens |
203
- | `history.jsonl` | Local clipboard history (JSON Lines) |
204
- | `run.json` | Daemon PID and metadata (when using `copyhub start` without `--foreground`) |
242
+ ## Troubleshooting
205
243
 
206
- ## Google Sheets layout
244
+ ### `invalid_client` or “client secret is invalid” after Google sign-in
207
245
 
208
- - Rows are appended when Sheet sync is enabled and you are logged in.
209
- - New tabs are created per **local calendar day**, named: **`COPYHUB-YYYY-MM-DD`**.
246
+ - Use OAuth client **Web application**, redirect `http://127.0.0.1:<port>/oauth2callback`.
247
+ - Rotate secret or **Download JSON** for a fresh client; enter again via wizard; on Mac **clear fields** before paste.
248
+ - `copyhub status` — verify Client ID/Secret source.
249
+ - CopyHub may show an HTML error page when exchanging the `code` fails.
210
250
 
211
- ## Overlay (Electron)
251
+ ### OAuth port already in use (`EADDRINUSE`)
252
+
253
+ Change port: `COPYHUB_OAUTH_REDIRECT_PORT` or `copyhub config … --redirect-port P`, and update the redirect URI in Google Console.
254
+
255
+ ### `copyhub start` says already running
256
+
257
+ Single background instance: `copyhub list` / `copyhub stop`, then start again.
258
+
259
+ ### Sheet not writing / API errors
260
+
261
+ - Enable Google Sheets API on the correct project.
262
+ - Check `copyhub status` (token, Sheet ID).
263
+ - Share the spreadsheet with the signed-in account.
264
+
265
+ ### Overlay won’t open / shortcut doesn’t work
266
+
267
+ - **macOS**: enable **Accessibility** for Terminal / Node / Electron.
268
+ - Default is **Control+Shift+H** (⌃ or Ctrl + Shift + H), not the Win key.
269
+ - Ensure the daemon is running (`copyhub list`) or try `copyhub overlay`.
270
+ - Avoid conflicts with other apps (Spotlight, Alfred, …).
271
+
272
+ ### Reset and start clean
273
+
274
+ ```bash
275
+ copyhub stop
276
+ copyhub reset --yes
277
+ ```
212
278
 
213
- - Global shortcut defaults to **`CommandOrControl+Shift+H`** if nothing else is set (`Ctrl+Shift+H` on Windows/Linux, `⌘⇧H` on macOS-style wording in Electron).
214
- - **macOS**: you may need to grant **Accessibility** permissions for global shortcuts.
215
- - Some **`Control+Alt+…`** combinations do not register reliably on Windows; prefer alternatives suggested on the setup page.
279
+ Then remove or edit `COPYHUB_GOOGLE_*` in your shell / `~/.copyhub/.env` if you no longer want env-based OAuth. `.env` files are **not** removed by `reset`.
216
280
 
217
- ## Troubleshooting: `invalid_client` after Google sign-in
281
+ ---
218
282
 
219
- That response comes from Google’s token endpoint when the **Client ID + Client Secret pair** does not match your OAuth client.
283
+ ## Security notes
220
284
 
221
- 1. **Use a “Web application” OAuth client** (not iOS/Android/Desktop-only flows meant for different redirect rules). Add **Authorized redirect URI**: `http://127.0.0.1:19999/oauth2callback` (change the port if you use `COPYHUB_OAUTH_REDIRECT_PORT` or saved `redirectPort`).
222
- 2. **Where credentials come from**: If **`~/.copyhub/config.json` contains both Client ID and Secret** (wizard or `copyhub config`), CopyHub uses **that pair** for OAuth — env vars are **ignored** for those two fields until you remove them from config. If config does **not** have both, CopyHub uses **`COPYHUB_GOOGLE_CLIENT_ID` / `COPYHUB_GOOGLE_CLIENT_SECRET`** from the environment / `.env`. CopyHub never mixes env Client ID with file Secret (that mismatch triggers `invalid_client`).
223
- 3. **Paste cleanly**: Re-open the localhost credential wizard or edit `config.json` so ID and secret have **no extra spaces or line breaks**.
224
- 4. Run **`copyhub status`** and confirm **Client ID/Secret source** matches what you expect.
285
+ - `config.json` and `tokens.json` contain OAuth secrets standard user-only permissions under `~/.copyhub`.
286
+ - Do not commit `.env` or CopyHub data to public git.
225
287
 
226
- If Google says **“The provided client secret is invalid”**, the secret does not match that Client ID (typo, truncated paste, or an old secret after you clicked **Reset secret** in Console). Download the client JSON again from Google Cloud, paste `client_id` and `client_secret` into the CopyHub wizard, and on **Mac/Safari** clear both fields before pasting so **iCloud Keychain** does not inject a saved password into the secret field.
288
+ ---
227
289
 
228
290
  ## License
229
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copyhub-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
4
4
  "description": "CopyHub — clipboard, local history, Google Sheets sync (OAuth). Windows, macOS, Linux.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -37,6 +37,91 @@ loadCopyhubEnv();
37
37
 
38
38
  const CLI_JS = fileURLToPath(new URL('./cli.js', import.meta.url));
39
39
 
40
+ /**
41
+ * Stop background daemon if present. Same behavior as `copyhub stop`.
42
+ * @returns {'stopped' | 'cleared-stale' | 'none'}
43
+ */
44
+ function stopBackgroundDaemonSync() {
45
+ pruneStaleRunState();
46
+ const s = readRunState();
47
+ if (!s) return 'none';
48
+ if (!isPidAlive(s.pid)) {
49
+ console.log(`PID ${s.pid} is not running — cleared run.json.`);
50
+ clearRunState();
51
+ return 'cleared-stale';
52
+ }
53
+ killDaemonTree(s.pid);
54
+ clearRunState();
55
+ console.log(`Stopped process PID ${s.pid}.`);
56
+ return 'stopped';
57
+ }
58
+
59
+ /**
60
+ * Shared by `start` and `restart`.
61
+ * @param {{ sheet?: boolean, overlay?: boolean, foreground?: boolean }} opts
62
+ */
63
+ async function runCopyhubStart(opts) {
64
+ pruneStaleRunState();
65
+
66
+ const useSheet = opts.sheet !== false;
67
+ const skipOverlay =
68
+ opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
69
+
70
+ const existing = readRunState();
71
+ if (existing && isPidAlive(existing.pid)) {
72
+ console.error(
73
+ `CopyHub already running in background (PID ${existing.pid}). See: copyhub list — Stop: copyhub stop`,
74
+ );
75
+ process.exit(1);
76
+ }
77
+ if (existing && !isPidAlive(existing.pid)) {
78
+ clearRunState();
79
+ }
80
+
81
+ if (opts.foreground) {
82
+ console.log('CopyHub foreground mode. Press Ctrl+C to stop.');
83
+ await ensureDir();
84
+
85
+ const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
86
+
87
+ const onStop = () => {
88
+ ctrl.stopSync();
89
+ process.exit(0);
90
+ };
91
+ process.on('SIGINT', onStop);
92
+ process.on('SIGTERM', onStop);
93
+ return;
94
+ }
95
+
96
+ await ensureDir();
97
+ const daemonArgs = [CLI_JS, '_daemon'];
98
+ if (!useSheet) daemonArgs.push('--no-sheet');
99
+ if (skipOverlay) daemonArgs.push('--no-overlay');
100
+
101
+ const child = spawn(process.execPath, daemonArgs, {
102
+ detached: true,
103
+ stdio: 'ignore',
104
+ windowsHide: process.platform === 'win32',
105
+ env: { ...process.env },
106
+ });
107
+ child.unref();
108
+
109
+ if (!child.pid) {
110
+ console.error('Could not spawn background process.');
111
+ process.exit(1);
112
+ }
113
+
114
+ writeRunState({
115
+ pid: child.pid,
116
+ startedAt: new Date().toISOString(),
117
+ foreground: false,
118
+ });
119
+
120
+ console.log(`CopyHub running in background (PID ${child.pid}). You may close this terminal.`);
121
+ console.log('Check: copyhub list | Stop: copyhub stop');
122
+ process.exit(0);
123
+ }
124
+
40
125
  program.name('copyhub').description(
41
126
  'CopyHub — clipboard, overlay history, Google Sheets sync (COPYHUB-daily tabs). Windows, macOS, Linux.',
42
127
  );
@@ -166,20 +251,30 @@ program
166
251
  .command('stop')
167
252
  .description('Stop the background process started by copyhub start (and overlay child)')
168
253
  .action(() => {
169
- pruneStaleRunState();
170
- const s = readRunState();
171
- if (!s) {
254
+ const r = stopBackgroundDaemonSync();
255
+ if (r === 'none') {
172
256
  console.log('No background process to stop.');
173
- return;
174
257
  }
175
- if (!isPidAlive(s.pid)) {
176
- console.log(`PID ${s.pid} is not running — cleared run.json.`);
177
- clearRunState();
178
- return;
258
+ });
259
+
260
+ program
261
+ .command('restart')
262
+ .description(
263
+ 'Stop the background daemon if running, then start again (reloads ~/.copyhub/config.json, .env, shortcut). Same flags as start.',
264
+ )
265
+ .option('--no-sheet', 'Local history only, do not write to Sheets')
266
+ .option('--no-overlay', 'Do not launch Electron')
267
+ .option('--foreground', 'Run in foreground after restart (Ctrl+C stops)')
268
+ .action(async (opts) => {
269
+ const r = stopBackgroundDaemonSync();
270
+ if (r === 'stopped') {
271
+ console.log('Starting again — config, ~/.copyhub/.env, and shell env will be re-read.');
272
+ } else if (r === 'none') {
273
+ console.log('No background daemon — starting CopyHub.');
274
+ } else {
275
+ console.log('Stale run state cleared — starting CopyHub.');
179
276
  }
180
- killDaemonTree(s.pid);
181
- clearRunState();
182
- console.log(`Stopped process PID ${s.pid}.`);
277
+ await runCopyhubStart(opts);
183
278
  });
184
279
 
185
280
  program
@@ -256,65 +351,7 @@ program
256
351
  .option('--no-overlay', 'Do not launch Electron')
257
352
  .option('--foreground', 'Run in foreground (Ctrl+C stops; no background PID file)')
258
353
  .action(async (opts) => {
259
- pruneStaleRunState();
260
-
261
- const useSheet = opts.sheet !== false;
262
- const skipOverlay =
263
- opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
264
-
265
- const existing = readRunState();
266
- if (existing && isPidAlive(existing.pid)) {
267
- console.error(
268
- `CopyHub already running in background (PID ${existing.pid}). See: copyhub list — Stop: copyhub stop`,
269
- );
270
- process.exit(1);
271
- }
272
- if (existing && !isPidAlive(existing.pid)) {
273
- clearRunState();
274
- }
275
-
276
- if (opts.foreground) {
277
- console.log('CopyHub foreground mode. Press Ctrl+C to stop.');
278
- await ensureDir();
279
-
280
- const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
281
-
282
- const onStop = () => {
283
- ctrl.stopSync();
284
- process.exit(0);
285
- };
286
- process.on('SIGINT', onStop);
287
- process.on('SIGTERM', onStop);
288
- return;
289
- }
290
-
291
- await ensureDir();
292
- const daemonArgs = [CLI_JS, '_daemon'];
293
- if (!useSheet) daemonArgs.push('--no-sheet');
294
- if (skipOverlay) daemonArgs.push('--no-overlay');
295
-
296
- const child = spawn(process.execPath, daemonArgs, {
297
- detached: true,
298
- stdio: 'ignore',
299
- windowsHide: process.platform === 'win32',
300
- env: { ...process.env },
301
- });
302
- child.unref();
303
-
304
- if (!child.pid) {
305
- console.error('Could not spawn background process.');
306
- process.exit(1);
307
- }
308
-
309
- writeRunState({
310
- pid: child.pid,
311
- startedAt: new Date().toISOString(),
312
- foreground: false,
313
- });
314
-
315
- console.log(`CopyHub running in background (PID ${child.pid}). You may close this terminal.`);
316
- console.log('Check: copyhub list | Stop: copyhub stop');
317
- process.exit(0);
354
+ await runCopyhubStart(opts);
318
355
  });
319
356
 
320
357
  program
@@ -359,6 +396,7 @@ program
359
396
  copyhub login | copyhub logout | copyhub status
360
397
  copyhub reset --yes (delete ~/.copyhub — stop daemon first is recommended)
361
398
  copyhub start [--no-sheet] [--no-overlay] [--foreground]
399
+ copyhub restart [--no-sheet] [--no-overlay] [--foreground] (stop daemon if running, then start — reloads config/.env)
362
400
  Default runs in background (terminal can close). Single instance — second start is blocked.
363
401
  copyhub list (ls) | copyhub stop
364
402
  copyhub overlay | copyhub commands / copyhub --help`);
package/src/oauth.js CHANGED
@@ -383,18 +383,22 @@ async function runCredentialBootstrap() {
383
383
  /** Shortcut presets (Electron Accelerator) per platform — embedded as JSON in the setup page. */
384
384
  const PLATFORM_PRESETS = {
385
385
  win: [
386
- { label: 'Ctrl + Shift + H · recommended', value: 'CommandOrControl+Shift+H' },
387
- { label: 'Control + Shift + H', value: 'Control+Shift+H' },
386
+ { label: 'Ctrl + Shift + H · recommended', value: 'Control+Shift+H' },
388
387
  { label: 'Alt + Shift + H', value: 'Alt+Shift+H' },
389
388
  ],
390
389
  mac: [
391
- { label: '⌘ + Shift + H · recommended', value: 'CommandOrControl+Shift+H' },
392
- { label: 'Command + Shift + H', value: 'Command+Shift+H' },
390
+ {
391
+ label: ' Control + Shift + H · recommended (Apple & PC keyboards)',
392
+ value: 'Control+Shift+H',
393
+ },
394
+ {
395
+ label: '⌘ Command + Shift + H',
396
+ value: 'Command+Shift+H',
397
+ },
393
398
  { label: '⌘ + Shift + V', value: 'Command+Shift+V' },
394
399
  ],
395
400
  linux: [
396
- { label: 'Ctrl + Shift + H · recommended', value: 'CommandOrControl+Shift+H' },
397
- { label: 'Control + Shift + H', value: 'Control+Shift+H' },
401
+ { label: 'Ctrl + Shift + H · recommended', value: 'Control+Shift+H' },
398
402
  { label: 'Alt + Shift + H', value: 'Alt+Shift+H' },
399
403
  ],
400
404
  };
@@ -632,7 +636,7 @@ function setupPageHtml(setupToken, currentSheetId, currentAccelerator, currentPl
632
636
  <button type="button" class="platform-btn" data-platform="mac" aria-pressed="false">
633
637
  <span class="ico" aria-hidden="true">⌘</span>
634
638
  <span class="name">macOS</span>
635
- <span class="tag">⌘ Command</span>
639
+ <span class="tag">⌃ Control default</span>
636
640
  </button>
637
641
  <button type="button" class="platform-btn" data-platform="linux" aria-pressed="false">
638
642
  <span class="ico" aria-hidden="true">🐧</span>
@@ -641,10 +645,10 @@ function setupPageHtml(setupToken, currentSheetId, currentAccelerator, currentPl
641
645
  </button>
642
646
  </div>
643
647
 
644
- <label class="field-label" for="acc">Accelerator (blank = default Ctrl/⌘ + Shift + H)</label>
648
+ <label class="field-label" for="acc">Accelerator (blank = Control + Shift + H)</label>
645
649
  <input id="acc" type="text" name="overlayAccelerator" value="${accVal}" placeholder="Pick a preset below or type your own" autocomplete="off" spellcheck="false" />
646
650
 
647
- <p class="hint">On Windows use <code>Control</code> in config, not <code>Ctrl</code>. Avoid <code>Control+Alt+…</code> (often grabbed by drivers).</p>
651
+ <p class="hint">Default everywhere: <code>Control+Shift+H</code> (⌃ or Ctrl + Shift + H — same key on Mac Apple keyboard & PC keyboard). On Windows type <code>Control</code> in config, not <code>Ctrl</code>. Avoid <code>Control+Alt+…</code> (often grabbed by drivers).</p>
648
652
  <p class="hint"><code>COPYHUB_OVERLAY_ACCELERATOR</code> in <code>.env</code>, if set, overrides this value.</p>
649
653
 
650
654
  <div id="chipRegion" class="chips" aria-live="polite"></div>
@@ -670,9 +674,9 @@ function setupPageHtml(setupToken, currentSheetId, currentAccelerator, currentPl
670
674
  var btns = document.querySelectorAll('.platform-btn');
671
675
 
672
676
  var hints = {
673
- win: 'Windows: CommandOrControl = Ctrl.',
674
- mac: 'macOS: CommandOrControl = ⌘ Command.',
675
- linux: 'Linux: same as Windows with Ctrl; clipboard depends on your desktop.'
677
+ win: 'Windows: default Control+Shift+H.',
678
+ mac: 'macOS: default Control+Shift+H (same as Ctrl on a PC keyboard). Use a preset only if you prefer Command.',
679
+ linux: 'Linux: default Control+Shift+H; clipboard depends on your desktop.'
676
680
  };
677
681
 
678
682
  function setPlatform(p) {
package/ui/main.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  Tray,
11
11
  Menu,
12
12
  nativeImage,
13
+ systemPreferences,
13
14
  } from 'electron';
14
15
  import { loadCopyhubEnv } from '../src/load-env.js';
15
16
  import { readRecentHistorySync } from '../src/storage.js';
@@ -32,17 +33,37 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
33
  /** Set to 1 so the window does not hide on blur (only Esc / pick row to copy). */
33
34
  const STICKY_NO_BLUR = process.env.COPYHUB_OVERLAY_STICKY === '1';
34
35
 
35
- /** Electron Accelerator: use `Control`, not `Ctrl`; `CommandOrControl` = Ctrl (Win) / Cmd (Mac). */
36
+ /**
37
+ * Electron Accelerator: use `Control`, not `Ctrl`; Unicode ⌃/⌘ → words.
38
+ * See migrateDarwinOverlayAccelerator — `CommandOrControl` is ⌘ on macOS, not ⌃.
39
+ */
36
40
  function normalizeAccelerator(raw) {
37
41
  if (!raw || typeof raw !== 'string') return '';
38
- let s = raw.trim();
42
+ let s = raw.trim().normalize('NFKC');
43
+ s = s.replace(/\u2303/g, 'Control'); // ⌃
44
+ s = s.replace(/\u2318/g, 'Command'); // ⌘
45
+ s = s.replace(/\u2325/g, 'Alt'); // ⌥
39
46
  s = s.replace(/\bCtrl\b/gi, 'Control');
40
47
  s = s.replace(/\bCmd\b/gi, 'Command');
48
+ s = s.replace(/\bCmdOrCtrl\b/gi, 'CommandOrControl');
41
49
  s = s.replace(/\s*\+\s*/g, '+');
42
50
  return s;
43
51
  }
44
52
 
45
- const DEFAULT_ACCEL = 'CommandOrControl+Shift+H';
53
+ /**
54
+ * On macOS, Electron maps `CommandOrControl` to ⌘. CopyHub defaults want ⌃ Control + Shift + H.
55
+ * Migrate only this chord so ⌃+Shift+H works if config still has the cross-platform preset.
56
+ * @param {string} normalized output of normalizeAccelerator
57
+ */
58
+ function migrateDarwinOverlayAccelerator(normalized) {
59
+ if (process.platform !== 'darwin' || !normalized) return normalized;
60
+ const compact = normalized.replace(/\s+/g, '');
61
+ if (/^commandorcontrol\+shift\+h$/i.test(compact)) return 'Control+Shift+H';
62
+ return normalized;
63
+ }
64
+
65
+ /** Same physical ⌃ / Ctrl key on Mac (Apple & Windows-layout keyboards) and on Win/Linux — one default everywhere. */
66
+ const DEFAULT_ACCEL = 'Control+Shift+H';
46
67
  const HIDE_ON_START = process.env.COPYHUB_OVERLAY_HIDE_ON_START === '1';
47
68
 
48
69
  /** Overlay size (slightly larger than earlier ~70% width). */
@@ -213,7 +234,7 @@ function toggleOverlay() {
213
234
  }
214
235
 
215
236
  /**
216
- * Register global shortcut: try .env (normalized) then default CommandOrControl+Shift+H.
237
+ * Register global shortcut: try .env / saved config (normalized) then default Control+Shift+H.
217
238
  * @returns {{ accelerator: string, usedFallback: boolean }}
218
239
  */
219
240
  function registerHotkeys() {
@@ -222,7 +243,7 @@ function registerHotkeys() {
222
243
  loadOverlayAcceleratorFromConfigSync();
223
244
  const candidates = [];
224
245
  if (raw) {
225
- const n = normalizeAccelerator(raw);
246
+ const n = migrateDarwinOverlayAccelerator(normalizeAccelerator(raw));
226
247
  if (n) candidates.push(n);
227
248
  }
228
249
  candidates.push(DEFAULT_ACCEL);
@@ -231,10 +252,14 @@ function registerHotkeys() {
231
252
  for (let i = 0; i < candidates.length; i++) {
232
253
  const acc = candidates[i];
233
254
  try {
234
- if (globalShortcut.register(acc, () => toggleOverlay())) {
255
+ const ok = globalShortcut.register(acc, () => toggleOverlay());
256
+ if (ok) {
235
257
  if (i > 0) usedFallback = true;
236
258
  return { accelerator: acc, usedFallback };
237
259
  }
260
+ console.warn(
261
+ `CopyHub overlay — globalShortcut could not register "${acc}" (in use by another app, or macOS Input Source / permissions).`,
262
+ );
238
263
  } catch (e) {
239
264
  console.warn('Invalid accelerator:', acc, /** @type {Error} */ (e).message);
240
265
  }
@@ -244,6 +269,23 @@ function registerHotkeys() {
244
269
  /* ignore */
245
270
  }
246
271
  }
272
+ if (process.platform === 'darwin') {
273
+ const trusted = systemPreferences.isTrustedAccessibilityClient(false);
274
+ if (!trusted) {
275
+ console.error(
276
+ 'CopyHub overlay — macOS: enable Accessibility for the app that launches Electron (e.g. Terminal, iTerm, or Node) in System Settings → Privacy & Security → Accessibility.',
277
+ );
278
+ try {
279
+ systemPreferences.isTrustedAccessibilityClient(true);
280
+ } catch {
281
+ /* ignore */
282
+ }
283
+ } else {
284
+ console.error(
285
+ 'CopyHub overlay — shortcut still failed: try Input Source US/QWERTY (Electron globalShortcut quirk on macOS), pick another chord in config, or open from the menu bar icon.',
286
+ );
287
+ }
288
+ }
247
289
  return { accelerator: '', usedFallback: false };
248
290
  }
249
291
 
@@ -546,12 +588,12 @@ if (gotLock) {
546
588
  };
547
589
  if (accelerator) {
548
590
  console.log('CopyHub overlay — shortcut in use:', accelerator);
549
- console.log('Windows tip: Ctrl+Shift+H (CommandOrControl+Shift+H).');
591
+ console.log('Default shortcut: Control+Shift+H (⌃ or Ctrl + Shift + H).');
550
592
  if (usedFallback) {
551
593
  console.warn(
552
- 'COPYHUB_OVERLAY_ACCELERATOR could not be registered. Using default CommandOrControl+Shift+H.',
594
+ 'COPYHUB_OVERLAY_ACCELERATOR could not be registered. Using default Control+Shift+H.',
553
595
  );
554
- console.warn('Leave COPYHUB_OVERLAY_ACCELERATOR unset in .env to always use Ctrl+Shift+H.');
596
+ console.warn('Leave COPYHUB_OVERLAY_ACCELERATOR unset to use the default Control+Shift+H.');
555
597
  }
556
598
  } else {
557
599
  console.error(