palato 0.1.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 ADDED
@@ -0,0 +1,103 @@
1
+ # palato
2
+
3
+ __Palato__ is an agent-agnostic taste profile tool. Answer two questions about your life, influences, and aesthetic — and wire that context into every AI coding agent you use.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm install palato -D
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ npx palato init
15
+ ```
16
+
17
+ The setup wizard detects your installed agents, scaffolds `PALATO.md` into your project, and wires it in automatically.
18
+
19
+ ## How it works
20
+
21
+ Palato drops a `PALATO.md` into your project — a structured profile of your design sensibilities, cultural references, and aesthetic rules. Each agent gets a copy so it knows what you care about before it writes a single line.
22
+
23
+ Instead of re-explaining your taste on every project, you write it once and every agent reads it.
24
+
25
+ ## Features
26
+
27
+ * Two-question setup — Captures the 20 things that most shape an artist's eye, distilled into two prompts
28
+ * Agent auto-config — Wires into Claude Code, Cursor, and Codex out of the box
29
+ * MCP server — Agents can query specific sections of your profile on demand via stdio
30
+ * Local-first — Your profile lives in your repo, nothing leaves your machine
31
+ * Zero lock-in — It's a markdown file. Read it, edit it, version it, delete it
32
+
33
+ ## Agent setup
34
+
35
+ ```
36
+ # Scaffold taste profile and configure agents
37
+ npx palato init
38
+
39
+ # Check everything is connected
40
+ npx palato doctor
41
+ ```
42
+
43
+ ## MCP server setup
44
+
45
+ The MCP server lets agents query your taste profile on demand via stdio — no port, no background process to manage. The agent spawns it automatically when it needs it.
46
+
47
+ **1. Add to your agent**
48
+
49
+ The easiest way — auto-detects and configures all supported agents:
50
+
51
+ ```
52
+ npx add-mcp "npx -y palato server"
53
+ ```
54
+
55
+ Or for Claude Code specifically:
56
+
57
+ ```
58
+ npx palato init
59
+ ```
60
+
61
+ **2. Or add manually to Claude Code**
62
+
63
+ In your `claude_desktop_config.json` or `.claude/settings.json`:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "palato": {
69
+ "command": "npx",
70
+ "args": ["-y", "palato", "server"]
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ **3. Verify**
77
+
78
+ ```
79
+ npx palato doctor
80
+ ```
81
+
82
+ ## MCP tools
83
+
84
+ Once configured, agents get access to:
85
+
86
+ | Tool | Description |
87
+ |------|-------------|
88
+ | `palato_get_profile` | Returns your full taste profile |
89
+ | `palato_get_section` | Returns a specific section |
90
+ | `palato_update` | Appends notes to a section |
91
+
92
+ ## Requirements
93
+
94
+ * Node 18+
95
+
96
+ ## Docs
97
+
98
+ Full documentation at __palato.dev__
99
+
100
+ ## License
101
+
102
+ © 2026
103
+ Licensed under PolyForm Shield 1.0.0
package/bin/palato.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { init } from '../src/cli/init.js';
4
+ import { doctor } from '../src/cli/doctor.js';
5
+ import { startServer } from '../src/mcp/server.js';
6
+
7
+ program
8
+ .name('palato')
9
+ .description('Wire your personal taste profile into any AI coding agent')
10
+ .version('0.1.0');
11
+
12
+ program
13
+ .command('init')
14
+ .description('Scaffold your taste profile and configure agents')
15
+ .action(init);
16
+
17
+ program
18
+ .command('doctor')
19
+ .description('Check palato is correctly configured')
20
+ .action(doctor);
21
+
22
+ program
23
+ .command('server')
24
+ .description('Start the MCP server (stdio transport)')
25
+ .action(startServer);
26
+
27
+ // parseAsync required — all action handlers are async
28
+ // parse() silently swallows rejections from async handlers
29
+ await program.parseAsync();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "palato",
3
+ "version": "0.1.0",
4
+ "description": "Wire your personal taste profile into any AI coding agent",
5
+ "type": "module",
6
+ "bin": {
7
+ "palato": "./bin/palato.js"
8
+ },
9
+ "scripts": {
10
+ "prepare": "chmod +x bin/palato.js"
11
+ },
12
+ "keywords": ["ai", "agent", "taste", "design", "mcp", "claude", "cursor"],
13
+ "author": "",
14
+ "license": "PolyForm-Shield-1.0.0",
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.0.0",
17
+ "zod": "^3.22.0",
18
+ "chalk": "^5.3.0",
19
+ "commander": "^12.0.0",
20
+ "inquirer": "^9.2.0",
21
+ "ora": "^8.0.1",
22
+ "fs-extra": "^11.2.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "files": [
28
+ "bin/",
29
+ "src/",
30
+ "README.md",
31
+ "LICENSE"
32
+ ]
33
+ }
@@ -0,0 +1,37 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export async function wireClaude() {
6
+ const cwd = process.cwd();
7
+
8
+ // 1. Symlink PALATO.md into .claude/skills so Claude Code reads it as a skill
9
+ const skillsDir = path.join(cwd, '.claude/skills');
10
+ await fs.ensureDir(skillsDir);
11
+ const dest = path.join(skillsDir, 'PALATO.md');
12
+ const src = path.join(cwd, 'PALATO.md');
13
+ if (!fs.existsSync(dest)) {
14
+ await fs.symlink(src, dest);
15
+ }
16
+
17
+ // 2. Write MCP server entry to ~/.claude/settings.json
18
+ const configPath = path.join(os.homedir(), '.claude', 'settings.json');
19
+ await fs.ensureDir(path.dirname(configPath));
20
+
21
+ let config = {};
22
+ try {
23
+ config = await fs.readJson(configPath);
24
+ } catch {
25
+ // File doesn't exist yet — start fresh
26
+ }
27
+
28
+ if (!config.mcpServers) config.mcpServers = {};
29
+
30
+ if (!config.mcpServers.palato) {
31
+ config.mcpServers.palato = {
32
+ command: 'npx',
33
+ args: ['-y', 'palato', 'server'],
34
+ };
35
+ await fs.writeJson(configPath, config, { spaces: 2 });
36
+ }
37
+ }
@@ -0,0 +1,12 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export async function wireCodex() {
5
+ const dest = path.join(process.cwd(), 'AGENTS.md');
6
+ const injection = `\n## Taste Profile\nSee [PALATO.md](./PALATO.md) for design preferences, cultural references, and aesthetic rules. Always apply these when generating UI or copy.\n`;
7
+ const existing = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
8
+
9
+ if (!existing.includes('PALATO')) {
10
+ await fs.appendFile(dest, injection);
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export async function wireCursor() {
5
+ const rcPath = path.join(process.cwd(), '.cursorrules');
6
+ const injection = `\n# palato — taste profile\n# See PALATO.md for full profile\n@PALATO.md\n`;
7
+ const existing = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, 'utf8') : '';
8
+
9
+ if (!existing.includes('palato')) {
10
+ await fs.appendFile(rcPath, injection);
11
+ }
12
+ }
@@ -0,0 +1,19 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export async function detectAgents() {
5
+ const detected = [];
6
+ const cwd = process.cwd();
7
+
8
+ if (fs.existsSync(path.join(cwd, '.claude'))) detected.push('claude');
9
+ if (
10
+ fs.existsSync(path.join(cwd, '.cursorrules')) ||
11
+ fs.existsSync(path.join(cwd, '.cursorrc'))
12
+ ) detected.push('cursor');
13
+ if (
14
+ fs.existsSync(path.join(cwd, 'AGENTS.md')) ||
15
+ process.env.OPENAI_API_KEY
16
+ ) detected.push('codex');
17
+
18
+ return detected;
19
+ }
@@ -0,0 +1,66 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ function checkClaudeMcpConfig() {
7
+ const configPath = path.join(os.homedir(), '.claude', 'settings.json');
8
+ try {
9
+ const config = fs.readJsonSync(configPath);
10
+ return !!(config?.mcpServers?.palato);
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export async function doctor() {
17
+ console.log(chalk.bold('\n palato doctor\n'));
18
+
19
+ const cwd = process.cwd();
20
+
21
+ const checks = [
22
+ {
23
+ label: 'PALATO.md exists',
24
+ pass: fs.existsSync(path.join(cwd, 'PALATO.md')),
25
+ fix: 'Run npx palato init',
26
+ },
27
+ {
28
+ label: 'Claude skill wired (.claude/skills/PALATO.md)',
29
+ pass: fs.existsSync(path.join(cwd, '.claude/skills/PALATO.md')),
30
+ fix: 'Run npx palato init and select Claude Code',
31
+ },
32
+ {
33
+ label: 'Claude Code MCP config contains palato (~/.claude/settings.json)',
34
+ pass: checkClaudeMcpConfig(),
35
+ fix: 'Run: npx add-mcp "npx -y palato server"',
36
+ },
37
+ {
38
+ label: '.cursorrules references palato',
39
+ pass: (() => {
40
+ try {
41
+ return fs.readFileSync(path.join(cwd, '.cursorrules'), 'utf8').includes('PALATO');
42
+ } catch { return false; }
43
+ })(),
44
+ fix: 'Run npx palato init and select Cursor',
45
+ },
46
+ {
47
+ label: 'AGENTS.md references palato',
48
+ pass: (() => {
49
+ try {
50
+ return fs.readFileSync(path.join(cwd, 'AGENTS.md'), 'utf8').includes('PALATO');
51
+ } catch { return false; }
52
+ })(),
53
+ fix: 'Run npx palato init and select Codex',
54
+ },
55
+ ];
56
+
57
+ for (const check of checks) {
58
+ if (check.pass) {
59
+ console.log(chalk.green(' ✓ ') + check.label);
60
+ } else {
61
+ console.log(chalk.red(' ✗ ') + check.label + chalk.dim(' → ' + check.fix));
62
+ }
63
+ }
64
+
65
+ console.log('');
66
+ }
@@ -0,0 +1,63 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import { wireClaude } from '../agents/claude.js';
7
+ import { wireCursor } from '../agents/cursor.js';
8
+ import { wireCodex } from '../agents/codex.js';
9
+ import { detectAgents } from './detect.js';
10
+
11
+ export async function init() {
12
+ console.log(chalk.bold('\n palato — taste profile setup\n'));
13
+
14
+ const detected = await detectAgents();
15
+
16
+ const { agents, confirm } = await inquirer.prompt([
17
+ {
18
+ type: 'checkbox',
19
+ name: 'agents',
20
+ message: 'Which agents should palato configure?',
21
+ choices: [
22
+ { name: 'Claude Code', value: 'claude', checked: detected.includes('claude') },
23
+ { name: 'Cursor', value: 'cursor', checked: detected.includes('cursor') },
24
+ { name: 'Codex / OpenAI', value: 'codex', checked: detected.includes('codex') },
25
+ ],
26
+ },
27
+ {
28
+ type: 'confirm',
29
+ name: 'confirm',
30
+ message: 'Scaffold PALATO.md into this project?',
31
+ default: true,
32
+ },
33
+ ]);
34
+
35
+ const spinner = ora('Setting up palato...').start();
36
+
37
+ if (confirm) {
38
+ const templatePath = new URL('../templates/PALATO.md', import.meta.url).pathname;
39
+ const dest = path.join(process.cwd(), 'PALATO.md');
40
+ if (!fs.existsSync(dest)) {
41
+ await fs.copy(templatePath, dest);
42
+ spinner.succeed(chalk.green('PALATO.md created — fill it in with your taste'));
43
+ } else {
44
+ spinner.warn('PALATO.md already exists — skipping');
45
+ }
46
+ }
47
+
48
+ for (const agent of agents) {
49
+ const s = ora(`Configuring ${agent}...`).start();
50
+ try {
51
+ if (agent === 'claude') await wireClaude();
52
+ if (agent === 'cursor') await wireCursor();
53
+ if (agent === 'codex') await wireCodex();
54
+ s.succeed(chalk.green(`${agent} configured`));
55
+ } catch (e) {
56
+ s.fail(`Failed to configure ${agent}: ${e.message}`);
57
+ }
58
+ }
59
+
60
+ console.log(chalk.bold('\n Next steps:'));
61
+ console.log(' 1. Edit ' + chalk.cyan('PALATO.md') + ' with your taste');
62
+ console.log(' 2. Run ' + chalk.cyan('npx palato doctor') + ' to verify setup\n');
63
+ }
@@ -0,0 +1,73 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { getProfile, getSection, updateSection } from './tools.js';
5
+
6
+ // StdioTransport uses stdout as the wire protocol.
7
+ // Any write to stdout corrupts the stream — all logging goes to stderr only.
8
+ const log = (...args) => process.stderr.write(args.join(' ') + '\n');
9
+
10
+ export async function startServer() {
11
+ const server = new McpServer({
12
+ name: 'palato',
13
+ version: '0.1.0',
14
+ });
15
+
16
+ server.tool(
17
+ 'palato_get_profile',
18
+ 'Returns the full taste profile from PALATO.md',
19
+ {},
20
+ async () => {
21
+ try {
22
+ const content = await getProfile();
23
+ return { content: [{ type: 'text', text: content }] };
24
+ } catch (e) {
25
+ log('[palato] palato_get_profile error:', e.message);
26
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
27
+ }
28
+ }
29
+ );
30
+
31
+ server.tool(
32
+ 'palato_get_section',
33
+ 'Returns a specific section of the taste profile',
34
+ {
35
+ section: z.string().describe('Section name e.g. "Visual Language", "Cultural References", "Anti-Patterns"'),
36
+ },
37
+ async ({ section }) => {
38
+ try {
39
+ const content = await getSection(section);
40
+ return { content: [{ type: 'text', text: content }] };
41
+ } catch (e) {
42
+ log('[palato] palato_get_section error:', e.message);
43
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
44
+ }
45
+ }
46
+ );
47
+
48
+ server.tool(
49
+ 'palato_update',
50
+ 'Appends a note or suggestion to a section of the taste profile',
51
+ {
52
+ section: z.string().describe('Section to update'),
53
+ note: z.string().describe('Note to append'),
54
+ },
55
+ async ({ section, note }) => {
56
+ try {
57
+ await updateSection(section, note);
58
+ return { content: [{ type: 'text', text: `Updated "${section}"` }] };
59
+ } catch (e) {
60
+ log('[palato] palato_update error:', e.message);
61
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
62
+ }
63
+ }
64
+ );
65
+
66
+ const transport = new StdioServerTransport();
67
+
68
+ process.on('uncaughtException', (e) => log('[palato] uncaughtException:', e.message));
69
+ process.on('unhandledRejection', (e) => log('[palato] unhandledRejection:', e));
70
+
71
+ await server.connect(transport);
72
+ log('[palato] MCP server running (stdio)');
73
+ }
@@ -0,0 +1,53 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ // Walk up from cwd until we find PALATO.md — same strategy git uses to find .git
5
+ function findProfilePath() {
6
+ let dir = process.cwd();
7
+ while (true) {
8
+ const candidate = path.join(dir, 'PALATO.md');
9
+ if (fs.existsSync(candidate)) return candidate;
10
+ const parent = path.dirname(dir);
11
+ if (parent === dir) return null;
12
+ dir = parent;
13
+ }
14
+ }
15
+
16
+ function profilePath() {
17
+ const found = findProfilePath();
18
+ if (!found) throw new Error('PALATO.md not found. Run npx palato init.');
19
+ return found;
20
+ }
21
+
22
+ export async function getProfile() {
23
+ try {
24
+ const p = profilePath();
25
+ return await fs.readFile(p, 'utf8');
26
+ } catch (e) {
27
+ return e.message;
28
+ }
29
+ }
30
+
31
+ export async function getSection(sectionName) {
32
+ const content = await getProfile();
33
+ const lines = content.split('\n');
34
+ const start = lines.findIndex(l => l.toLowerCase().includes(sectionName.toLowerCase()));
35
+ if (start === -1) return `Section "${sectionName}" not found.`;
36
+
37
+ const end = lines.findIndex((l, i) => i > start && l.startsWith('## '));
38
+ const slice = end === -1 ? lines.slice(start) : lines.slice(start, end);
39
+ return slice.join('\n').trim();
40
+ }
41
+
42
+ export async function updateSection(sectionName, note) {
43
+ const p = profilePath();
44
+ const content = await fs.readFile(p, 'utf8');
45
+ const lines = content.split('\n');
46
+ const idx = lines.findIndex(l => l.toLowerCase().includes(sectionName.toLowerCase()));
47
+ if (idx === -1) throw new Error(`Section "${sectionName}" not found in PALATO.md`);
48
+
49
+ const end = lines.findIndex((l, i) => i > idx && l.startsWith('## '));
50
+ const insertAt = end === -1 ? lines.length : end;
51
+ lines.splice(insertAt, 0, `- ${note}`);
52
+ await fs.writeFile(p, lines.join('\n'));
53
+ }
@@ -0,0 +1,69 @@
1
+ # My Taste Profile
2
+ > This file defines my aesthetic sensibility. AI agents should reference this when making any design, copy, or architectural decisions. Treat it as ground truth — not a suggestion.
3
+
4
+ ---
5
+
6
+ ## Where I Come From
7
+ <!-- Your childhood environment, cultural background, where you've lived, how you grew up, any hardship or defining experiences, your relationship with spirituality or philosophy, anything about your inner life that shaped how you see the world -->
8
+
9
+ -
10
+
11
+ ---
12
+
13
+ ## What Made Me
14
+ <!-- The artists, directors, writers, or thinkers you've studied or obsessed over, your formal training (or lack of it), the political or social era that formed you, your relationship with failure, love, and the work you wish you'd made -->
15
+
16
+ -
17
+
18
+ ---
19
+
20
+ ## Visual Language
21
+ <!-- Typography, color, spacing, shape language, density — how things should look and feel -->
22
+
23
+ - Typography:
24
+ - Color:
25
+ - Spacing / density:
26
+ - Shape language:
27
+ - Texture / materiality:
28
+
29
+ ---
30
+
31
+ ## Cultural References
32
+
33
+ ### Film & Directors
34
+ -
35
+
36
+ ### Art & Design
37
+ -
38
+
39
+ ### Music
40
+ -
41
+
42
+ ### Places & Architecture
43
+ -
44
+
45
+ ### Literature & Writing
46
+ -
47
+
48
+ ---
49
+
50
+ ## Anti-Patterns
51
+ <!-- Things you never want to see in anything built for you — be ruthless -->
52
+
53
+ -
54
+
55
+ ---
56
+
57
+ ## Voice & Copy
58
+
59
+ - Tone:
60
+ - What good writing sounds like:
61
+ - What I hate in copy:
62
+
63
+ ---
64
+
65
+ ## Code Aesthetic
66
+
67
+ - I prefer:
68
+ - I avoid:
69
+ - I refuse: