@penpair/core 0.3.1 → 0.3.3

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/dist/cli-main.js CHANGED
@@ -40,6 +40,7 @@ import { init } from "./instance/init.js";
40
40
  import { ConnectError, connectProject, keyFileCheck } from "./instance/connect.js";
41
41
  import { hostedOrigin, setup } from "./instance/setup.js";
42
42
  import { askHidden, askLine, openInBrowser } from "./instance/tty.js";
43
+ import { validateSkillAgent } from "./skill-hosts.js";
43
44
  import { loadAdapters, readRawConfig, reservedCommands, } from "./adapters.js";
44
45
  import { requireLocalProfile } from "./local-profile.js";
45
46
  import { assertEndpointIsInstance, doctorExternal, LifecycleError, } from "./instance/connection.js";
@@ -110,7 +111,7 @@ Compare:
110
111
  @@INSTANCE@@Skills (the rules an AI agent follows, shipped with penpair):
111
112
  penpair skills list them
112
113
  penpair skills install [--to <folder>] [--only <name>] [--force] [--agents AGENTS.md] [--dry-run]
113
- penpair skills check [--only <name>] [--agents AGENTS.md] [--json]
114
+ penpair skills check [--agent codex|claude|copilot] [--only <name>] [--agents <file>] [--json]
114
115
  (built-in help lists configured extensions without importing their trusted code)
115
116
  (config-free inspection requires --standalone; writes require a project)
