@oked/claude-code 0.1.3 → 0.1.5

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 (2) hide show
  1. package/dist/cli.js +98 -8
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2,7 +2,9 @@
2
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
3
3
  import { hostname } from "os";
4
4
  import { join, dirname } from "path";
5
+ import { fileURLToPath } from "url";
5
6
  import { spawn } from "child_process";
7
+ import { createInterface } from "readline";
6
8
  import { OKedClient, loadOKedConfig, OKED_CONFIG_PATH } from "@oked/sdk";
7
9
  const MCP_TOOL_MATCHER = "mcp__.*";
8
10
  const DEFAULT_TOOL_MATCHER = `Bash|Write|Edit|Agent|${MCP_TOOL_MATCHER}`;
@@ -17,7 +19,18 @@ const HOOK_CONFIG = {
17
19
  ],
18
20
  };
19
21
  const DEFAULT_BACKEND_URL = process.env.OKED_BACKEND_URL || "https://api.oked.ai";
20
- const CLIENT_VERSION = "0.1.0";
22
+ // Derived from this package's package.json at runtime so the version the
23
+ // backend sees (client_version) can never drift from what's actually shipped.
24
+ // From dist/cli.js, package.json sits one dir up at the package root.
25
+ const CLIENT_VERSION = (() => {
26
+ try {
27
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
28
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0.0.0";
29
+ }
30
+ catch {
31
+ return "0.0.0";
32
+ }
33
+ })();
21
34
  function getSettingsPath() {
22
35
  return join(process.cwd(), ".claude", "settings.json");
23
36
  }
@@ -82,12 +95,66 @@ function openBrowser(url) {
82
95
  args = [url];
83
96
  }
84
97
  try {
85
- spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
98
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
99
+ // The launcher may fail asynchronously (e.g. xdg-open missing). Swallow it
100
+ // so it can't crash the process; the URL is always printed as a fallback.
101
+ child.on("error", () => { });
102
+ child.unref();
103
+ return true;
86
104
  }
87
105
  catch {
88
106
  // Best effort. The user has the URL on screen anyway.
107
+ return false;
89
108
  }
90
109
  }
110
+ /**
111
+ * Best-effort copy to the OS clipboard. Resolves true only if a clipboard tool
112
+ * actually accepted the text, so the "(copied to clipboard)" line never lies on
113
+ * a headless box. Linux is tried via wl-copy (Wayland), then xclip, then xsel.
114
+ * The code is always printed, so a failure here is purely cosmetic.
115
+ */
116
+ function copyToClipboard(text) {
117
+ const candidates = process.platform === "darwin"
118
+ ? [{ cmd: "pbcopy", args: [] }]
119
+ : process.platform === "win32"
120
+ ? [{ cmd: "clip", args: [] }]
121
+ : [
122
+ { cmd: "wl-copy", args: [] },
123
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
124
+ { cmd: "xsel", args: ["--clipboard", "--input"] },
125
+ ];
126
+ const tryCopy = ({ cmd, args }) => new Promise((resolve) => {
127
+ let settled = false;
128
+ const done = (ok) => {
129
+ if (!settled) {
130
+ settled = true;
131
+ resolve(ok);
132
+ }
133
+ };
134
+ try {
135
+ const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
136
+ child.on("error", () => done(false)); // tool missing / not spawnable
137
+ child.on("close", (code) => done(code === 0));
138
+ child.stdin?.on("error", () => { }); // ignore EPIPE if the tool died early
139
+ child.stdin?.end(text);
140
+ }
141
+ catch {
142
+ done(false);
143
+ }
144
+ });
145
+ // Try each candidate in order; stop at the first that succeeds.
146
+ return candidates.reduce((acc, cand) => acc.then((ok) => (ok ? true : tryCopy(cand))), Promise.resolve(false));
147
+ }
148
+ /** Wait for the user to press Enter. Resolves immediately if stdin is closed. */
149
+ function waitForEnter(message) {
150
+ return new Promise((resolve) => {
151
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
152
+ rl.question(message, () => {
153
+ rl.close();
154
+ resolve();
155
+ });
156
+ });
157
+ }
91
158
  async function pair(clientType) {
92
159
  const codeRes = await fetch(`${DEFAULT_BACKEND_URL}/api/v1/device/code`, {
93
160
  method: "POST",
@@ -104,14 +171,36 @@ async function pair(clientType) {
104
171
  }
105
172
  const code = (await codeRes.json());
106
173
  console.log("");
107
- console.log(" To pair this device, open:");
108
- console.log(` ${code.verification_uri_complete}`);
174
+ console.log(" Pair this device with your OKed account.");
109
175
  console.log("");
110
- console.log(" Or visit " + code.verification_uri + " and enter the code:");
176
+ console.log(" Your pairing code:");
111
177
  console.log(` ${code.user_code}`);
178
+ if (await copyToClipboard(code.user_code)) {
179
+ console.log(" (copied to clipboard)");
180
+ }
181
+ console.log("");
182
+ // Let the user read the code and initiate the browser launch themselves, so
183
+ // the popup isn't a surprise. Skip the prompt when non-interactive (no TTY,
184
+ // or `--yes`/`-y`) so CI and scripted installs still work.
185
+ const interactive = Boolean(process.stdin.isTTY) &&
186
+ !process.argv.includes("--yes") &&
187
+ !process.argv.includes("-y");
188
+ if (interactive) {
189
+ await waitForEnter(" Press Enter to open your browser (Ctrl+C to cancel)... ");
190
+ }
191
+ const opened = openBrowser(code.verification_uri_complete);
192
+ console.log("");
193
+ if (opened) {
194
+ console.log(" Opened your browser to:");
195
+ }
196
+ else {
197
+ console.log(" Couldn't open your browser automatically. Open this link:");
198
+ }
199
+ console.log(` ${code.verification_uri_complete}`);
200
+ console.log("");
201
+ console.log(` (Or visit ${code.verification_uri} and enter the code above.)`);
112
202
  console.log("");
113
- console.log(" Waiting for confirmation in your browser...");
114
- openBrowser(code.verification_uri_complete);
203
+ console.log(" Waiting for confirmation...");
115
204
  const deadline = Date.now() + code.expires_in * 1000;
116
205
  const intervalMs = Math.max(1, code.interval) * 1000;
117
206
  while (Date.now() < deadline) {
@@ -190,9 +279,10 @@ async function init() {
190
279
  return;
191
280
  writeOkedConfig(apiKey, DEFAULT_BACKEND_URL);
192
281
  console.log("");
193
- console.log(` Paired. Key saved to ${OKED_CONFIG_PATH}`);
282
+ console.log(` Paired Key saved to ${OKED_CONFIG_PATH}`);
194
283
  console.log("");
195
284
  console.log("Every Claude Code session in this project is now protected.");
285
+ console.log("Open a new Claude Code session to activate the hook.");
196
286
  }
197
287
  async function status() {
198
288
  const settingsPath = getSettingsPath();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oked/claude-code",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "OKed for Claude Code - zero-code human approval for sensitive actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@oked/sdk": "0.1.3"
46
+ "@oked/sdk": "0.1.5"
47
47
  },
48
48
  "devDependencies": {
49
49
  "typescript": "^5.6.0"