@atlassian/chippy 0.0.1-beta.1 → 0.0.1-beta.2

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 (54) hide show
  1. package/README.md +142 -54
  2. package/bin/chippy.mjs +53 -95
  3. package/bin/pack.mjs +20 -0
  4. package/bin/skill-dir.mjs +15 -0
  5. package/lib/avalanche.js +54 -23
  6. package/lib/avalanche.js.map +2 -2
  7. package/lib/bootstrap.js +20 -1
  8. package/lib/bootstrap.js.map +3 -3
  9. package/lib/extension/avalanche.es.js +173 -34
  10. package/lib/extension/avalanche.es.js.map +1 -1
  11. package/lib/extension/chippy-page-bridge.js +164 -44
  12. package/lib/mcp-skill/README.md +2 -4
  13. package/lib/mcp-skill/SKILL.md +21 -16
  14. package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
  15. package/lib/mcp-skill/extension/avalanche.es.js +173 -34
  16. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -1
  17. package/lib/mcp-skill/extension/avalanche.iife.js +173 -34
  18. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -1
  19. package/lib/mcp-skill/extension/background.js +2 -0
  20. package/lib/mcp-skill/extension/chippy-page-bridge.js +164 -44
  21. package/lib/mcp-skill/extension/content-script.js +23 -0
  22. package/lib/mcp-skill/extension/devtools-hook-shim.js +18 -1
  23. package/lib/mcp-skill/extension/early-bootstrap.js +3 -1
  24. package/lib/mcp-skill/extension/manifest.json +1 -1
  25. package/lib/mcp-skill/extension/panel-settings.js +2 -20
  26. package/lib/mcp-skill/extension/startup-handoff.js +30 -0
  27. package/lib/mcp-skill/references/README.md +2 -2
  28. package/lib/mcp-skill/references/avalanche-detector.md +3 -3
  29. package/lib/mcp-skill/references/browser-runtime-api.md +4 -4
  30. package/lib/mcp-skill/references/command-output-formats.md +12 -2
  31. package/lib/mcp-skill/references/command-surface.md +6 -6
  32. package/lib/mcp-skill/references/mode-a-injection.md +43 -9
  33. package/lib/mcp-skill/references/playwright-cli-setup.md +5 -5
  34. package/lib/mcp-skill/references/playwright-probes.md +1 -1
  35. package/lib/mcp-skill/references/recipes.txt +1 -1
  36. package/lib/mcp-skill/references/stability-render.md +4 -4
  37. package/lib/mcp-skill/references/test-mode-playbook.md +2 -1
  38. package/lib/mcp-skill/references/troubleshooting.md +6 -1
  39. package/lib/mcp-skill/scripts/chippy-helper-server.js +116 -58
  40. package/lib/mcp-skill/scripts/chippy.sh +41 -7
  41. package/lib/mcp-skill/scripts/helper-access.js +20 -6
  42. package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
  43. package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
  44. package/lib/plugin.js.map +2 -2
  45. package/lib/storybook/connection-fallback.jsx +1 -1
  46. package/lib/storybook/manager-owner.js +42 -0
  47. package/lib/storybook/manager.jsx +22 -0
  48. package/lib/storybook/preview.js +53 -18
  49. package/lib/storybook/setup.js +15 -4
  50. package/lib/types/chippy-bridge/fiber-source.d.ts +14 -3
  51. package/lib/types/chippy-bridge/react-renderer.d.ts +30 -5
  52. package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
  53. package/lib/types/debug-render/core.d.ts +6 -0
  54. package/package.json +3 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- ![Chippy logo](.agents/skills/chippy/extension/icon-48.png)
1
+ ![Chippy logo](https://raw.githubusercontent.com/atlassian-labs/chippy/main/.agents/skills/chippy/extension/icon-48.png)
2
2
 
3
3
  # Chippy
4
4
 
5
- [![Atlassian license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](CONTRIBUTING.md)
5
+ [![Atlassian license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](https://github.com/atlassian-labs/chippy/blob/main/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/atlassian-labs/chippy/blob/main/CONTRIBUTING.md)
6
6
 
7
7
  **A friend who helps you understand what’s happening, find the cause, and fix it.**
8
8
 
@@ -18,7 +18,7 @@ on the page and leave it a note.
18
18
 
19
19
  Chippy works with standard React and needs no patched packages. Development
20
20
  builds give the most detail; production builds show less, and every answer says
21
- what was available. See [React compatibility](docs/react-compatibility.md).
21
+ what was available. See [React compatibility](https://github.com/atlassian-labs/chippy/blob/main/docs/react-compatibility.md).
22
22
 
23
23
  ## What you can ask
24
24
 
@@ -29,38 +29,43 @@ what was available. See [React compatibility](docs/react-compatibility.md).
29
29
 
30
30
  ## Install
31
31
 
32
- You need Node.js 22.12 or later and Chrome or another Chromium browser.
32
+ Chippy has three parts:
33
33
 
34
- 1. Install the agent skill:
34
+ - The **agent skill** tells your coding agent how to run Chippy.
35
+ - The **local helper** is a small server on `127.0.0.1:7823`. The agent starts
36
+ it when it needs it.
37
+ - The **page runtime** runs inside the page and reads React.
38
+
39
+ The skill and the helper are the same in every setup. You choose how the page
40
+ runtime gets into the page; see [Connect a page](#connect-a-page).
41
+
42
+ You need Node.js 22.20 or later, which the skills CLI requires.
43
+
44
+ 1. Install the agent skill with the [skills CLI](https://github.com/vercel-labs/skills):
35
45
 
36
46
  ```sh
37
- npx @atlassian/chippy install
47
+ npx skills add atlassian-labs/chippy --skill chippy -g
38
48
  ```
39
49
 
40
- This copies the skill to `~/.agents/skills/chippy` (run it again to update)
41
- and prints the next steps with the exact paths for your machine. To install
42
- it in a project instead, pass `--dir`, for example
43
- `npx @atlassian/chippy install --dir .agents/skills/chippy`; the paths below
44
- then start from that folder.
50
+ Choose your coding agents when it asks, or name them with `-a`, for example
51
+ `-a claude-code`. With `-g`, every project can use the skill, and its files
52
+ live in `~/.agents/skills/chippy`; each agent's skills folder links to that
53
+ copy. Leave out `-g` to install it in the current project's
54
+ `.agents/skills/chippy` instead. Run `npx skills update chippy` to update it.
45
55
 
46
- 2. Load the browser extension: open `chrome://extensions`, turn on
47
- **Developer mode**, choose **Load unpacked**, and select the `extension`
48
- folder the installer printed (`~/.agents/skills/chippy/extension` by
49
- default). The folder picker hides dot-folders, so paste the path instead of
50
- browsing: press **Cmd+Shift+G** on macOS or **Ctrl+L** on Linux, or use the
51
- address bar on Windows.
56
+ 2. Connect a page with one of the routes in [Connect a page](#connect-a-page).
52
57
 
53
- 3. Open your app, click the Chippy extension icon, and enable Chippy for the
54
- page's origin.
55
-
56
- 4. Ask your agent to use Chippy on the page.
58
+ 3. Ask your agent to use Chippy on the page.
57
59
 
58
60
  The agent runs Chippy through the skill's `chippy.sh` script, which needs bash
59
61
  (macOS, Linux, or WSL on Windows). You don't start anything yourself: the agent
60
62
  starts a local helper on `127.0.0.1:7823` when it needs one. A sandboxed agent
61
63
  asks for permission to reach it; approve the prompt or allow `chippy.sh`. If
62
- your agent runs in a container or a remote machine, see
63
- [MCP setup](.agents/skills/chippy/references/mcp.md) for reaching the helper.
64
+ your agent runs in a container or on a remote machine, the browser must reach
65
+ the helper's port through an SSH tunnel, a published container port, or other
66
+ forwarding your environment provides; Chippy does not set that up.
67
+ [Local and wide binds](https://github.com/atlassian-labs/chippy/blob/main/docs/trust-boundary.md#local-and-wide-binds)
68
+ lists what to set for each setup.
64
69
 
65
70
  **Optional: connect over MCP.** If your agent supports MCP, it can call Chippy
66
71
  as tools instead of running a script. Ask it:
@@ -69,27 +74,112 @@ as tools instead of running a script. Ask it:
69
74
 
70
75
  The agent adds the Chippy server to its own MCP configuration and checks the
71
76
  connection. Some agents need a restart before the new tools appear. To set it
72
- up yourself, see [MCP setup](.agents/skills/chippy/references/mcp.md).
77
+ up yourself, see [MCP setup](https://github.com/atlassian-labs/chippy/blob/main/.agents/skills/chippy/references/mcp.md).
78
+
79
+ ## Connect a page
80
+
81
+ Every route loads the same page runtime and talks to the same helper. The
82
+ routes differ in what you set up, whether Chippy stays after a reload, and
83
+ whether Chippy can record the initial page load.
84
+
85
+ | Route | What you set up | After a reload | Records the initial load |
86
+ | --- | --- | --- | --- |
87
+ | [Browser extension](#browser-extension) | Load the extension once, then enable it for each site | Chippy loads again | Yes |
88
+ | [Agent-driven browser](#agent-driven-browser) | Nothing. The agent opens its own Playwright browser | Chippy loads again | Yes, when the agent loads Chippy before the page |
89
+ | [Local proxy](#local-proxy) | Nothing. The agent starts the proxy, and you open the proxy address | Chippy loads again | Yes |
90
+ | [Storybook addon](#storybook-addon) | Add the addon to your Storybook configuration | Chippy remounts the story and stays | The story's mount |
91
+ | [Console import](#console-import) | Paste one line into the page's console | Chippy is removed. Paste the line again | No |
92
+ | [Your app's code](#your-apps-code) | Install the npm package and call `connectChippy()` | Call it again | No |
93
+
94
+ "Records the initial load" means that the agent can reload the page and record
95
+ the renders from the first commit. Every route records renders that happen
96
+ after Chippy connects.
97
+
98
+ ### Browser extension
99
+
100
+ Use the extension for day-to-day work in your own Chrome or Chromium browser.
101
+
102
+ 1. Open `chrome://extensions`, turn on **Developer mode**, and choose
103
+ **Load unpacked**. Select the skill's `extension` folder:
104
+ `~/.agents/skills/chippy/extension`, or `.agents/skills/chippy/extension`
105
+ in your project if you installed without `-g`. The folder picker hides
106
+ folders whose names start with a dot, so type the path instead of browsing.
107
+ On macOS, press **Cmd+Shift+G**, paste the path, press **Return**, and
108
+ choose **Select**. On Linux, press **Ctrl+L** and paste the path. On
109
+ Windows, paste it into the address bar. After `npx skills update`, click
110
+ **Reload** on the Chippy card in `chrome://extensions`.
111
+ 2. Open your app, click the Chippy extension icon, and enable Chippy for the
112
+ page's origin.
113
+
114
+ ### Agent-driven browser
115
+
116
+ The agent opens a Playwright browser, loads Chippy into the page, and drives
117
+ the page itself. No extension is needed. Use this route for repeatable flows,
118
+ headless checks, and CI. Ask your agent to open the page with Chippy in
119
+ Playwright; the skill has the setup steps.
120
+
121
+ ### Local proxy
73
122
 
74
- ## Two ways to start
123
+ The helper can sit in front of a dev server on your machine and add Chippy to
124
+ the page before the page's own scripts run. No extension is needed, and this
125
+ route works in any browser, including one that your agent drives. Ask your
126
+ agent to start the Chippy proxy for your page, then open the address that it
127
+ gives you on `127.0.0.1:7823`. The proxy forwards only to servers on this
128
+ machine, and it adds Chippy only to the page address that the agent gave it.
129
+ It removes the page's Content Security Policy headers, so it also works for
130
+ pages that block the console import.
75
131
 
76
- - **From the agent:** ask it about the page. It finds components by name, text,
77
- test ID, source file, or prop, reads their state, clicks through the page, and
78
- records renders.
79
- - **From the page:** click the Chippy button in the page corner and drag a probe
80
- marker (P1, or P2 to compare two elements) onto an element. Choose whether you
81
- want it explained, investigated, or changed, and add a note. Then ask the agent
82
- to read it, or to keep watching for new notes while you click around.
132
+ ### Storybook addon
83
133
 
84
- No extension? Once the agent has started the helper, paste this into the page's
85
- console:
134
+ In Storybook 10.3 or later, add the addon to your Storybook configuration:
135
+
136
+ ```js
137
+ export default {
138
+ addons: ['@atlassian/chippy/storybook'],
139
+ };
140
+ ```
141
+
142
+ The addon adds Chippy panels to Storybook and connects each story to your agent.
143
+ When the agent reloads the page, Storybook remounts the current story instead of
144
+ reloading the preview. See the
145
+ [package documentation](https://github.com/atlassian-labs/chippy/blob/main/docs/npm-package.md#storybook)
146
+ for build targets.
147
+
148
+ ### Console import
149
+
150
+ Use this for a quick look at a page where you cannot install the extension.
151
+ Once the agent has started the helper, paste this into the page's console:
86
152
 
87
153
  ```js
88
154
  import('http://127.0.0.1:7823/__chippy/inject.mjs')
89
155
  ```
90
156
 
91
- Repeat it after every reload. Sites with a strict Content Security Policy block
92
- it, and it can't record the initial page load; use the extension for that.
157
+ A reload removes Chippy, so paste the line again after every reload or
158
+ navigation. Sites with a strict Content Security Policy block the import.
159
+
160
+ ### Your app's code
161
+
162
+ For pages that the extension cannot reach, such as an embedded browser, call
163
+ `connectChippy()` from `@atlassian/chippy/browser` in the page. It loads the
164
+ same runtime as the console import. See
165
+ [Connect a browser page to an agent](https://github.com/atlassian-labs/chippy/blob/main/docs/npm-package.md#connect-a-browser-page-to-an-agent).
166
+
167
+ ## Ask the agent, or point at the page
168
+
169
+ - **Ask the agent** about the page. It finds components by name, text, test ID,
170
+ source file, or prop, reads their state, clicks through the page, and records
171
+ renders.
172
+ - **Point at the page:** click the Chippy button in the page corner and drag a
173
+ probe marker (P1, or P2 to compare two elements) onto an element. Choose
174
+ whether you want it explained, investigated, or changed, and add a note. Then
175
+ ask the agent to read it, or to keep watching for new notes while you click
176
+ around. The extension, the local proxy, and the Storybook addon show the
177
+ Chippy button. The console import does not.
178
+
179
+ <p align="center">
180
+ <a href="assets/chippy-call.mp4"><img src="assets/chippy-call.svg" width="900" alt="Animated clip: the asks roll in and Jira keeps them in line, so you call a chippy. Chippy gives your coding agent a hand in the running app: it finds the component and source line for a button to move, reads live props and state to show why a button stays disabled, compares before and after, finds what set off a slow click, connects the page, React, and your code, and turns the page into an experimentation lab."></a>
181
+ <br><a href="assets/chippy-call.mp4">Watch with sound</a>
182
+ </p>
93
183
 
94
184
  ## Evidence, then edits
95
185
 
@@ -99,20 +189,18 @@ side effects during render, say so and the agent uses read-only inspection
99
189
  instead. Chippy changes your source only when you ask it to, and checks the fix
100
190
  against the same page.
101
191
 
102
- ## Use Chippy from your own code
192
+ ## Use Chippy without an agent
103
193
 
104
- Install `@atlassian/chippy` in your project when the extension is not the right fit:
194
+ The `@atlassian/chippy` npm package also works with no agent, helper, or
195
+ extension:
105
196
 
106
197
  - **Catch re-render regressions in tests.** Record which components rendered
107
- during an interaction, and why, in a test or a script, with no agent, helper, or
108
- extension.
109
- - **Connect pages the extension cannot reach**, such as an embedded browser or a
110
- test runner, or connect from your app's startup code so Chippy sees the first
111
- render.
112
- - **Add Chippy to Storybook** as an addon.
198
+ during an interaction, and why, in a test or a script. Install the recorder in
199
+ your app's entry module before React DOM loads, and it records from the first
200
+ commit.
113
201
  - **Ship your own build** with extra plugins for your stack.
114
202
 
115
- See the [package documentation](docs/npm-package.md) for setup.
203
+ See the [package documentation](https://github.com/atlassian-labs/chippy/blob/main/docs/npm-package.md) for setup.
116
204
 
117
205
  ## How Chippy compares
118
206
 
@@ -171,19 +259,19 @@ change is verified.
171
259
 
172
260
  ## Learn more
173
261
 
174
- - [Agent skill](.agents/skills/chippy/SKILL.md) and its [references](.agents/skills/chippy/references/README.md)
175
- - [React compatibility](docs/react-compatibility.md)
176
- - [Plugins](docs/plugins.md) for app-specific knowledge
177
- - [Trust boundary](docs/trust-boundary.md): what page data Chippy sees and who can reach the helper
178
- - [Security policy](SECURITY.md)
179
- - [Changelog](CHANGELOG.md)
262
+ - [Agent skill](https://github.com/atlassian-labs/chippy/blob/main/.agents/skills/chippy/SKILL.md) and its [references](https://github.com/atlassian-labs/chippy/blob/main/.agents/skills/chippy/references/README.md)
263
+ - [React compatibility](https://github.com/atlassian-labs/chippy/blob/main/docs/react-compatibility.md)
264
+ - [Plugins](https://github.com/atlassian-labs/chippy/blob/main/docs/plugins.md) for app-specific knowledge
265
+ - [Trust boundary](https://github.com/atlassian-labs/chippy/blob/main/docs/trust-boundary.md): what page data Chippy sees and who can reach the helper
266
+ - [Security policy](https://github.com/atlassian-labs/chippy/blob/main/SECURITY.md)
267
+ - [Changelog](https://github.com/atlassian-labs/chippy/blob/main/CHANGELOG.md)
180
268
 
181
- To build, test, or change Chippy, see [Contributing](CONTRIBUTING.md) and the
182
- [code of conduct](CODE_OF_CONDUCT.md).
269
+ To build, test, or change Chippy, see [Contributing](https://github.com/atlassian-labs/chippy/blob/main/CONTRIBUTING.md) and the
270
+ [code of conduct](https://github.com/atlassian-labs/chippy/blob/main/CODE_OF_CONDUCT.md).
183
271
 
184
272
  ## License
185
273
 
186
274
  Copyright (c) 2026 Atlassian US., Inc.
187
- Apache 2.0 licensed, see [LICENSE](LICENSE) file.
275
+ Apache 2.0 licensed, see [LICENSE](https://github.com/atlassian-labs/chippy/blob/main/LICENSE) file.
188
276
 
189
277
  [![With ❤️ from Atlassian](https://raw.githubusercontent.com/atlassian-internal/oss-assets/master/banner-cheers.png)](https://www.atlassian.com)
package/bin/chippy.mjs CHANGED
@@ -1,121 +1,79 @@
1
1
  #!/usr/bin/env node
2
- // Installs the Chippy agent skill from this package and prints the next steps,
3
- // or packs a custom build with extra plugins (see pack.mjs).
4
- // npx @atlassian/chippy install [--dir <path>] [--force]
2
+ // Packs a custom Chippy build with extra plugins (see pack.mjs). The agent
3
+ // skill is installed with the skills CLI, not by this package.
5
4
  // npx @atlassian/chippy pack [--config <file>]
6
- import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
7
- import { homedir } from 'node:os';
5
+ import { readFileSync, realpathSync } from 'node:fs';
8
6
  import { dirname, join, resolve } from 'node:path';
9
7
  import { fileURLToPath } from 'node:url';
10
8
 
11
9
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
- const HEALTH_URL = 'http://127.0.0.1:7823/__chippy/health';
13
-
14
- export function defaultSkillDir() {
15
- return join(homedir(), '.agents', 'skills', 'chippy');
16
- }
17
-
18
- function isChippySkill(directory) {
19
- const skill = join(directory, 'SKILL.md');
20
- return existsSync(skill) && /^name:\s*chippy\s*$/m.test(readFileSync(skill, 'utf8'));
21
- }
22
-
23
- // Copies the bundled skill to `target`. An existing Chippy skill is replaced;
24
- // any other existing directory is left alone unless `force` is set.
25
- export function installSkill({ source = join(PACKAGE_ROOT, 'lib', 'mcp-skill'), target = defaultSkillDir(), force = false } = {}) {
26
- const destination = resolve(target);
27
- if (!isChippySkill(source)) throw new Error(`Bundled skill not found at ${source}; build the package first`);
28
- if ([homedir(), dirname(homedir()), '/', resolve('.')].includes(destination)) {
29
- throw new Error(`Choose a dedicated skill directory, not ${destination}`);
30
- }
31
- if (existsSync(destination) && !isChippySkill(destination) && !force) {
32
- throw new Error(`${destination} exists and is not a Chippy skill; pass --force to replace it`);
10
+ export const SKILL_INSTALL_COMMAND = 'npx skills add atlassian-labs/chippy --skill chippy -g';
11
+
12
+ const COMMAND_OPTIONS = { pack: ['--config'] };
13
+ const HELP = new Set(['--help', '-h', 'help']);
14
+ const VERSION = new Set(['--version', '-v']);
15
+
16
+ export function parseArgs(argv) {
17
+ const args = { command: argv[0], config: 'chippy.config.mjs', help: false, version: false };
18
+ if (HELP.has(args.command)) return { ...args, command: undefined, help: true };
19
+ if (VERSION.has(args.command)) return { ...args, command: undefined, version: true };
20
+ if (args.command === 'install') return args;
21
+ const allowed = COMMAND_OPTIONS[args.command] ?? [];
22
+ const value = (i) => {
23
+ if (i >= argv.length || argv[i].startsWith('-')) throw new Error(`${argv[i - 1]} needs a value`);
24
+ return argv[i];
25
+ };
26
+ for (let i = 1; i < argv.length; i++) {
27
+ if (HELP.has(argv[i])) args.help = true;
28
+ else if (VERSION.has(argv[i])) args.version = true;
29
+ else if (!allowed.includes(argv[i])) throw new Error(`Unknown option for ${args.command}: ${argv[i]}`);
30
+ else if (argv[i] === '--config') args.config = value(++i);
33
31
  }
34
- mkdirSync(dirname(destination), { recursive: true });
35
- rmSync(destination, { recursive: true, force: true });
36
- cpSync(source, destination, { recursive: true });
37
- return destination;
32
+ return args;
38
33
  }
39
34
 
40
- export function mcpConfig(skillDir) {
41
- return { mcpServers: { chippy: { command: 'node', args: [join(skillDir, 'scripts', 'chippy-mcp.cjs')] } } };
42
- }
35
+ function usage(name) {
36
+ return `Usage: npx ${name} pack [--config <file>]
37
+ npx ${name} --version
43
38
 
44
- async function helperStatus() {
45
- try {
46
- const response = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(1000) });
47
- return response.ok ? 'running' : `responded ${response.status}`;
48
- } catch {
49
- return 'not running (the MCP server or `chippy.sh start` starts it on demand)';
50
- }
39
+ pack Builds a custom skill and extension with extra plugins from a config
40
+ file (default chippy.config.mjs). See the package README.
41
+
42
+ Install the Chippy agent skill with the skills CLI:
43
+ ${SKILL_INSTALL_COMMAND}
44
+ `;
51
45
  }
52
46
 
53
- function parseArgs(argv) {
54
- const args = { command: argv[0], dir: undefined, force: false, config: 'chippy.config.mjs' };
55
- for (let i = 1; i < argv.length; i++) {
56
- if (argv[i] === '--force') args.force = true;
57
- else if (argv[i] === '--dir') args.dir = argv[++i];
58
- else if (argv[i] === '--config') args.config = argv[++i];
59
- else throw new Error(`Unknown option: ${argv[i]}`);
60
- }
61
- return args;
47
+ // The skill used to be installed by this package; point earlier instructions
48
+ // at the skills CLI instead of failing with a bare usage message.
49
+ function installMoved() {
50
+ return `This package no longer installs the Chippy skill. Install it with the skills CLI:
51
+ ${SKILL_INSTALL_COMMAND}
52
+ Drop -g to install it in the current project only.
53
+ `;
62
54
  }
63
55
 
64
56
  async function main(argv) {
65
- const { name } = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
57
+ const { name, version } = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
66
58
  const args = parseArgs(argv);
67
- if (args.command === 'pack') {
68
- await pack(args.config);
59
+ if (args.version) {
60
+ process.stdout.write(`${version}\n`);
69
61
  return;
70
62
  }
71
- if (args.command !== 'install') {
72
- process.stdout.write(`Usage: npx ${name} install [--dir <path>] [--force]
73
- npx ${name} pack [--config <file>]
74
-
75
- install Installs the Chippy agent skill (default ${defaultSkillDir()}).
76
- pack Builds a custom skill and extension with extra plugins from a config
77
- file (default chippy.config.mjs). See the package README.
78
- `);
79
- process.exitCode = args.command === undefined || args.command === '--help' ? 0 : 2;
63
+ if (args.command === 'install') {
64
+ process.stderr.write(installMoved());
65
+ process.exitCode = 1;
80
66
  return;
81
67
  }
82
- const skillDir = installSkill({ target: args.dir ?? defaultSkillDir(), force: args.force });
83
- process.stdout.write(`Installed the Chippy skill to ${skillDir}
84
-
85
- 1. Install the browser extension: open chrome://extensions, turn on Developer
86
- mode, choose Load unpacked, and select this folder:
87
- ${join(skillDir, 'extension')}${folderPickerHint(skillDir)}
88
- Without the extension, once your agent has started the helper, run this in
89
- the page's console:
90
- import('http://127.0.0.1:7823/__chippy/inject.mjs')
91
-
92
- 2. Enable Chippy for your page's origin in the extension popup, then ask your
93
- agent to use Chippy on the page. The agent runs
94
- ${join(skillDir, 'scripts', 'chippy.sh')}
95
-
96
- Optional: connect over MCP if your agent supports it. Ask your agent:
97
- Connect Chippy over MCP by following ${join(skillDir, 'references', 'mcp.md')}
98
- Or add this server to your agent's MCP configuration yourself:
99
- ${JSON.stringify(mcpConfig(skillDir), null, 2).replace(/^/gm, ' ')}
100
-
101
- Helper: ${await helperStatus()}
102
- `);
103
- }
104
-
105
- // The skill lives in a dot-folder, which folder pickers hide by default.
106
- const FOLDER_PICKER_HINT = {
107
- darwin: 'The folder is hidden in Finder: press Cmd+Shift+G in the picker and paste the path.',
108
- win32: 'Paste the path into the picker\'s address bar.',
109
- linux: 'The folder is hidden: press Ctrl+L in the picker and paste the path.',
110
- };
111
-
112
- function folderPickerHint(skillDir) {
113
- if (!skillDir.split(/[\\/]/).some((part) => part.startsWith('.'))) return '';
114
- return `\n ${FOLDER_PICKER_HINT[process.platform] ?? FOLDER_PICKER_HINT.linux}`;
68
+ if (args.help || !COMMAND_OPTIONS[args.command]) {
69
+ process.stdout.write(usage(name));
70
+ process.exitCode = args.help || args.command === undefined ? 0 : 2;
71
+ return;
72
+ }
73
+ await pack(args.config);
115
74
  }
116
75
 
117
76
  async function pack(configPath) {
118
- // Loaded on demand: install must not depend on the packer.
119
77
  const { loadPackConfig, packChippy } = await import('./pack.mjs');
120
78
  const result = await packChippy(await loadPackConfig(resolve(configPath)));
121
79
  process.stdout.write(`Packed Chippy:
package/bin/pack.mjs CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from 'node:fs';
9
9
  import { dirname, join, relative, resolve } from 'node:path';
10
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
11
+ import { ownsDirectory } from './skill-dir.mjs';
11
12
 
12
13
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
14
  const BUILTIN_PLUGINS = [
@@ -18,14 +19,32 @@ const RUNTIME_BUNDLES = ['extension/avalanche.iife.js', 'extension/avalanche.es.
18
19
  const BUILD_CONFIG_CONSUMERS = ['extension/background.js', 'extension/panel-settings.js'];
19
20
  const PLUGIN_ID = /^[a-z][a-z0-9-]*$/;
20
21
  const CHROME_VERSION = /^\d+(\.\d+){0,3}$/;
22
+ const CONFIG_KEYS = {
23
+ '': ['plugins', 'defaultPlugins', 'extension', 'skill', 'outDir'],
24
+ 'plugins[]': ['id', 'entry', 'label'],
25
+ extension: ['name', 'version', 'description', 'key'],
26
+ skill: ['overlay'],
27
+ };
21
28
 
22
29
  function fail(message) {
23
30
  throw new Error(`chippy pack: ${message}`);
24
31
  }
25
32
 
33
+ // A misspelled key would otherwise build silently without the setting.
34
+ function rejectUnknownKeys(value, keys, path) {
35
+ if (!value || typeof value !== 'object') return;
36
+ for (const key of Object.keys(value)) {
37
+ if (!keys.includes(key)) fail(`unknown config key ${path ? `${path}.` : ''}${key}; expected one of ${keys.join(', ')}`);
38
+ }
39
+ }
40
+
26
41
  /** Validates a pack config and resolves its paths against `baseDir`. */
27
42
  export function normalizePackConfig(config, baseDir) {
28
43
  if (!config || typeof config !== 'object') fail('the config must export an object');
44
+ rejectUnknownKeys(config, CONFIG_KEYS[''], '');
45
+ rejectUnknownKeys(config.extension, CONFIG_KEYS.extension, 'extension');
46
+ rejectUnknownKeys(config.skill, CONFIG_KEYS.skill, 'skill');
47
+ (config.plugins ?? []).forEach((plugin, index) => rejectUnknownKeys(plugin, CONFIG_KEYS['plugins[]'], `plugins[${index}]`));
29
48
  const plugins = (config.plugins ?? []).map((plugin, index) => {
30
49
  if (!plugin || typeof plugin !== 'object') fail(`plugins[${index}] must be { id, entry, label? }`);
31
50
  if (!PLUGIN_ID.test(plugin.id ?? '')) fail(`plugins[${index}].id must match ${PLUGIN_ID}`);
@@ -150,6 +169,7 @@ export async function packChippy(config, { source = join(PACKAGE_ROOT, 'lib', 'm
150
169
  const { plugins, defaultPlugins, extension, overlay, outDir } = config;
151
170
  const code = await bundlePlugins(plugins, defaultPlugins);
152
171
  const skillDir = join(outDir, 'chippy');
172
+ if (!ownsDirectory(skillDir)) fail(`${skillDir} exists and is not a Chippy skill; choose another outDir`);
153
173
  rmSync(skillDir, { recursive: true, force: true });
154
174
  mkdirSync(outDir, { recursive: true });
155
175
  cpSync(source, skillDir, {
@@ -0,0 +1,15 @@
1
+ // Ownership checks for the directories Chippy's CLI replaces. A directory is
2
+ // Chippy's to replace when it is empty or holds a Chippy skill; anything else
3
+ // belongs to someone else and is never deleted.
4
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export function isChippySkill(directory) {
8
+ const skill = join(directory, 'SKILL.md');
9
+ return existsSync(skill) && /^name:\s*chippy\s*$/m.test(readFileSync(skill, 'utf8'));
10
+ }
11
+
12
+ export function ownsDirectory(directory) {
13
+ return !existsSync(directory) || readdirSync(directory).length === 0 || isChippySkill(directory);
14
+ }
15
+