116
117
  penpair --version`;
@@ -339,6 +340,9 @@ async function main(argv, cli) {
339
340
  agents: {
340
341
  type: "string",
341
342
  },
343
+ agent: {
344
+ type: "string",
345
+ },
342
346
  "dry-run": {
343
347
  type: "boolean",
344
348
  },
@@ -543,6 +547,7 @@ async function main(argv, cli) {
543
547
  "kit",
544
548
  ],
545
549
  skills: [
550
+ "agent",
546
551
  "to",
547
552
  "only",
548
553
  "force",
@@ -809,6 +814,9 @@ async function main(argv, cli) {
809
814
  ].some((value) => value !== undefined)) {
810
815
  throw new Error("Installation flags require skills install");
811
816
  }
817
+ if (values.agent !== undefined && (command !== "skills" || rest[0] !== "check")) {
818
+ throw new CommandArgumentError("--agent requires skills check");
819
+ }
812
820
  if (command === "skills" &&
813
821
  rest[0] !== "install" &&
814
822
  rest[0] !== "check" &&
@@ -1182,6 +1190,7 @@ async function main(argv, cli) {
1182
1190
  if (rest[0] === "check") {
1183
1191
  const report = await checkSkills({
1184
1192
  root,
1193
+ agent: validateSkillAgent(values.agent),
1185
1194
  agents: values.agents,
1186
1195
  only: values.only,
1187
1196
  });
@@ -1230,7 +1239,7 @@ async function main(argv, cli) {
1230
1239
  output.log("\nChoose --agents <path> to integrate these rows:\n" + report.agentsRows.join("\n"));
1231
1240
  }
1232
1241
  for (const old of report.legacy) {
1233
- output.log(`\nnote ${old.folder} is the old name of ${old.renamedTo}. ${old.carriedFacts ? "Your project-facts.md was copied to the new folder. " : ""}Delete the old folder and its links (.agents/skills/${path.posix.basename(old.folder)}, .claude/skills/${path.posix.basename(old.folder)}) and update its row in your AGENTS.md.`);
1242
+ output.log(`\nnote ${old.folder} is replaced by ${old.renamedTo}. ${old.carriedFacts ? "Your project-facts.md was copied to the new folder. " : ""}Review and migrate custom guidance before removing the old folder, its discovery links and stale instruction rows. No legacy files were deleted.`);
1234
1243
  }
1235
1244
  output.log("\nThen fill in references/project-facts.md in penpair-design: it is yours and is never replaced.");
1236
1245
  return report.instructions?.status === "choice-required" ? 3 : 0;
@@ -1,4 +1,5 @@
1
1
  import { type SkillInstructionCheck } from "./skill-check-instructions.js";
2
+ import { type SkillAgent } from "./skill-hosts.js";
2
3
  export interface SkillDiagnostic {
3
4
  name: string;
4
5
  path: string;
@@ -9,6 +10,7 @@ export interface SkillDiagnostic {
9
10
  }
10
11
  interface EntryOptions {
11
12
  root: string;
13
+ agent: SkillAgent;
12
14
  name: string;
13
15
  bundledDirectory: string;
14
16
  instructions: SkillInstructionCheck;
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { parseSkillMetadata, parseSkillYaml } from "./skill-metadata.js";
4
4
  import { readSkillFile } from "./skill-file.js";
5
5
  import { resolveSkillLink } from "./skill-check-instructions.js";
6
+ import { skillHosts } from "./skill-hosts.js";
6
7
  async function invocationAllowed(root, skillPath) {
7
8
  const relative = path.posix.join(path.posix.dirname(skillPath), "agents/openai.yaml");
8
9
  const present = await lstat(path.join(root, relative)).catch((error) => {
@@ -28,8 +29,8 @@ async function invocationAllowed(root, skillPath) {
28
29
  return allowed !== false;
29
30
  }
30
31
  export async function checkSkillEntry(options) {
31
- const { root, name, bundledDirectory, instructions } = options;
32
- const skillPath = path.posix.join(".agents/skills", name, "SKILL.md");
32
+ const { root, agent, name, bundledDirectory, instructions } = options;
33
+ const skillPath = path.posix.join(skillHosts[agent].folder, name, "SKILL.md");
33
34
  const report = {
34
35
  name,
35
36
  path: skillPath,
@@ -57,18 +58,35 @@ export async function checkSkillEntry(options) {
57
58
  for (const target of localLinks) {
58
59
  await resolveSkillLink(root, skillPath, target);
59
60
  }
60
- report.implicitInvocation = await invocationAllowed(root, skillPath);
61
- if (!report.implicitInvocation) {
62
- report.problems.push("Implicit invocation is disabled in openai.yaml; explicit invocation remains available.");
61
+ if (agent === "codex") {
62
+ report.implicitInvocation = await invocationAllowed(root, skillPath);
63
+ }
64
+ else if (agent === "claude") {
65
+ const frontmatter = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---/.exec(source);
66
+ const disabled = parseSkillYaml(frontmatter[1])["disable-model-invocation"];
67
+ if (disabled !== undefined && typeof disabled !== "boolean") {
68
+ throw new Error("Invalid Claude invocation policy");
69
+ }
70
+ report.implicitInvocation = disabled !== true;
71
+ }
72
+ if (report.implicitInvocation === false) {
73
+ report.problems.push("Implicit invocation is disabled by this host's skill policy; explicit invocation remains available.");
63
74
  }
64
75
  for (const folder of [
65
76
  ".skills",
66
77
  ".claude/skills",
78
+ ".agents/skills",
79
+ ".github/skills",
67
80
  ]) {
68
81
  const otherPath = path.posix.join(folder, name, "SKILL.md");
69
82
  const other = await readSkillFile(root, otherPath).catch(() => undefined);
70
83
  if (other !== undefined && other !== source) {
71
- report.problems.push("Codex and another installed skill copy differ; review the conflicting copies before refreshing.");
84
+ report.problems.push("Discoverable skill copies differ; review the conflicting copies before refreshing.");
85
+ }
86
+ else if (other !== undefined &&
87
+ (await realpath(path.join(root, otherPath))) !==
88
+ (await realpath(path.join(root, skillPath)))) {
89
+ report.problems.push("Independent skill copies require consolidation even when their contents match; preserve customizations before removing duplicates.");
72
90
  }
73
91
  }
74
92
  const routes = instructions.links[name] ?? [];
@@ -1,13 +1,16 @@
1
1
  import { type SkillDiagnostic } from "./skill-check-entry.js";
2
2
  import { type SkillInstructionCheck } from "./skill-check-instructions.js";
3
+ import { type SkillAgent } from "./skill-hosts.js";
3
4
  export interface SkillCheckOptions {
4
5
  root: string;
6
+ agent?: SkillAgent;
5
7
  agents?: string;
6
8
  only?: string[];
7
9
  }
8
10
  export interface SkillCheckReport {
9
- agent: "codex";
11
+ agent: SkillAgent;
10
12
  ready: boolean;
13
+ legacy: string[];
11
14
  instructions: Pick<SkillInstructionCheck, "path" | "problems">;
12
15
  skills: SkillDiagnostic[];
13
16
  limitations: string[];
@@ -1,8 +1,11 @@
1
- import { realpath } from "node:fs/promises";
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import path from "node:path";
2
3
  import { bundledSkillsDirectory, listBundledSkills } from "./skills.js";
3
4
  import { checkSkillEntry } from "./skill-check-entry.js";
4
5
  import { checkSkillInstructions } from "./skill-check-instructions.js";
6
+ import { skillHosts, validateSkillAgent } from "./skill-hosts.js";
5
7
  export async function checkSkills(options) {
8
+ const agent = validateSkillAgent(options.agent);
6
9
  const root = await realpath(options.root);
7
10
  const bundledDirectory = bundledSkillsDirectory();
8
11
  const names = await listBundledSkills(bundledDirectory);
@@ -10,18 +13,36 @@ export async function checkSkills(options) {
10
13
  if (!selected.length || selected.some((name) => !names.includes(name))) {
11
14
  throw new Error("Select one or more bundled skill names; use penpair skills to list them");
12
15
  }
13
- const instructions = await checkSkillInstructions(root, options.agents ?? "AGENTS.md");
16
+ const instructions = await checkSkillInstructions(root, options.agents ?? skillHosts[agent].instructions);
14
17
  const skills = await Promise.all([
15
18
  ...new Set(selected),
16
19
  ].map((name) => checkSkillEntry({
17
20
  root,
21
+ agent,
18
22
  name,
19
23
  bundledDirectory,
20
24
  instructions,
21
25
  })));
26
+ const legacy = [];
27
+ if (selected.includes("penpair-setup")) {
28
+ for (const folder of [
29
+ ".skills",
30
+ ".agents/skills",
31
+ ".claude/skills",
32
+ ".github/skills",
33
+ ]) {
34
+ const relative = path.posix.join(folder, "penpair-setup-guide");
35
+ if (await lstat(path.join(root, relative)).catch(() => undefined)) {
36
+ legacy.push(relative);
37
+ }
38
+ }
39
+ }
22
40
  return {
23
- agent: "codex",
24
- ready: !instructions.problems.length && skills.every((skill) => !skill.problems.length),
41
+ agent,
42
+ ready: !legacy.length &&
43
+ !instructions.problems.length &&
44
+ skills.every((skill) => !skill.problems.length),
45
+ legacy,
25
46
  instructions: {
26
47
  path: instructions.path,
27
48
  problems: instructions.problems,
@@ -29,7 +50,8 @@ export async function checkSkills(options) {
29
50
  skills,
30
51
  limitations: [
31
52
  "Offline project-file checks only; no connection, adapter/provider execution, credential reads or writes.",
32
- "Does not inspect global Codex settings, session skill catalogs, context budgets or UI selection badges.",
53
+ "Does not inspect global host settings, session skill catalogs, context budgets or UI selection badges.",
54
+ "Checks the selected instruction file, not whether the running host loaded it; Copilot invocation policy is not evaluated.",
33
55
  "A valid install and allowed invocation policy do not prove model selection, correct workflow application, visual quality or owner approval.",
34
56
  ],
35
57
  };
@@ -0,0 +1,16 @@
1
+ export declare const skillHosts: {
2
+ readonly codex: {
3
+ readonly folder: ".agents/skills";
4
+ readonly instructions: "AGENTS.md";
5
+ };
6
+ readonly claude: {
7
+ readonly folder: ".claude/skills";
8
+ readonly instructions: "CLAUDE.md";
9
+ };
10
+ readonly copilot: {
11
+ readonly folder: ".agents/skills";
12
+ readonly instructions: "AGENTS.md";
13
+ };
14
+ };
15
+ export type SkillAgent = keyof typeof skillHosts;
16
+ export declare function validateSkillAgent(value?: string): SkillAgent;
@@ -0,0 +1,20 @@
1
+ export const skillHosts = {
2
+ codex: {
3
+ folder: ".agents/skills",
4
+ instructions: "AGENTS.md",
5
+ },
6
+ claude: {
7
+ folder: ".claude/skills",
8
+ instructions: "CLAUDE.md",
9
+ },
10
+ copilot: {
11
+ folder: ".agents/skills",
12
+ instructions: "AGENTS.md",
13
+ },
14
+ };
15
+ export function validateSkillAgent(value = "codex") {
16
+ if (value === "codex" || value === "claude" || value === "copilot") {
17
+ return value;
18
+ }
19
+ throw new Error("Skill agent must be codex, claude or copilot");
20
+ }
package/dist/skills.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface InstallOptions {
18
18
  only?: string[];
19
19
  /** Replace files that differ from the shipped ones (never the project-owned ones). */
20
20
  force?: boolean;
21
- /** Make the skills discoverable for Codex and Claude Code with relative links (default true). */
21
+ /** Make one shared copy discoverable for Codex, Claude Code and Copilot (default true). */
22
22
  links?: boolean;
23
23
  /** For tests. */
24
24
  source?: string;
package/dist/skills.js CHANGED
@@ -71,22 +71,30 @@ export async function describeSkill(skillDirectory) {
71
71
  /** Skills that changed name. Installing the new one leaves the old folder behind, which `installSkills` then reports. */
72
72
  export const renamedSkills = {
73
73
  "penpot-design": "penpair-design",
74
+ "penpair-setup-guide": "penpair-setup",
74
75
  };
75
76
  /** Where the penpair skills already live in a project as real folders (not links), so a later install goes to the same place. */
76
77
  export async function existingSkillsFolder(root) {
77
- for (const folder of [
78
+ const folders = [
78
79
  ".skills",
79
80
  ".claude/skills",
80
81
  ".agents/skills",
81
- ]) {
82
- for (const name of [
82
+ ".github/skills",
83
+ ];
84
+ // Prefer an existing canonical pack over a legacy bootstrap in another host.
85
+ for (const names of [
86
+ [
83
87
  "penpair-design",
84
88
  "penpair-setup",
85
- ...Object.keys(renamedSkills),
86
- ]) {
87
- const found = await lstat(path.join(root, folder, name)).catch(() => undefined);
88
- if (found?.isDirectory()) {
89
- return folder;
89
+ ],
90
+ Object.keys(renamedSkills),
91
+ ]) {
92
+ for (const folder of folders) {
93
+ for (const name of names) {
94
+ const found = await lstat(path.join(root, folder, name)).catch(() => undefined);
95
+ if (found?.isDirectory()) {
96
+ return folder;
97
+ }
90
98
  }
91
99
  }
92
100
  }
@@ -197,6 +205,26 @@ export async function installSkills(options) {
197
205
  carriedFacts: carriedFacts && !options.dryRun,
198
206
  });
199
207
  }
208
+ // A standalone bootstrap guide may have been installed in a host's own folder.
209
+ // Report those copies for review; never delete a user's customized skill.
210
+ if (chosen.includes("penpair-setup")) {
211
+ for (const folder of [
212
+ ".skills",
213
+ ".agents/skills",
214
+ ".claude/skills",
215
+ ".github/skills",
216
+ ]) {
217
+ const legacyFolder = path.posix.join(folder, "penpair-setup-guide");
218
+ if (!report.legacy.some((entry) => entry.folder === legacyFolder) &&
219
+ (await exists(path.join(options.root, legacyFolder)))) {
220
+ report.legacy.push({
221
+ folder: legacyFolder,
222
+ renamedTo: "penpair-setup",
223
+ carriedFacts: false,
224
+ });
225
+ }
226
+ }
227
+ }
200
228
  if (options.dryRun) {
201
229
  report.wouldInstall = [
202
230
  ...writes.values(),
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in its own file so that the command line and the library entry can both read it without importing each other. Equals package.json (a test checks). */
2
- export declare const version = "0.3.1";
2
+ export declare const version = "0.3.3";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in its own file so that the command line and the library entry can both read it without importing each other. Equals package.json (a test checks). */
2
- export const version = "0.3.1";
2
+ export const version = "0.3.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penpair/core",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Design together. Build together. The library and engine behind penpair: connect AI agents and people to a Penpot design file, safely. Install @penpair/web or @penpair/docker for the command.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,63 +1,281 @@
1
1
  ---
2
2
  name: penpair-setup
3
- description: Install Penpair or connect and repair a project's Penpot setup, including a failing doctor check, suspended plugin or missing configuration. Use for connection and setup work; use penpair-design to edit designs and design-judgment to choose their composition.
3
+ description: Install Penpair through assisted or manual steps, connect an assistant to Penpot, or repair a failing doctor check, suspended plugin or missing configuration. Use for first setup and connection repair; use penpair-design for work in a connected design file.
4
4
  metadata:
5
5
  version: "0.1.0"
6
- scope: setup and connection
7
- audience: any AI agent helping a person get from nothing to a working Penpot connection
8
- compatibility: Node 22 and a terminal; a Penpot account on the web, or Docker Desktop for the on-computer variant.
6
+ audience: any AI coding assistant (Claude Code, Codex, GitHub Copilot) guiding a person through a first setup
7
+ compatibility: Node 22 or later and a terminal. A Penpot account on the web, or Docker Desktop for Penpot on this computer. The person does the browser steps and pastes their own key.
9
8
  ---
10
9
 
11
- # penpair-setup
10
+ # Penpair setup
12
11
 
13
- Two ways to run Penpot, one way to connect. **Web** (design.penpot.app, or a team's own Penpot) is the default: the package `@penpair/web`, nothing else to install. **Docker** (the package `@penpair/docker`, which includes everything in `@penpair/web`) puts a private Penpot on this computer: offline, own data, throwaway per project. Both end the same way: the person's personal key in `.env.local` as `PENPAIR_MCP_URL`, the Penpot file open in a visible browser tab with its plugin connected, and `penpair doctor` all `ok`.
12
+ You are the person's guide from "I have heard of penpair" to "my assistant just designed something in my Penpot file". penpair connects an AI assistant to a [Penpot](https://penpot.app) design file, with tested operations and written rules, so the assistant can build and check real pages. The person may be a designer who rarely opens a terminal, or a developer who has never used Penpot; read the cues and adjust your words.
14
13
 
15
- ## The key never passes through you
14
+ ## Choose how the person wants to install
16
15
 
17
- For an existing installation, use its local command and reviewed version
18
- (`pnpm exec penpair` in pnpm projects) in place of the first-use `npx` examples
19
- below. Preserve its chosen command package and current profile.
16
+ Installation route and Penpot hosting are separate choices. Web is the default hosting choice in either route; Docker remains optional.
20
17
 
21
- The MCP key is the person's password to their Penpot. Keep it out of chat, logs, commands and commits. The person pastes it themselves into a prompt that hides it (`penpair connect`), or pipes it from their clipboard (`pbpaste | npx @penpair/web connect`). Your shell cannot answer a hidden prompt, so hand that one step to the person with the exact words below.
18
+ - **Assisted — recommended:** use the guided steps below. Explain each action, run permitted project commands and hand browser/key steps to the person.
19
+ - **Manual:** when the person asks to install it themselves, read https://raw.githubusercontent.com/niklas-westman/penpair/main/docs/getting-started.md and give them the relevant commands and expected results. Do not execute installation, initialization, skill writes or connection commands for them. Run diagnostics only if they ask you to. Wait for their result when guiding one step at a time.
22
20
 
23
- ## Steps
21
+ Honor a route already stated. For an otherwise unspecified first installation, recommend assisted setup and offer manual; do not add this choice to a focused repair request. Read https://raw.githubusercontent.com/niklas-westman/penpair/main/docs/installation.md when explaining the alternatives, including the person-operated terminal wizard. If a reference cannot be read, say so and use the steps in this skill as the manual instructions without executing them.
24
22
 
25
- 1. **Look.** If `penpair.config.json` exists, run `npx @penpair/web doctor --json` and read its versioned envelope: `data.ok`, then `data.checks`. Done when you can name the first failing check, or say all pass. If all pass, skip connection repair and check the companion recommendations in step 7 before reporting setup complete.
26
- 2. **Choose where Penpot runs.** Recommend web. Choose Docker when the person asks for offline work, data that stays on this computer, or a disposable instance. Ask once, in one sentence, and take the answer. Done when the choice is stated. The decision table and what is checked: [references/web-or-docker.md](references/web-or-docker.md).
27
- 3. **Initialise.**
28
- - Web: `npx @penpair/web init --external https://design.penpot.app --gitignore` (a team's own Penpot: its address instead).
29
- - Docker: `npm i -D @penpair/docker` (the same command as `@penpair/web` with the Docker commands added; install it instead of `@penpair/web`, not beside it), then `npx penpair init --gitignore` and `npx penpair up` (the first start downloads about 2 GB; Docker Desktop must be running). Done when `up` says Penpot is up. For a person at a terminal, `npx @penpair/docker setup --docker` does it in one go.
30
- - The one-command alternative for a person at a terminal is `npx @penpair/web setup` (add `--web` or `--docker` to skip the question).
31
- 4. **Hand the browser steps to the person**, in these words, adapted to the choice:
32
- 1. Open Penpot (the address `init` or `up` printed), log in or create an account, create a design file.
33
- 2. Account, Integrations, MCP Server: switch it on and copy the address.
34
- 3. In your own terminal, in this folder, run `npx @penpair/web connect` and paste the address (nothing is shown as you paste).
35
- 4. In the design file: File, then MCP Server, then Connect. Keep that tab in front while we work.
36
- 5. **Verify.** Run `npx @penpair/web doctor --json` after the person says they are done. Done when `data.ok` is true and the check named `editor and plugin` says it is connected. A failing check carries a `remedy`; say it in plain words and let the person act. Common ones:
23
+ When switching routes, keep the chosen command package, package manager, config, credentials and completed steps. Inspect progress before running a new initialization; do not reset a working setup. Both routes need the same file pin, healthy doctor/pages checks and reviewed skill/instruction installation. Connection-ready is separate from comparison-ready. Manual instructions or a user-reported result are not checks you performed yourself.
37
24
 
