unramble 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -10
  2. package/cli/unramble.mjs +43 -10
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  <p align="center"><b>Press Ctrl+G and the rambling prompt you just dictated becomes the one you meant to type — in place, inside Claude Code, on the plan you already have.</b></p>
9
9
 
10
10
  <p align="center">
11
- <a href="#install">Install</a> ·
11
+ <a href="https://yadava5.github.io/unramble/"><b>Install in one click</b></a> ·
12
12
  <a href="#what-it-does">What it does</a> ·
13
13
  <a href="#plan-or-api-key">Plan or API key</a> ·
14
14
  <a href="docs/BENCHMARKS.md">Benchmarks</a> ·
@@ -35,15 +35,19 @@ lost. Ctrl+G again puts your original back.
35
35
 
36
36
  ## Install
37
37
 
38
- Pick one. Both install the same plugin and bind Ctrl+G.
38
+ **One click:** [**Install in Claude Code →**](https://yadava5.github.io/unramble/) opens Claude
39
+ Code in your terminal with the install command typed in; press Enter.
39
40
 
40
- **A. In your terminal** (zsh, bash, PowerShell — not inside Claude Code; needs Node 18+):
41
+ **In your terminal** (zsh, bash, PowerShell — not inside Claude Code; needs Node 18+):
41
42
 
42
43
  ```sh
43
44
  npx unramble
44
45
  ```
45
46
 
46
- **B. Inside Claude Code** — start `claude`, then type these at its `❯` prompt, one at a time:
47
+ That installs the plugin and binds Ctrl+G in one step.
48
+
49
+ **Or inside Claude Code**, with no Node needed: start `claude`, then type these at its `❯`
50
+ prompt, one at a time. Claude Code has no shorter form for a plugin outside its own directory.
47
51
 
48
52
  ```text
49
53
  /plugin install unramble --marketplace yadava5/unramble
@@ -54,7 +58,9 @@ Answer `y` to add the marketplace and pick a scope (user is the default). `setup
54
58
  it keeps a backup of your `keybindings.json` and never overwrites a key you bound yourself.
55
59
 
56
60
  Then quit Claude Code and start a new session (`claude`), say or type a prompt, and press
57
- **Ctrl+G**. Built and tested on Claude Code **2.1.308**; see
61
+ **Ctrl+G**. Works with Claude Code 2.1.308 and every release after it: CI tests the newest
62
+ Claude Code every day on Linux, macOS and Windows. It always uses the newest Sonnet or Opus,
63
+ because it asks for the model by family, never by version. See
58
64
  [Where it is tested](#where-it-is-tested).
59
65
 
60
66
  ## What it does
@@ -93,6 +99,9 @@ count that as a minor defect.)
93
99
  - **Lose a machine value.** A rewrite that drops a path, `--flag`, number, URL, `@ref` or
94
100
  `[Pasted text]` chip is refused and your draft stays — unless you corrected that value on
95
101
  the spot ("src/utils, no wait, src/lib").
102
+ - **Add a link or a command you never said.** Pasted text can carry instructions; a rewrite
103
+ that brings in a URL, `curl`/`wget`/`sudo`/`rm -…`, shell chaining or invisible characters
104
+ you did not say is refused. On 2,240 recorded rewrites this refused none.
96
105
  - **Touch pasted logs or code.** They are masked before the model sees the draft and put back
97
106
  byte for byte.
98
107
  - **Answer your question or do the task.** "Can you check X?" stays a question for Claude Code.
@@ -169,13 +178,23 @@ editor as VS Code and Cursor). If one does not work, please
169
178
  - **A short window can hide the band.** In an 80×24 window with a tall status line, Claude Code
170
179
  may not draw the `⌃G refine` row; Ctrl+G still works.
171
180
  - **Long pastes stay as chips.** When a dictation app pastes a long block, Claude Code folds it
172
- into a `[Pasted text #N]` chip that plugins cannot read in 2.1.308, so that text is kept as
181
+ into a `[Pasted text #N]` chip that plugins cannot read (as of 2.1.308), so that text is kept as
173
182
  is. Claude Code's built-in voice input, macOS Dictation and typing all work fully.
174
183
  - **Garbled speech stays garbled.** If the transcription is wrong in a way context cannot fix
175
184
  ("Apple ID" for a project name), the rewrite keeps it as heard rather than guess. Add the
176
185
  name to *Names you often say*.
