pointer-feedback 0.2.0 → 0.2.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 +30 -15
  2. package/dist/cli.js +147 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,7 +31,7 @@ pointer init --server https://api.pointer.moamen.work --key ptr_... --project my
31
31
 
32
32
  `init` only expects these files to be shared via git: **`.pointer/config.json`**, **`.pointer/stack.json`**,
33
33
  and — in a multi-project repo (see **Monorepos** below) — every **`.pointer/projects/<key>.stack.json`**.
34
- Everything else (`pointer.sh`, `manifest.json`, and — only if you opted into `--local-credentials` —
34
+ Everything else (`pointer.sh`, `manifest.json`, and — only if you opted into `--scope repo` —
35
35
  `credentials.env`) is derived or per-machine and stays gitignored, along with every skill layout the
36
36
  CLI can write: `.claude/skills/pointer-init/`, `.claude/skills/pointer-feedback/` (Claude Code),
37
37
  `.cursor/rules/pointer-init.md`, `.cursor/rules/pointer-feedback.md` (Cursor),
@@ -175,23 +175,31 @@ its own `index.html` but no `project.json`) instead of asking about the repo roo
175
175
 
176
176
  ### Authentication
177
177
 
178
- Authenticate **once per machine**, not once per repo. `pointer login` validates an API key and
179
- saves it to a global, per-machine credential store; every other command (`init` in join mode,
180
- `doctor`, `apply`, `list`, `mcp`, and `.pointer/pointer.sh`) then finds it without being asked again
181
- in this repo, or any other repo on the same machine, against the same server.
178
+ Authenticate **once per machine**, not once per repo. `pointer login` with no `--key`, on a real
179
+ terminal opens your browser to sign in (mirrors `gh auth login`): it prints a link and a short
180
+ code, waits for you to approve it in the dashboard, and saves the personal API key it hands back to
181
+ a global, per-machine credential store. Pass `--key <key>` to skip the browser and validate a pasted
182
+ key instead (from your profile page or the dashboard's quick-start guide). Every other command
183
+ (`init` in join mode, `doctor`, `apply`, `list`, `mcp`, and `.pointer/pointer.sh`) then finds
184
+ whichever key was saved without being asked again — in this repo, or any other repo on the same
185
+ machine, against the same server.
182
186
 
183
187
  **Resolution order**, identical everywhere a key is needed:
184
188
 
185
189
  1. **`POINTER_API_KEY`** environment variable — the right choice for CI, and always wins outright.
186
190
  2. This repo's **`.pointer/credentials.env`** — written only when you opt out of the global store
187
- (`--local-credentials`, or answering "no" to the save prompt below).
191
+ (`--scope repo`, or choosing "Repo" at the prompt below; `--local-credentials` is the older alias).
188
192
  3. The **global store**: `~/.config/pointer/credentials.json` (honours `$XDG_CONFIG_HOME`; on
189
193
  Windows, `%APPDATA%\pointer\credentials.json`), keyed by server, mode `0600`.
190
194
 
191
195
  ```bash
192
196
  # Once per machine, per server:
193
197
  pointer login --server https://api.pointer.moamen.work
194
- # API key (from Pointer -> profile -> API key; input hidden): ****************
198
+ # Open this link and enter the code to sign in:
199
+ # https://app.pointer.moamen.work/cli-login?code=ABCD-EFGH
200
+ # Code: ABCD-EFGH
201
+ # Waiting for approval… (Ctrl+C to cancel)
202
+ # ✔ Signed in to https://api.pointer.moamen.work as Jane Doe (jane@example.com) — saved for all repos on this machine
195
203
 
196
204
  pointer whoami
197
205
  # https://api.pointer.moamen.work — Jane Doe (jane@example.com) — key source: global store
@@ -199,17 +207,24 @@ pointer whoami
199
207
  pointer logout # removes this machine's saved key for a server
200
208
  ```
201
209
 
202
- - **`pointer login [--server <url>] [--key <key>]`** — resolves the server from `--server`, then
203
- this repo's `.pointer/config.json`, then `$POINTER_SERVER`, then the build default. Prompts for
204
- the key (hidden input) unless `--key` is given; validates it exactly like `init` does
205
- (`/api/auth/login-with-key` + `/api/auth/me`); saves it to the global store.
210
+ - **`pointer login [--server <url>] [--key <key>] [--no-browser]`** — resolves the server from
211
+ `--server`, then this repo's `.pointer/config.json`, then `$POINTER_SERVER`, then the build
212
+ default. With no `--key` it runs the browser sign-in flow above (`--no-browser` prints the
213
+ link/code but skips trying to open a browser); `--key <key>` validates a pasted key instead,
214
+ exactly like `init` does (`/api/auth/login-with-key` + `/api/auth/me`). Either way the resulting
215
+ key is saved to the global store (or `.pointer/credentials.env` with `--scope repo`).
206
216
  - **`pointer logout [--server <url>]`** — removes this machine's saved entry for that server. Never
207
217
  touches a repo's own `.pointer/credentials.env`.
208
218
  - **`pointer whoami [--server <url>] [--json]`** — prints the server, the signed-in account, and
209
219
  which source answered the key (`env` / `repo` / `global`) — **never the key itself**.
210
220
 
221
+ **`init`'s first-run sign-in.** When no key resolves from anywhere (env, repo, or global store) and
222
+ `--key` wasn't passed, an interactive `init` asks how to sign in — "Sign in in your browser
223
+ (recommended)" (the same device-code flow as `pointer login`) or "Paste an API key" (today's hidden
224
+ prompt). `--yes`/`--json` skip the question and require `--key`.
225
+
211
226
  **`init` and the global store.** A first install that authenticates a key it did not already trust