38
- | Check says | Meaning and fix |
39
- | --- | --- |
40
- | docker: not answering | Docker Desktop is not running. Start it, wait until it says running. |
41
- | containers / backend not ready | Docker projects only: run `npx penpair up`; wait ten seconds; check again. |
42
- | editor and plugin: points at another instance | The key in `.env.local` belongs to a different Penpot. Run `npx @penpair/web connect --replace` and paste this Penpot's key. |
43
- | editor and plugin: not connected or suspended | The tab is asleep or the plugin is not connected. Do step 4.4 again; keep the tab in front. |
44
- | key file: git would commit it | Add `.env.local` to `.gitignore`; `penpair connect` does it. |
25
+ For the assisted route, work as a guide, not an installer: **one step at a time, one question at a time**. Say what you are about to do and why in a sentence, do it, show the result, and move on. Ask only the questions listed below, at the moment they matter. Wait for the person whenever a step needs their browser or their key; never guess that they are done.
45
26
 
46
- 6. **Install the rules.** Use the installed local command when available (`pnpm exec penpair` in pnpm projects). Preview `penpair skills install --agents AGENTS.md --dry-run`, then apply the reviewed update to the selected instruction file. Preserve project customizations and fill in the installed `penpair-design/references/project-facts.md` (file pin, token sets, naming, who approves); use the destination printed by the installer. Run `penpair skills check --json` for Codex discovery/routing diagnostics, selecting `--only` for intentional partial installs. A passing file check does not prove a session loaded or correctly applied a skill. Done when required project facts and diagnostics are ready, or their missing evidence is reported explicitly.
27
+ ## Repair an existing connection
47
28
 