177
- - **The plugin API is early access.** CI runs the latest Claude Code weekly to catch a release
178
- that moves it.
186
+ - **The plugin API is early access.** CI runs the newest Claude Code every day to catch a
187
+ release that moves it.
188
+
189
+ ## Update
190
+
191
+ ```sh
192
+ npx unramble update
193
+ ```
194
+
195
+ Or have Claude Code keep it current: in `/plugin`, open the `unramble` marketplace and turn on
196
+ auto-update. Every change reaches `main` only through a reviewed pull request, so that is
197
+ what auto-update follows.
179
198
 
180
199
  ## Uninstall
181
200
 
@@ -198,8 +217,15 @@ plan instead of an API key. I used Claude Code to build it.
198
217
  ## Contributing
199
218
 
200
219
  Reports of a rewrite that changed your meaning are the most useful thing you can send; there
201
- is an [issue template](https://github.com/yadava5/unramble/issues/new/choose) for it. See
202
- [CONTRIBUTING.md](CONTRIBUTING.md).
220
+ is an [issue template](https://github.com/yadava5/unramble/issues/new/choose) for it. Pull
221
+ requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
222
+
223
+ ## Security
224
+
225
+ Every change reaches `main` through a reviewed pull request, releases publish only from a
226
+ maintainer-approved tag with npm provenance, and CI fails any change that widens what the
227
+ shipped code can reach. [SECURITY.md](SECURITY.md) lists exactly what the plugin touches and how
228
+ to report a vulnerability privately.
203
229
 
204
230
  ## License
205
231
 
package/cli/unramble.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // npx unramble [install | uninstall | status] — sets the plugin up in one paste.
2
+ // npx unramble [install | update | uninstall | status] — sets the plugin up in one paste.
3
3
  //
4
4
  // install: claude plugin install unramble --marketplace yadava5/unramble, then binds Ctrl+G
5
5
  // in keybindings.json (a backup is kept beside it).
6
+ // update: pulls the newest unramble from its marketplace.
6
7
  // uninstall: removes the plugin and takes back only the keybindings install added.
7
8
  // status: shows whether the plugin is installed and Ctrl+G is bound.
8
9
  //
@@ -11,14 +12,26 @@
11
12
  import { spawnSync } from 'node:child_process'
12
13
  import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
13
14
  import { homedir } from 'node:os'
14
- import { join } from 'node:path'
15
+ import { isAbsolute, join, resolve } from 'node:path'
15
16
 
16
17
  import { isBound, parse, plan, serialize, unplan } from '../hooks/keybindings.js'
17
18
 
18
- const MARKETPLACE = process.env.UNRAMBLE_MARKETPLACE || 'yadava5/unramble'
19
+ // UNRAMBLE_MARKETPLACE points the installer at a fork or a local checkout (CI uses it). On
20
+ // Windows the argument passes through cmd.exe, so only plain repo names and paths are taken.
21
+ const override = process.env.UNRAMBLE_MARKETPLACE
22
+ if (override && (!/^[\w@./:\\ ~#-]+$/.test(override) || override.startsWith('-'))) {
23
+ process.stderr.write('unramble: UNRAMBLE_MARKETPLACE must be owner/repo or a folder path\n')
24
+ process.exit(1)
25
+ }
26
+ const MARKETPLACE = override ? (existsSync(override) ? resolve(override) : override) : 'yadava5/unramble'
19
27
  const PLUGIN = 'unramble'
20
28
  const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude')
29
+ if (!isAbsolute(configDir)) {
30
+ process.stderr.write(`unramble: CLAUDE_CONFIG_DIR must be an absolute path (got ${configDir})\n`)
31
+ process.exit(1)
32
+ }
21
33
  const keybindings = join(configDir, 'keybindings.json')
34
+ const backup = `${keybindings}.unramble-backup`
22
35
 
23
36
  const say = (line = '') => process.stdout.write(`${line}\n`)
24
37
  const fail = line => {
@@ -26,11 +39,18 @@ const fail = line => {
26
39
  process.exit(1)
27
40
  }
28
41
 
29
- /** Runs `claude` with the person's own environment and terminal; true on success. */
42
+ const windows = process.platform === 'win32'
43
+
44
+ /**
45
+ * Runs `claude` with the person's own environment and terminal; true on success. It runs from
46
+ * the home folder: on Windows (where `claude` is a .cmd shim and needs the shell) cmd.exe looks
47
+ * in the current folder before PATH, so a `claude.cmd` in a cloned repo must never be the one run.
48
+ */
30
49
  function claude(args, { quiet = false } = {}) {
31
- const r = spawnSync('claude', args, {
50
+ const r = spawnSync('claude', windows ? args.map(a => (a.includes(' ') ? `"${a}"` : a)) : args, {
51
+ cwd: homedir(),
32
52
  stdio: quiet ? 'pipe' : 'inherit',
33
- shell: process.platform === 'win32',
53
+ shell: windows,
34
54
  encoding: 'utf8',
35
55
  })
36
56
  if (r.error?.code === 'ENOENT') {
@@ -48,6 +68,11 @@ function readKeys() {
48
68
  }
49
69
  }
50
70
 
71
+ /** The first backup is the person's own file; a later install or uninstall never replaces it. */
72
+ function keepBackup(text) {
73
+ if (text !== null && !existsSync(backup)) copyFileSync(keybindings, backup)
74
+ }
75
+
51
76
  function bindKeys() {
52
77
  const { text, file } = readKeys()
53
78
  const next = plan(file)
@@ -57,7 +82,7 @@ function bindKeys() {
57
82
  say(` Free that chord in ${keybindings}, then run /unramble setup in Claude Code.`)
58
83
  return
59
84
  }
60
- if (text !== null) copyFileSync(keybindings, `${keybindings}.unramble-backup`)
85
+ keepBackup(text)
61
86
  writeFileSync(keybindings, serialize(next.file))
62
87
  say(`✓ Bound Ctrl+G in ${keybindings}${text !== null ? ' (backup: keybindings.json.unramble-backup)' : ''}`)
63
88
  for (const c of next.changes) say(` ${c}`)
@@ -80,13 +105,21 @@ function uninstall() {
80
105
  const { text, file } = readKeys()
81
106
  const back = unplan(file)
82
107
  if (back.changes.length) {
83
- if (text !== null) copyFileSync(keybindings, `${keybindings}.unramble-backup`)
108
+ keepBackup(text)
84
109
  writeFileSync(keybindings, serialize(back.file))
85
110
  say(`✓ Took back the Ctrl+G bindings in ${keybindings}`)
86
111
  }
87
112
  say(removed ? '✓ unramble is uninstalled' : '! The plugin was not installed (or the uninstall failed); see above.')
88
113
  }
89
114
 
115
+ function update() {
116
+ claude(['plugin', 'marketplace', 'update', PLUGIN], { quiet: true })
117
+ if (!claude(['plugin', 'update', `${PLUGIN}@${PLUGIN}`]).ok) {
118
+ fail('the update did not finish; is unramble installed? Try: npx unramble')
119
+ }
120
+ say('✓ unramble is up to date. Start a new Claude Code session to load it.')
121
+ }
122
+
90
123
  function status() {
91
124
  const listed = claude(['plugin', 'list'], { quiet: true })
92
125
  const installed = new RegExp(`\\b${PLUGIN}@`).test(listed.out)
@@ -95,9 +128,9 @@ function status() {
95
128
  }
96
129
 
97
130
  const command = process.argv[2] ?? 'install'
98
- const commands = { install, uninstall, status }
131
+ const commands = { install, update, uninstall, status }
99
132
  if (command === '--help' || command === '-h' || !(command in commands)) {
100
- say('Usage: npx unramble [install | uninstall | status]')
133
+ say('Usage: npx unramble [install | update | uninstall | status]')
101
134
  process.exit(command in commands || command.startsWith('-') ? 0 : 1)
102
135
  }
103
136
  commands[command]()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unramble",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Ctrl+G turns a rambling, dictated prompt into a clean, ordered one, in place, on your own Claude Code plan.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -30,7 +30,8 @@
30
30
  "node": ">=18"
31
31
  },
32
32
  "scripts": {
33
- "test": "claude plugin validate . && claude plugin test . && node --test cli/*.test.mjs",
34
- "test:cli": "node --test cli/*.test.mjs"
33
+ "test": "node scripts/guard.mjs && claude plugin validate . && claude plugin test . && node --test cli/*.test.mjs scripts/*.test.mjs",
34
+ "test:cli": "node --test cli/*.test.mjs",
35
+ "guard": "node scripts/guard.mjs"
35
36
  }
36
37
  }