212
- (typed interactively, or passed via `--key`) asks:
227
+ (typed interactively, signed in via the browser, or passed via `--key`) asks:
213
228
 
214
229
  ```
215
230
  Save this key for all repos on this machine? (Y/n)
@@ -217,7 +232,7 @@ Save this key for all repos on this machine? (Y/n)
217
232
 
218
233
  Answering yes (the default, and also the default under `--yes`/`--json` when `--key` is given)
219
234
  saves it globally and writes **no** `.pointer/credentials.env` at all — there is nothing repo-local
220
- to gitignore, review, or accidentally commit. Answering no, or passing **`--local-credentials`**,
235
+ to gitignore, review, or accidentally commit. Choosing "Repo" at the prompt, or passing **`--scope repo`** (alias `--local-credentials`),
221
236
  keeps the pre-global-store behaviour: the key is written to `.pointer/credentials.env` instead
222
237
  (still gitignored, still per-machine). A join (`init` run again in an already-configured repo) never
223
238
  even asks, either way: it tries `resolveApiKey`'s three sources first and only prompts when none of
@@ -226,7 +241,7 @@ them resolve.
226
241
  **CI**: set `POINTER_API_KEY` — it always wins, and nothing is written anywhere.
227
242
 
228
243
  **Multiple accounts on one machine** (e.g. a personal key for most repos, a service account for
229
- one): run `pointer init --key <key> --local-credentials` (or `pointer login` normally, then override
244
+ one): run `pointer login --scope repo` in that repo, or `pointer init --key <key> --scope repo` (or `pointer login` normally, then override
230
245
  per-repo with `.pointer/credentials.env`) for the repo that needs the different key — the repo-local
231
246
  file wins over the global store for that repo only.
232
247
 
@@ -322,7 +337,7 @@ pointer reply 12 "Investigating this now."
322
337
  ### `pointer mcp`
323
338
  Run the Model Context Protocol (MCP) server over standard I/O for AI coding agents.
324
339
 
325
- Exposes typed tools to Claude Code, Cursor, Windsurf, OpenCode, and any MCP-compatible environment without exposing API keys to the model context.
340
+ Exposes typed tools to your AI tool over MCP any MCP-compatible environment without exposing API keys to the model context.
326
341
 
327
342
  #### Configuration (User-Level, Do Not Commit)
328
343
 
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ var init_build_constants = __esm({
15
15
  "src/build-constants.ts"() {
16
16
  "use strict";
17
17
  BUILD_DEFAULT_SERVER = true ? "https://api.pointer.moamen.work" : "https://api.pointer.moamen.work";
18
- BUILD_CLI_VERSION = true ? "0.2.0" : "0.0.0-dev";
18
+ BUILD_CLI_VERSION = true ? "0.2.1" : "0.0.0-dev";
19
19
  }
20
20
  });
21
21
 
@@ -3067,6 +3067,65 @@ function summarizeDesignTokens(tokens, libraries) {
3067
3067
 
3068
3068
  // src/commands/init.ts
3069
3069
  init_credentials();
3070
+
3071
+ // src/device-login.ts
3072
+ init_api();
3073
+ import { spawn } from "node:child_process";
3074
+ import { hostname } from "node:os";
3075
+ function openBrowser(url) {
3076
+ try {
3077
+ let child;
3078
+ if (process.platform === "darwin") {
3079
+ child = spawn("open", [url], { detached: true, stdio: "ignore" });
3080
+ } else if (process.platform === "win32") {
3081
+ child = spawn("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore", windowsHide: true });
3082
+ } else {
3083
+ child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
3084
+ }
3085
+ child.unref();
3086
+ child.on("error", () => {
3087
+ });
3088
+ } catch {
3089
+ }
3090
+ }
3091
+ function sleep(ms) {
3092
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3093
+ }
3094
+ async function runDeviceLogin(server, options = {}) {
3095
+ const clientName = options.clientName ?? `pointer-feedback CLI on ${hostname()}`;
3096
+ const start = await api(server, "/api/auth/device/start", {
3097
+ method: "POST",
3098
+ body: { clientName }
3099
+ });
3100
+ console.log("Open this link and enter the code to sign in:");
3101
+ console.log(` ${start.verificationUrl}`);
3102
+ console.log(` Code: ${start.userCode}`);
3103
+ console.log("Waiting for approval\u2026 (Ctrl+C to cancel)");
3104
+ if (!options.noBrowser)
3105
+ openBrowser(start.verificationUrl);
3106
+ const deadline = Date.now() + start.expiresInSeconds * 1e3;
3107
+ const intervalMs = Math.max(1, start.intervalSeconds) * 1e3;
3108
+ while (Date.now() < deadline) {
3109
+ await sleep(intervalMs);
3110
+ const poll = await api(server, "/api/auth/device/poll", {
3111
+ method: "POST",
3112
+ body: { deviceCode: start.deviceCode }
3113
+ });
3114
+ if (poll.status === "approved") {
3115
+ if (!poll.apiKey) {
3116
+ return { ok: false, reason: "expired" };
3117
+ }
3118
+ return { ok: true, result: { apiKey: poll.apiKey, displayName: poll.displayName, email: poll.email } };
3119
+ }
3120
+ if (poll.status === "denied")
3121
+ return { ok: false, reason: "denied" };
3122
+ if (poll.status === "expired" || poll.status === "unknown")
3123
+ return { ok: false, reason: "expired" };
3124
+ }
3125
+ return { ok: false, reason: "expired" };
3126
+ }
3127
+
3128
+ // src/commands/init.ts
3070
3129
  async function initCommand(cwd2, options = {}) {
3071
3130
  const isYes = options["yes"] || options["json"];
3072
3131
  const isJson = options["json"];
@@ -3090,7 +3149,12 @@ async function initCommand(cwd2, options = {}) {
3090
3149
  if (!isYes && !options["server"] && !config.server) {
3091
3150
  server = await ask("Server URL", { default: server });
3092
3151
  }
3093
- const localCredentialsFlag = Boolean(options["local-credentials"]);
3152
+ const scopeFlag = typeof options["scope"] === "string" ? String(options["scope"]).toLowerCase() : void 0;
3153
+ if (scopeFlag !== void 0 && scopeFlag !== "global" && scopeFlag !== "repo") {
3154
+ console.error(`Invalid --scope "${options["scope"]}". Valid values: global, repo.`);
3155
+ process.exit(2);
3156
+ }
3157
+ const localCredentialsFlag = Boolean(options["local-credentials"]) || scopeFlag === "repo";
3094
3158
  let key = options["key"];
3095
3159
  let keySource = null;
3096
3160
  if (!key) {
@@ -3165,6 +3229,37 @@ async function initCommand(cwd2, options = {}) {
3165
3229
  }
3166
3230
  } else {
3167
3231
  let attempts = 0;
3232
+ if (!key) {
3233
+ const choice = await select("How do you want to sign in?", [
3234
+ "Sign in in your browser (recommended)",
3235
+ "Paste an API key"
3236
+ ]);
3237
+ if (choice.startsWith("Sign in in your browser")) {
3238
+ const outcome = await runDeviceLogin(server, { noBrowser: options["no-browser"] === true });
3239
+ if (!outcome.ok) {
3240
+ if (outcome.reason === "denied") {
3241
+ console.error("Sign-in was denied.");
3242
+ } else {
3243
+ console.error("The sign-in code expired. Run `pointer init` again.");
3244
+ }
3245
+ process.exit(3);
3246
+ }
3247
+ key = outcome.result.apiKey;
3248
+ try {
3249
+ const login = await api(server, "/api/auth/login-with-key", {
3250
+ method: "POST",
3251
+ body: { apiKey: key }
3252
+ });
3253
+ if (login?.status !== "ok" || !login?.token)
3254
+ throw new Error(login?.status || "invalid");
3255
+ token = login.token;
3256
+ me = login.user ?? await api(server, "/api/auth/me", { token });
3257
+ } catch {
3258
+ console.error("Invalid API key.");
3259
+ process.exit(3);
3260
+ }
3261
+ }
3262
+ }
3168
3263
  while (!me) {
3169
3264
  if (!key) {
3170
3265
  key = await ask(`API key (from ${product} -> profile -> API key; input hidden)`, { secret: true });
@@ -3195,9 +3290,12 @@ async function initCommand(cwd2, options = {}) {
3195
3290
  let writeLocalCreds = keySource !== "global" && keySource !== "env";
3196
3291
  if (freshlyAuthenticated) {
3197
3292
  let saveGlobally = !localCredentialsFlag;
3198
- if (saveGlobally && !isYes) {
3199
- const answer = await ask("Save this key for all repos on this machine? (Y/n)", { default: "y" });
3200
- saveGlobally = /^y/i.test(answer.trim());
3293
+ if (saveGlobally && !isYes && scopeFlag === void 0) {
3294
+ const choice = await select("Where should this API key be stored?", [
3295
+ "Global \u2014 this machine, every repo (~/.config/pointer/credentials.json)",
3296
+ "Repo \u2014 .pointer/credentials.env in this repo only (gitignored)"
3297
+ ]);
3298
+ saveGlobally = choice.startsWith("Global");
3201
3299
  }
3202
3300
  if (saveGlobally) {
3203
3301
  await saveGlobalCredential(server, { apiKey: key, email: me?.email, displayName: me?.displayName });
@@ -3465,7 +3563,7 @@ and the pointer-init skill uses them to mount the widget for you afterwards.
3465
3563
  If you know the file, name it and re-run \u2014 that always wins over detection:
3466
3564
  npx -y pointer-feedback init --html path/to/index.html
3467
3565
  Otherwise the pointer-init skill was installed for ${tool}; run it and it will mount the widget:
3468
- claude -> /pointer-init (or @pointer-init for cursor)
3566
+ /pointer-init (or @pointer-init, depending on your AI tool)
3469
3567
  Config is already saved in .pointer/config.json, so neither will ask for the key or project again.`);
