getobsrv 0.7.1 → 0.8.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 CHANGED
@@ -28,6 +28,38 @@ Both panes stay in lock-step (scroll, navigation) and the 1x pane is fully inter
28
28
  fit), and are shown at true physical size — usually a small, dense render on a
29
29
  desktop monitor, exactly like the phone in your hand.
30
30
 
31
+ ## Quickstart
32
+
33
+ **The desktop app** — download the DMG for your chip from
34
+ [Releases](https://github.com/vibesyemmy/obsrv/releases), drag Obsrv.app to
35
+ Applications, then clear the quarantine flag once (the build is not yet
36
+ notarised, so macOS falsely reports it as "damaged"):
37
+
38
+ ```bash
39
+ xattr -cr /Applications/Obsrv.app
40
+ ```
41
+
42
+ Open it and set your monitor's diagonal in Settings — that one number is what
43
+ makes the target pane render at true physical size.
44
+
45
+ **The CLI, and Claude Code / MCP clients:**
46
+
47
+ ```bash
48
+ npm i -g getobsrv # or use npx -y getobsrv
49
+ obsrv install-skill # teach Claude Code when to use it
50
+ claude mcp add --scope user obsrv -- npx -y getobsrv mcp # give it the tools
51
+ ```
52
+
53
+ `install-skill` copies the [obsrv-screens skill](skills/obsrv-screens/SKILL.md)
54
+ into `~/.claude/skills/` (`--dest` for elsewhere, `--print` to pipe it into
55
+ another agent framework). The skill is what makes an agent reach for Obsrv on
56
+ its own when frontend work needs checking; the MCP registration is what gives
57
+ it the tools to do so. New sessions pick both up.
58
+
59
+ **Both together** — install the app *and* the tools, then flip **Agent control**
60
+ on in the app's toolbar: agent testing now drives the window you are watching
61
+ instead of rendering invisibly.
62
+
31
63
  ## Use
32
64
 
33
65
  ```bash
@@ -70,7 +102,8 @@ npx -y getobsrv diff http://localhost:5173 --preset laptop-768 --out-dir diffout
70
102
  `npx -y getobsrv --help` (or `node bin/obsrv.js --help` in a checkout) lists every preset, profile and flag. Diff findings
71
103
  are informational (exit 0); CI thresholds are the caller's job. A ready-made
72
104
  Claude Code skill that wraps the loop (snap matrix → read the PNGs → diff →
73
- fix → re-snap) lives at [skills/obsrv-screens/SKILL.md](skills/obsrv-screens/SKILL.md).
105
+ fix → re-snap) lives at [skills/obsrv-screens/SKILL.md](skills/obsrv-screens/SKILL.md);
106
+ `obsrv install-skill` copies it into `~/.claude/skills/` so agents find it.
74
107
 
75
108
  ### MCP server
76
109
 
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // `obsrv install-skill` — copy the packaged Claude Code skill into the user's
3
+ // skills directory, so an agent picks up the snap → look → diff → fix loop
4
+ // without being told about it. Plain Node: no Electron, no build needed.
5
+ 'use strict'
6
+
7
+ const { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } = require('node:fs')
8
+ const { homedir } = require('node:os')
9
+ const { join, resolve } = require('node:path')
10
+
11
+ const SKILL_NAME = 'obsrv-screens'
12
+ const source = join(__dirname, '..', 'skills', SKILL_NAME)
13
+
14
+ function usage() {
15
+ return `obsrv install-skill — install the ${SKILL_NAME} skill for Claude Code
16
+
17
+ Usage:
18
+ obsrv install-skill [flags]
19
+
20
+ Flags:
21
+ --dest <dir> Skills directory to install into (default ~/.claude/skills).
22
+ --force Overwrite an existing, different copy.
23
+ --print Write SKILL.md to stdout instead of installing.
24
+ --help Show this message.
25
+
26
+ Installs to <dest>/${SKILL_NAME}/. New Claude Code sessions pick the skill up;
27
+ sessions already running need a restart.`
28
+ }
29
+
30
+ /** Copies a directory tree. Shallow enough for a skill (SKILL.md + optional references). */
31
+ function copyTree(from, to) {
32
+ mkdirSync(to, { recursive: true })
33
+ for (const entry of readdirSync(from)) {
34
+ const src = join(from, entry)
35
+ const dst = join(to, entry)
36
+ if (statSync(src).isDirectory()) copyTree(src, dst)
37
+ else copyFileSync(src, dst)
38
+ }
39
+ }
40
+
41
+ /** True when every file in `from` exists in `to` with identical bytes. */
42
+ function sameTree(from, to) {
43
+ for (const entry of readdirSync(from)) {
44
+ const src = join(from, entry)
45
+ const dst = join(to, entry)
46
+ if (!existsSync(dst)) return false
47
+ if (statSync(src).isDirectory()) {
48
+ if (!statSync(dst).isDirectory() || !sameTree(src, dst)) return false
49
+ } else if (!readFileSync(src).equals(readFileSync(dst))) return false
50
+ }
51
+ return true
52
+ }
53
+
54
+ function main(argv) {
55
+ let dest = join(homedir(), '.claude', 'skills')
56
+ let force = false
57
+
58
+ for (let i = 0; i < argv.length; i++) {
59
+ const flag = argv[i]
60
+ if (flag === '--help' || flag === '-h') {
61
+ console.log(usage())
62
+ return 0
63
+ }
64
+ if (flag === '--print') {
65
+ process.stdout.write(readFileSync(join(source, 'SKILL.md'), 'utf8'))
66
+ return 0
67
+ }
68
+ if (flag === '--force') {
69
+ force = true
70
+ continue
71
+ }
72
+ if (flag === '--dest') {
73
+ const value = argv[++i]
74
+ if (!value) {
75
+ console.error('obsrv install-skill: --dest needs a directory')
76
+ return 2
77
+ }
78
+ dest = resolve(value)
79
+ continue
80
+ }
81
+ console.error(`obsrv install-skill: unknown flag: ${flag}\n\n${usage()}`)
82
+ return 2
83
+ }
84
+
85
+ if (!existsSync(source)) {
86
+ console.error(`obsrv install-skill: the packaged skill is missing (looked in ${source})`)
87
+ return 1
88
+ }
89
+
90
+ const target = join(dest, SKILL_NAME)
91
+ if (existsSync(target)) {
92
+ if (sameTree(source, target)) {
93
+ console.error(`obsrv install-skill: already up to date at ${target}`)
94
+ return 0
95
+ }
96
+ if (!force) {
97
+ console.error(
98
+ `obsrv install-skill: ${target} exists and differs — pass --force to overwrite it, ` +
99
+ 'or --dest to install elsewhere',
100
+ )
101
+ return 1
102
+ }
103
+ }
104
+
105
+ copyTree(source, target)
106
+ console.error(
107
+ `obsrv install-skill: installed to ${target}\n` +
108
+ 'New Claude Code sessions will pick it up; restart any session already running.',
109
+ )
110
+ return 0
111
+ }
112
+
113
+ process.exit(main(process.argv.slice(2)))
package/bin/obsrv.js CHANGED
@@ -20,6 +20,14 @@ if (process.argv[2] === 'mcp') {
20
20
  return
21
21
  }
22
22
 
23
+ // `obsrv install-skill` copies the packaged Claude Code skill into the user's
24
+ // skills directory. Also plain node — it never renders anything.
25
+ if (process.argv[2] === 'install-skill') {
26
+ process.argv.splice(2, 1)
27
+ require('./install-skill.js')
28
+ return
29
+ }
30
+
23
31
  const cliEntry = join(__dirname, '..', 'out', 'main', 'cli.js')
24
32
  if (!existsSync(cliEntry)) {
25
33
  console.error('obsrv: out/main/cli.js is missing — run `npm run build` in the Obsrv repo first')
package/out/cli/args.js CHANGED
@@ -22,6 +22,8 @@ function usage() {
22
22
  Usage:
23
23
  obsrv snap <url> [flags] Render <url> on a target screen; write a PNG, print JSON.
24
24
  obsrv diff <url> [flags] Render <url> at 1x and against a 2x reference; print JSON metrics.
25
+ obsrv mcp Serve the MCP server on stdio (for Claude Code and other clients).
26
+ obsrv install-skill Install the obsrv-screens skill for Claude Code (--help for flags).
25
27
 
26
28
  Shared flags:
27
29
  --preset <id> Screen preset (default ${exports.DEFAULT_PRESET}):
package/out/main/cli.js CHANGED
@@ -53,6 +53,8 @@ function usage() {
53
53
  Usage:
54
54
  obsrv snap <url> [flags] Render <url> on a target screen; write a PNG, print JSON.
55
55
  obsrv diff <url> [flags] Render <url> at 1x and against a 2x reference; print JSON metrics.
56
+ obsrv mcp Serve the MCP server on stdio (for Claude Code and other clients).
57
+ obsrv install-skill Install the obsrv-screens skill for Claude Code (--help for flags).
56
58
 
57
59
  Shared flags:
58
60
  --preset <id> Screen preset (default ${DEFAULT_PRESET}):
package/out/main/index.js CHANGED
@@ -660,6 +660,7 @@ function registerIpc(ctx) {
660
660
  });
661
661
  electron.ipcMain.handle(IPC.setViewport, (e, width, height, rawDsf) => {
662
662
  assertRenderer(e);
663
+ viewportArrived = true;
663
664
  const dsf = parseDeviceScaleFactor(rawDsf);
664
665
  if (dsf === null) throw new Error("invalid deviceScaleFactor");
665
666
  const v = target.setViewport(width, height, dsf);
@@ -714,6 +715,7 @@ function registerIpc(ctx) {
714
715
  waiter(report);
715
716
  });
716
717
  const scrollBoth = async (req) => {
718
+ await awaitViewportStable();
717
719
  const base = { x: req.x, y: req.y };
718
720
  if (req.selector !== void 0) base.selector = req.selector;
719
721
  if (!native.webContents.isDestroyed()) native.webContents.send(IPC.applyScroll, base);
@@ -786,6 +788,18 @@ function registerIpc(ctx) {
786
788
  }
787
789
  }
788
790
  });
791
+ let viewportPending = false;
792
+ let viewportArrived = false;
793
+ const VIEWPORT_ARRIVAL_MS = 600;
794
+ const awaitViewportStable = async () => {
795
+ if (!viewportPending) return;
796
+ viewportPending = false;
797
+ const arrivalDeadline = Date.now() + VIEWPORT_ARRIVAL_MS;
798
+ while (!viewportArrived && Date.now() < arrivalDeadline) {
799
+ await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
800
+ }
801
+ await settleTarget();
802
+ };
789
803
  const SETTLE_POLL_MS = 80;
790
804
  const SETTLE_STABLE_READS = 2;
791
805
  const SETTLE_BUDGET_MS = 4e3;
@@ -880,6 +894,10 @@ function registerIpc(ctx) {
880
894
  navigate: navigateBoth,
881
895
  apply: (patch) => {
882
896
  if (win.isDestroyed()) return;
897
+ if (patch.presetId !== void 0) {
898
+ viewportPending = true;
899
+ viewportArrived = false;
900
+ }
883
901
  if (!rendererReported) {
884
902
  if (pendingApplies.length >= MAX_PENDING_APPLIES) {
885
903
  if (!warnedPendingOverflow) {
@@ -894,12 +912,14 @@ function registerIpc(ctx) {
894
912
  win.webContents.send(IPC.agentApply, patch);
895
913
  },
896
914
  captureVisible: async () => {
915
+ await awaitViewportStable();
897
916
  await settleTarget();
898
917
  const image = await win.webContents.capturePage();
899
918
  const size = image.getSize();
900
919
  return { data: image.toPNG().toString("base64"), width: size.width, height: size.height };
901
920
  },
902
921
  captureTarget: async () => {
922
+ await awaitViewportStable();
903
923
  const settled = await settleTarget();
904
924
  const bounds = canvasBounds ?? targetBounds;
905
925
  const known = bounds !== null && bounds.width >= 1 && bounds.height >= 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getobsrv",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "See your site the way 1x screens see it",
5
5
  "main": "./out/main/index.js",
6
6
  "bin": {