48
- 7. **Recommend companions for the task.** Read the [adapter guide](https://github.com/niklas-westman/penpair/blob/main/docs/adapters.md). A library adapter brings the project's chosen component-library tokens and recipes into the workflow. When frontend implementation/comparison is in scope, recommend `@penpair/reflection-check`, installed at the same version as the command/core and merged into the existing `adapters` list. State the remaining requirements: approved baselines, explicit case mappings and a real browser provider. Record those as pending until configured and exercised; connection-ready does not mean comparison-ready. Design-only projects can defer comparison setup.
29
+ For a focused repair, skip first-install route and hosting questions. Keep the
30
+ installed version, package manager, profile and private credential reference.
31
+ Run the local `penpair doctor --json` only when execution is in scope; inspect
32
+ `data.ok` and `data.checks`, follow the first relevant remedy, then recheck. Do not
33
+ reinitialize a working config or replace credentials for an unrelated failure.
34
+ Once connected, check skill routing and relevant companion readiness separately.
35
+ For a manual request, give the diagnostic commands and interpret the person's
36
+ results without claiming to have run them.
49
37
 
50
- ## Moving between web and Docker
38
+ ## You may only have this file
51
39
 
52
- A project has one profile. To switch, keep the old `penpair.config.json` aside, run `init` again for the other kind, then `penpair connect --replace` with the new Penpot's key (keys belong to one Penpot; a key for the other one is refused). Designs do not move on their own: export from the old Penpot (the File menu's export, as `.penpot`) and import into the new one.
40
+ The person may have linked this file into a chat in their own project, with no copy of penpair anywhere. Everything you need to guide them is on this page; nothing has to be cloned. penpair is open source at https://github.com/niklas-westman/penpair and installs from npm. When you need more than this page, read these raw files (plain Markdown, fetchable):
41
+
42
+ | Read it for | URL |
43
+ | --- | --- |
44
+ | The overview, every package and command | https://raw.githubusercontent.com/niklas-westman/penpair/main/README.md |
45
+ | The same setup by hand, with browser steps | https://raw.githubusercontent.com/niklas-westman/penpair/main/docs/getting-started.md |
46
+ | Web or Docker, the full comparison | https://raw.githubusercontent.com/niklas-westman/penpair/main/packages/core/skills/penpair-setup/references/web-or-docker.md |
47
+ | Companion adapters: why, installation, registration and browser readiness | https://raw.githubusercontent.com/niklas-westman/penpair/main/docs/adapters.md |
48
+ | Extension points, snippets, instances | https://raw.githubusercontent.com/niklas-westman/penpair/main/docs/guide.md |
49
+
50
+ `npx penpair --help` lists every command of the installed version, and `npx penpair --version` says which one that is. The version on npm can be older than the repository; when this page and `--help` differ, `--help` is right for the person's install.
51
+
52
+ ### Which packages exist, and which to install
53
+
54
+ | Package | Install it when | What it gives |
55
+ | --- | --- | --- |
56
+ | `@penpair/web` | Penpot runs on the web (design.penpot.app or a team's server). The default | The `penpair` command and the library |
57
+ | `@penpair/docker` | Penpot should run in Docker on this computer | Everything in `@penpair/web`, plus `up`, `down`, `status`, `backup`, `restore`. Install it **instead of** `@penpair/web`, never beside it: both provide the `penpair` command |
58
+ | Library adapter from the companion guide | The project already uses a supported component library | Library-aware tokens, drift checks and recipes alongside the chosen command package |
59
+ | `@penpair/reflection-check` | Recommended when implementing a frontend from Penpot and verifying it in a browser | Approved design cases mapped to real browser/provider comparison evidence; install next to one command package at the same version |
60
+ | `@penpair/core` | Not for setup. Only for someone writing their own tooling on the library | Comes with the two command packages anyway |
61
+ | `penpair` (no scope) | Never: the name is not on npm yet | Nothing |
62
+
63
+ So one command package, the library companion when that library is used, and the recommended Reflection Check companion for frontend verification. Adapter installation and registration are separate from approved-baseline and browser-provider readiness. Install them as dev dependencies; then `npx penpair <command>` runs the project's own copy. **Without a local install, bare `npx penpair` fails** (that name is not on npm): use `npx @penpair/web <command>` (or `npx @penpair/docker <command>`) instead.
64
+
65
+ ### The commands this guide uses
66
+
67
+ | Command | What it does |
68
+ | --- | --- |
69
+ | `penpair init --external <address> --gitignore` | Web: writes `penpair.config.json` for that Penpot, keeps `.env.local` out of git |
70
+ | `penpair init --gitignore [--port 9002]` then `penpair up` | Docker: writes the config and a private `.penpair/` folder, then starts Penpot (about 2 GB the first time) |
71
+ | `penpair connect [--replace]` | Takes the person's MCP link through a hidden prompt (or a pipe), saves it in `.env.local`, then waits up to five minutes for the plugin |
72
+ | `penpair doctor [--json]` | Every check, `ok` or not, each with a `remedy`. `--json` is for you |
73
+ | `penpair pages` | The open file and its pages: the quickest proof the connection works |
74
+ | `penpair skills install` | Copies the agent rules (`penpair-setup`, `penpair-design`, `penpair-recipes`, `penpair-handoff`, `penpair-verify`, `design-judgment`) into the project and links them for Claude Code and Codex |
75
+ | `penpair setup` | The same path as a guided terminal wizard, for a person who prefers to do it alone. It asks questions you cannot answer for them, so you use the steps below instead |
76
+
77
+ Everything that changes a Penpot file looks first and writes only with `--apply`.
78
+
79
+ ## The key is the person's password
80
+
81
+ The MCP link Penpot gives the person contains a personal key. Anyone with it can edit their Penpot files. So:
82
+
83
+ - It never goes through you: not in chat, not in a command you run, not in a file you write, not in a commit.
84
+ - The person pastes it into `penpair connect`, which hides it and saves it to `.env.local` as `PENPAIR_MCP_URL` (readable only by them, git-ignored).
85
+ - If they paste it into the chat anyway, tell them plainly that it is now exposed and ask them to get a new key in Penpot (switch the MCP server off and on, or regenerate it if Penpot offers that, and check the address changed), then connect with `npx penpair connect --replace`. Do not repeat the link back.
86
+
87
+ This matters more than speed; a guide that leaks the key has failed even if everything else works.
88
+
89
+ ## The path
90
+
91
+ Keep a short checklist in your replies so the person sees where they are:
92
+
93
+ ```
94
+ 1 Check 2 Web or Docker 3 Account 4 Install 5 Plugin 6 MCP link 7 Verify 8 Companions 9 Rules 10 First POC
95
+ ```
96
+
97
+ ### 1. Check where things stand
98
+
99
+ - `node -v`: penpair needs 22 or later. If it is missing or older, point them to https://nodejs.org (the LTS installer) and wait.
100
+ - **Find the right folder.** penpair is set up per project, in the root of the project whose design it will hold. Look at where you are:
101
+ - **A project of theirs** (a `package.json`, an app): the normal case. Confirm it is the one they mean.
102
+ - **A clone of the penpair repository itself** (root `package.json` named `penpair-workspace`, a `packages/core` folder): that is penpair's source, not a place to set it up. Ask which project it is for, and work there. Installing from npm in that project is right even if they cloned penpair; linking the clone is only for people changing penpair.
103
+ - **No project at all** (an empty folder, or a designer who just wants to try it): offer to make one, for example `mkdir penpair-trial && cd penpair-trial && npm init -y`. A `package.json` is all penpair needs.
104
+ - **A monorepo:** install at the workspace root (pnpm needs `-w`, Yarn 1 needs `-W`), unless they want it in one package.
105
+ - Pick the package manager from the lockfile (`pnpm-lock.yaml`, `yarn.lock`, else npm) and use it in every install command. The commands here are written for npm.
106
+ - **Already started?** If `penpair.config.json` exists, install the project's dependencies first (`npm install`), then run `npx penpair doctor --json` (no local package in `package.json`? use `npx @penpair/web doctor --json`). All `ok` means penpair is set up: say so and jump to step 8. A failing check names a `remedy`; the usual ones are in step 7. Fix it and continue from the step it belongs to.
107
+
108
+ ### 2. Ask: Penpot on the web, or in Docker?
109
+
110
+ Ask once, recommend web, take the answer:
111
+
112
+ > Where should Penpot run? **On the web** (recommended): a free account at design.penpot.app or your team's own Penpot, nothing else to install, easy to share. **In Docker on this computer**: works offline and keeps the data on your machine, but needs Docker Desktop and a 2 GB download.
113
+
114
+ If they have a team Penpot, use its address everywhere this guide says `https://design.penpot.app`. The full comparison is in the web-or-Docker file listed above.
115
+
116
+ While they decide, set one expectation early: penpair works through the Penpot tab in their browser, so that tab has to stay open and visible while the assistant works. It cannot design unattended overnight.
117
+
118
+ ### 3. A Penpot account and a design file
119
+
120
+ - **Web:** they open https://design.penpot.app, sign up (with email or one of the sign-in buttons), confirm the email if asked, then create a project and a design file for this work. A fresh file is best for the first try.
121
+ - **Docker:** do step 4 first, because Penpot has to be running. Then they create the account on the local address `up` prints (no email confirmation there), and a project and a design file.
122
+
123
+ Wait until they say they have a design file open.
124
+
125
+ ### 4. Install penpair and write the config
126
+
127
+ Install it as a dev dependency so the whole team gets the same version. Install one command package, never both.
128
+
129
+ - **Web:**
130
+ ```bash
131
+ npm i -D @penpair/web
132
+ npx penpair init --external https://design.penpot.app --gitignore
133
+ ```
134
+ - **Docker:** Docker Desktop must be running (`docker info` answers).
135
+ ```bash
136
+ npm i -D @penpair/docker
137
+ npx penpair init --gitignore # add --port 9002 if 9001 is taken
138
+ npx penpair up # the first start downloads about 2 GB
139
+ ```
140
+ When `up` prints the address (for example http://127.0.0.1:9001), do step 3 there.
141
+
142
+ `init` writes `penpair.config.json` (no secrets, meant to be committed; `instance` is just a name, from the folder) and adds `.env.local` to `.gitignore`. Show them the config in a sentence or two. Some versions print `next: penpair connect` here; do step 5 first anyway, because `connect` waits for the plugin.
143
+
144
+ ### 5. Switch on MCP and connect the plugin
145
+
146
+ The plugin comes first because `penpair connect` (step 6) waits for it, and then confirms right away.
147
+
148
+ 1. In Penpot, open your **account menu** (your name or avatar), then **Integrations**, then **MCP Server**. Switch it on and **copy the address** it shows. It is your personal key; treat it like a password, and do not paste it here.
149
+ 2. Open the design file. Choose **File**, then **MCP Server**, then **Connect**. A small window may appear; leave it.
150
+ 3. **Keep this browser tab visible** while the assistant works. A tab in the background goes to sleep and the connection pauses; bringing it to the front resumes it.
151
+
152
+ ### 6. Where the MCP link goes
153
+
154
+ The link goes into `.env.local` in the project, as `PENPAIR_MCP_URL=…`. `penpair connect` puts it there for them: it takes the link through a hidden prompt, refuses one that belongs to a different Penpot, makes the file readable only by them and keeps it out of git. You cannot answer a hidden prompt, and you should not see the key anyway, so the person runs it, with the link from step 5.1 still on their clipboard:
155
+
156
+ - **Their own terminal**, in this project folder: `npx penpair connect`, paste, Enter. Nothing appears as they paste; that is on purpose.
157
+ - **In Claude Code**, without leaving the chat (macOS): type `! pbpaste | npx penpair connect`. It reads the clipboard, so the key never enters the conversation. (Linux: `xclip -selection clipboard -o` instead of `pbpaste`.)
158
+
159
+ When `connect` offers to pin the file, have the person check its name and ID before accepting. If they skipped it, verify the correct file is open before asking them to run `npx penpair connect --pin-file`; that flag accepts without another confirmation.
160
+
161
+ It ends with "Connected to the file …" and the file's pages. If it says it did not see the editor, the key is already saved: redo step 5.2 and 5.3, then run `npx penpair connect` again (it reuses the saved key). By hand, that single line in `.env.local` is all it is; the file must stay out of git.
162
+
163
+ ### 7. Verify
164
+
165
+ Run `npx penpair doctor --json` and read `data.ok` and each `data.checks` entry (the exit code is non-zero while anything fails). Done when `data.ok` is `true` and the `editor and plugin` check says connected. Then run `npx penpair pages` and read the page names back to them: proof that you can see their file.
166
+
167
+ A failing check carries a `remedy`; say it in plain words and let them act. The usual ones: Docker not running (start Docker Desktop), containers not running (`npx penpair up`), the key belongs to another Penpot (`npx penpair connect --replace` with this Penpot's link), plugin not connected or the tab is asleep (redo step 5.2 and 5.3).
168
+
169
+ **An address that is not theirs, before `connect` has run.** Until `.env.local` exists, penpair borrows a Penpot MCP server from the assistant's own configuration (`~/.codex/config.toml` or `~/.claude.json`, a server named `penpot`), and `doctor` names it: `(from codex-config)` or `(from claude-config)`. That is often an old local Penpot, for example `http://localhost:9001`, and the check fails with "points at … which is not …". It is not their mistake and nothing needs deleting: the order is environment, then `.env.local`, then those configurations, so step 6 fixes it. Its remedy says to put the address in `.env.local`; that is what `connect` does, so do not edit the file for them.
170
+
171
+ ### 8. Choose companion adapters
172
+
173
+ Explain the library and implementation-checking roles using the adapter guide above. Keep existing adapter entries and align directly installed versions with `npx penpair --version`.
174
+
175
+ **Library companion**
176
+
177
+ Inspect the application's actual dependencies and declared theme. Read the
178
+ companion guide above for an adapter matching the library already in use; follow
179
+ its maintained package/configuration instructions rather than inventing library
180
+ defaults. Explain the available integration and ask whether it belongs in the
181
+ setup. A library change is a separate project decision, not a prerequisite for
182
+ Penpair installation.
183
+
184
+ If selected, align the adapter version with the command/core, append its entry
185
+ without replacing the existing `adapters` list, and preserve project theme choices.
186
+ Preview token operations before writes and obtain authorization for the intended
187
+ Penpot target. Review ownership and designer-edit conflicts rather than silently
188
+ replacing intentional values. Record unsupported libraries or missing adapter
189
+ inputs as pending, then continue to frontend verification below.
190
+
191
+ **Frontend-verification companion**
192
+
193
+ When the requested workflow includes frontend implementation and visual checking, recommend `@penpair/reflection-check`. Explain: approved Penpot references plus explicit case mappings and a real project browser provider let the agent compare the implemented route/theme/state with the intended design. It can be used with or without the library companion.
194
+
195
+ Use the adapter guide for the matching-version install and append `"@penpair/reflection-check"` to the existing `adapters` array. Keep the project's package manager and dependency location. Record installed/registered separately from references approved, cases mapped, provider configured and browser verified. The package does not bundle a browser or configure the application's states. A design-only setup can defer it; a frontend handoff should explicitly recommend it and record missing integration work rather than leave the companion invisible.
196
+
197
+ Continue to the handoff skill once installed. A reusable browser starter is available at https://github.com/niklas-westman/penpair/blob/main/examples/reflection-browser/README.md; project-specific state hooks and a real provider are still required. No owner approval or browser result may be inferred from a successful install.
198
+
199
+ ### 9. Install the rules the assistant follows
200
+
201
+ Choose the instruction file the active host reads: `AGENTS.md` for Codex,
202
+ `CLAUDE.md` for Claude Code, or `.github/copilot-instructions.md` for Copilot's
203
+ native repository instructions. Preserve an existing shared-instruction strategy.
204
+ The commands below use Codex as an example; substitute both the destination and
205
+ `--agent claude` or `--agent copilot` for the active host. These are discovery
206
+ links to one skill pack, not separate host-specific copies. Use the project's
207
+ package manager and installed local command (`pnpm exec penpair` for pnpm).
208
+
209
+ **Just trying it?** Use a throwaway project folder, install the skills with an explicit instruction destination below and keep unknown project facts marked as pending. A commit can wait; the first design trial remains optional.
210
+
211
+ ```bash
212
+ npx penpair skills install --agents AGENTS.md --dry-run
213
+ npx penpair skills install --agents AGENTS.md
214
+ npx penpair skills check --agent codex --agents AGENTS.md --json
215
+ ```
216
+
217
+ Review the preview before applying. This installs setup, design, recipe, handoff, verification and judgment skills, creates discovery links and adds task routing to the selected instruction file. Use `CLAUDE.md` instead if that is the project's chosen file. Existing text outside the owned section is preserved. Discovery checks do not prove actual skill use in a session. Even in a trial folder, select the instruction destination explicitly so routing can be verified.
218
+
219
+ Then fill in `penpair-design/references/project-facts.md` (in the skills folder the install named) together with them: which Penpot file, the token sets, the names they use, who approves a design. Ask; do not invent. Placeholders they cannot answer yet can stay, marked as open.
220
+
221
+ In a git repository, offer to commit the setup (ask first; they may want to review it). No repository: skip this, or offer `git init` if they want one.
222
+
223
+ ```bash
224
+ git add penpair.config.json .gitignore AGENTS.md .skills .agents .claude # whichever exist; never .env.local
225
+ git commit -m "Add penpair"
226
+ ```
227
+
228
+ Teammates later run only `npm install` and `npx penpair connect` with their own key: the key belongs to a person, not to the repo.
229
+
230
+ ### 10. Optional first design trial
231
+
232
+ Offer a small proof of concept only after connection setup is complete and the person authorizes a scratch page and task. Setup alone does not authorize a design write. Otherwise stop at the readiness summary.
233
+
234
+ 1. **They make the page.** penpair works only on the page that is open in the editor, so ask them to add a page called **penpair POC** (the + next to Pages, in the left sidebar), click it so it is the open page, and keep the tab visible.
235
+ 2. **They pick what to build.** Offer two or three prompts; they should be able to say it in their own words next time:
236
+ - "Do a test POC: a sign-in screen for phone on the page penpair POC, with a small token set for its colours. Then export it and show me."
237
+ - "Do a test POC: a small dashboard with a header, three stat cards and a table. Audit it for off-scale spacing and show me the export."
238
+ - With a configured library adapter: "Do a test POC: a settings page that uses only our declared library tokens. Export it and show me."
239
+ 3. **Learn the rules first.** Read the installed `penpair-design` skill (`.skills/penpair-design/SKILL.md`, or https://raw.githubusercontent.com/niklas-westman/penpair/main/packages/core/skills/penpair-design/SKILL.md if step 9 was skipped) and its plugin gotchas before your first script. In short: every change to Penpot is a **project snippet**, a `.mjs` file whose default export has `inspect` (looks and returns what would change), `apply` (writes once, safe to run again) and `readback` (checks from scratch and returns `problems: []`). Each part is plain Penpot plugin code, under 90 KB.
240
+ 4. **Start from the example.** https://raw.githubusercontent.com/niklas-westman/penpair/main/skills/penpair-setup-guide/references/poc-snippet.mjs is a phone sign-in board with a small colour token set and a button component. Save it as `snippets/poc-sign-in.mjs` in the project. Its `page` constant must be the exact name of the page they opened; change it if they named it differently. Adapt the content to what they picked (and rename the snippet's `name` if it is no longer a sign-in).
241
+ 5. **Register it.** Add one key to `penpair.config.json`, keeping the JSON valid:
242
+ ```json
243
+ "snippets": ["snippets"]
244
+ ```
245
+ Then `npx penpair snippets check` should say `ok every snippet is valid plugin code and small enough`, and `npx penpair snippets` lists it as `[project]`.
246
+ 6. **Run it, look, then write.**
247
+ ```bash
248
+ npx penpair run poc-sign-in # looks only: what would change
249
+ npx penpair run poc-sign-in --apply # writes once, then reads back; problems must be empty
250
+ npx penpair tree --page "penpair POC" # find the board's id
251
+ npx penpair export <boardId> poc.png --page "penpair POC"
252
+ npx penpair audit --page "penpair POC" --prefix POC
253
+ ```
254
+ Export twice and look at the second image; the first after a write can be stale. The audit lists unbound colours, off-scale spacing, overflow and empty text: on this first POC, unbound colours are expected and make a good next step.
255
+ 7. **Show and explain.** Show the image and name what it proved: you read the real file, wrote in one verified step, made tokens and a component, checked your own work. Then what they can ask for next: bind the colours to the tokens, a tablet variant, an audit of an existing page, or Reflection baselines to check the coded page against the design (`penpair baselines`, in the README).
256
+
257
+ ## Wrapping up
258
+
259
+ Close with a short summary: where Penpot runs, what was installed, where the key lives (`.env.local`, never committed), which library companion is connected, if any, and the daily routine:
260
+
261
+ 1. Open the design file in Penpot, **File, MCP Server, Connect**, keep the tab visible.
262
+ 2. (Docker only) `npx penpair up` if the computer was restarted; `npx penpair down` to stop.
263
+ 3. When something feels off: `npx penpair doctor`.
264
+
265
+ Say plainly which steps were skipped or not verified, if any.
53
266
 
54
267
  ## Rules
55
268
 
56
- - Never run `up`, `down`, `restore` or `init` outside the project folder the person named.
57
- - Never run `penpair probe --deep --write` on a file the person has not called a scratch file; it creates and removes a temporary board.
58
- - Read `penpair --help` for the current commands; this skill names only the setup ones.
59
- - Report each check you did not run.
269
+ - Run commands only in the project folder the person confirmed. Never run `up`, `down`, `restore` or `init` anywhere else.
270
+ - Writes to their Penpot (a token push, the POC) happen only after they said yes, and the POC goes on its own new page.
271
+ - Never run `penpair probe --deep --write` unless they call the file a scratch file.
272
+ - If a command or menu here does not match what they see, trust what they see, check `npx penpair --help`, and tell them the guide is out of date at that point.
60
273
 
61
274
  ## Private references and rotation
62
275
 
63
276
  A person can configure `PENPAIR_MCP_URL_FILE` or a `connection.credential` file/macOS Keychain reference. Resolve it only at connection time and keep returned values out of messages. Connect reuses the reference without copying its URL into `.env.local`. Rotate that value in private storage, reconnect the intended visible editor and confirm the same public file pin with doctor. The legacy hidden `connect --replace` flow handles pasted URLs. Treat permission/history warnings as owner review tasks; retain historical metadata without printing old keys or rewriting history automatically.
277
+
278
+ ## Hosting reference
279
+
280
+ Read [web or Docker](references/web-or-docker.md) when comparing hosting choices.
281
+ The same assisted/manual installation routes apply to both.
@@ -1,6 +1,6 @@
1
1
  interface:
2
2
  display_name: "Penpair Setup"
3
- short_description: "Connect or repair a Penpot project"
4
- default_prompt: "Use $penpair-setup to diagnose this project's Penpot connection and report the remaining setup steps."
3
+ short_description: "Install Penpair or repair its connection"
4
+ default_prompt: "Use $penpair-setup to install Penpair using my preferred assisted or manual route, or diagnose this project's existing connection."
5
5
  policy:
6
6
  allow_implicit_invocation: true