3470
3568
  }
3471
3569
  }
@@ -11778,31 +11876,34 @@ async function loginCommand(cwd2, options = {}) {
11778
11876
  console.error("Invalid API key.");
11779
11877
  process.exit(3);
11780
11878
  }
11781
- } else {
11782
- let attempts = 0;
11783
- while (!me) {
11784
- key = await ask(`API key (from ${product} -> profile -> API key; input hidden)`, { secret: true });
11785
- try {
11786
- const login = await api(server, "/api/auth/login-with-key", {
11787
- method: "POST",
11788
- body: { apiKey: key }
11789
- });
11790
- if (login?.status !== "ok" || !login?.token)
11791
- throw new Error(login?.status || "invalid");
11792
- me = login.user ?? await api(server, "/api/auth/me", { token: login.token });
11793
- } catch {
11794
- attempts++;
11795
- if (attempts >= 3) {
11796
- console.error("Invalid API key.");
11797
- process.exit(3);
11798
- }
11799
- console.error("Invalid API key. Try again.");
11879
+ } else if (process.stdin.isTTY || options["no-browser"] === true) {
11880
+ const outcome = await runDeviceLogin(server, { noBrowser: options["no-browser"] === true });
11881
+ if (!outcome.ok) {
11882
+ if (outcome.reason === "denied") {
11883
+ console.error("Sign-in was denied.");
11884
+ } else {
11885
+ console.error("The sign-in code expired. Run `pointer login` again.");
11800
11886
  }
11887
+ process.exit(3);
11801
11888
  }
11802
- closePrompts();
11889
+ key = outcome.result.apiKey;
11890
+ me = { displayName: outcome.result.displayName, email: outcome.result.email };
11891
+ } else {
11892
+ console.error("No key provided and no terminal to sign in from \u2014 run `pointer login --key <key>` or set POINTER_API_KEY.");
11893
+ process.exit(2);
11894
+ }
11895
+ const scope = typeof options["scope"] === "string" ? String(options["scope"]).toLowerCase() : "global";
11896
+ if (scope !== "global" && scope !== "repo") {
11897
+ console.error(`Invalid --scope "${options["scope"]}". Valid values: global, repo.`);
11898
+ process.exit(2);
11803
11899
  }
11804
- await saveGlobalCredential(server, { apiKey: key, email: me?.email, displayName: me?.displayName });
11805
11900
  const who = me?.displayName ? `${me.displayName}${me?.email ? ` (${me.email})` : ""}` : me?.email ?? "you";
11901
+ if (scope === "repo") {
11902
+ await writeCredentials(root, key, { server });
11903
+ console.log(`\u2714 Signed in to ${server} as ${who} \u2014 saved to .pointer/credentials.env (this repo only; overrides the global store here)`);
11904
+ process.exit(0);
11905
+ }
11906
+ await saveGlobalCredential(server, { apiKey: key, email: me?.email, displayName: me?.displayName });
11806
11907
  console.log(`\u2714 Signed in to ${server} as ${who} \u2014 saved for all repos on this machine`);
11807
11908
  process.exit(0);
11808
11909
  }
@@ -11896,7 +11997,8 @@ function parseArgs(args) {
11896
11997
  "dry-run",
11897
11998
  "no-commit",
11898
11999
  "version",
11899
- "local-credentials"
12000
+ "local-credentials",
12001
+ "no-browser"
11900
12002
  ]);
11901
12003
  for (let i = 0; i < args.length; i++) {
11902
12004
  const arg = args[i];
@@ -11986,8 +12088,11 @@ Options:
11986
12088
  --no-skills Skip skills installation
11987
12089
  --no-design Skip design token detection
11988
12090
  --source-map Wire in the Vite plugin that stamps component source hashes
11989
- --local-credentials Write .pointer/credentials.env instead of saving the key to this
11990
- machine's global store (~/.config/pointer/credentials.json)
12091
+ --scope <global|repo> Where the API key is stored: global (default \u2014 this machine, all repos,
12092
+ ~/.config/pointer/credentials.json) or repo (.pointer/credentials.env,
12093
+ gitignored, this repo only). --local-credentials is an alias for --scope repo
12094
+ --no-browser When signing in in the browser (first run, no key resolved yet), print
12095
+ the link/code but don't try to open a browser
11991
12096
  -y, --yes Non-interactive
11992
12097
  --json JSON output (implies --yes)
11993
12098
  -h, --help Show help
@@ -12000,14 +12105,22 @@ Options:
12000
12105
  console.log(`
12001
12106
  Usage: pointer login [options]
12002
12107
 
12003
- Authenticate once per machine: validates an API key and saves it to
12108
+ Authenticate once per machine and save the result to
12004
12109
  ~/.config/pointer/credentials.json (honours $XDG_CONFIG_HOME / $POINTER_CONFIG_DIR), keyed by
12005
12110
  server. Every repo on this machine then resolves a key for that server without being asked again.
12006
12111
 
12112
+ With no --key on a real terminal, opens your browser to sign in (mirrors \`gh auth login\`): prints
12113
+ a link and a short code, waits for you to approve it in the dashboard, then saves the personal API
12114
+ key it hands back. Pass --key to skip the browser and validate a pasted key instead, unchanged from
12115
+ before.
12116
+
12007
12117
  Options:
12008
- --server <url> Server URL (default: this repo's .pointer/config.json, then $POINTER_SERVER)
12009
- --key <key> API key (prompted, hidden, when omitted)
12010
- -h, --help Show this help
12118
+ --server <url> Server URL (default: this repo's .pointer/config.json, then $POINTER_SERVER)
12119
+ --key <key> API key \u2014 skips the browser flow entirely
12120
+ --no-browser Print the sign-in link/code but don't try to open a browser
12121
+ --scope <global|repo> global (default): save for every repo on this machine;
12122
+ repo: write .pointer/credentials.env in the current repo only
12123
+ -h, --help Show this help
12011
12124
  `);
12012
12125
  process.exit(0);
12013
12126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pointer-feedback",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Click-to-comment feedback for your web app: install the widget, then turn pending comments into AI apply prompts.",
5
5
  "type": "module",
6
6
  "license": "MIT",