@dayby/mcp-server 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -5,11 +5,10 @@ Post your dev progress to [DayBy.dev](https://dayby.dev) from Claude, Cursor, or
5
5
  ## How It Works
6
6
 
7
7
  ```
8
- You're coding with Claude learn something cool →
9
- draft_post (sanitized locally, never touches network)
10
- Claude shows you a clean preview →
11
- You say "ship it"
12
- publish_post → DayBy API (only sanitized content sent)
8
+ draft_post sanitized locally, never touches network
9
+ Claude shows you a clean preview
10
+ you approve
11
+ publish_post DayBy API (sanitized content only)
13
12
  ```
14
13
 
15
14
  **The raw context from your codebase never touches the network.** Only the sanitized, approved version gets published.
@@ -18,11 +17,14 @@ You're coding with Claude → learn something cool →
18
17
 
19
18
  | Tool | What it does | Touches network? |
20
19
  |---|---|---|
21
- | `draft_post` | Creates a sanitized draft from your description | No |
22
- | `edit_draft` | Modify a draft before publishing | No |
23
- | `check_content` | Dry-run: see what would get stripped | No |
24
- | `publish_post` | Publish an approved draft to DayBy | Yes (sanitized only) |
25
- | `list_posts` | List your recent DayBy posts | Yes |
20
+ | `draft_post` | Creates a sanitized draft from your description | No |
21
+ | `edit_draft` | Modify a draft before publishing | No |
22
+ | `check_content` | Dry-run: see what would get stripped | No |
23
+ | `publish_post` | Publish an approved draft to DayBy | Yes (sanitized only) |
24
+ | `list_posts` | List your recent DayBy posts | Yes |
25
+ | `get_post` | Fetch a single post by slug | Yes |
26
+ | `update_post` | Update title, content, or visibility | Yes |
27
+ | `delete_post` | Permanently delete a post | Yes |
26
28
 
27
29
  ## What Gets Stripped (Automatically)
28
30
 
@@ -33,70 +35,103 @@ You're coding with Claude → learn something cool →
33
35
  - SSH keys, JWTs, GitHub tokens
34
36
  - Database connection URLs
35
37
  - File paths with usernames
36
- - Plus anything you configure in blocklist
38
+ - Plus anything you configure in blocklist
37
39
 
38
40
  ## Setup
39
41
 
40
- ### 1. Get a DayBy API Key
42
+ ### 1. Install
41
43
 
42
- 1. Sign up at [dayby.dev](https://dayby.dev)
43
- 2. Go to Settings → API
44
- 3. Enable API access and generate a key
44
+ **Option A npx (no install needed):**
45
45
 
46
- ### 2. Configure Sanitizer (Optional but Recommended)
46
+ ```bash
47
+ npx @dayby/mcp-server
48
+ ```
47
49
 
48
- Create `~/.dayby/sanitizer.json`:
50
+ **Option B — global install:**
49
51
 
50
- ```json
51
- {
52
- "blockedTerms": ["YourCompany", "ProjectCodename"],
53
- "blockedDomains": ["internal.yourcompany.com"],
54
- "blockedNames": ["Your Boss Name"],
55
- "customPatterns": ["JIRA-\\d+", "INTERNAL-\\d+"]
56
- }
52
+ ```bash
53
+ npm install -g @dayby/mcp-server
57
54
  ```
58
55
 
59
- ### 3. Install
56
+ **Option C — from source:**
60
57
 
61
58
  ```bash
62
- npm install -g @dayby/mcp-server
59
+ git clone https://github.com/ja-roque/dayby-mcp-server.git
60
+ cd dayby-mcp-server
61
+ npm install && npm run build
63
62
  ```
64
63
 
65
- ### 4. Add to Claude Code / Claude Desktop / Cursor
64
+ ### 2. Authenticate
65
+
66
+ Run the auth command to connect your DayBy account:
67
+
68
+ ```bash
69
+ dayby-mcp auth
70
+ ```
71
+
72
+ This opens your browser for a one-click authorization. Your token is saved locally at `~/.dayby/credentials.json`.
73
+
74
+ To log out:
75
+
76
+ ```bash
77
+ dayby-mcp auth --logout
78
+ ```
79
+
80
+ Alternatively, you can set the `DAYBY_API_KEY` environment variable (from Settings > API on dayby.dev).
81
+
82
+ ### 3. Add to your MCP client
66
83
 
67
84
  **Claude Code (simplest):**
85
+
68
86
  ```bash
69
87
  claude mcp add dayby -- dayby-mcp
70
88
  ```
71
89
 
90
+ Or with npx:
91
+
92
+ ```bash
93
+ claude mcp add dayby -- npx @dayby/mcp-server
94
+ ```
95
+
72
96
  **Claude Desktop** (`claude_desktop_config.json`):
97
+
73
98
  ```json
74
99
  {
75
100
  "mcpServers": {
76
101
  "dayby": {
77
- "command": "dayby-mcp",
78
- "env": {
79
- "DAYBY_API_KEY": "your-api-key-here"
80
- }
102
+ "command": "npx",
103
+ "args": ["@dayby/mcp-server"]
81
104
  }
82
105
  }
83
106
  }
84
107
  ```
85
108
 
86
109
  **Cursor** (`.cursor/mcp.json`):
110
+
87
111
  ```json
88
112
  {
89
113
  "mcpServers": {
90
114
  "dayby": {
91
- "command": "dayby-mcp",
92
- "env": {
93
- "DAYBY_API_KEY": "your-api-key-here"
94
- }
115
+ "command": "npx",
116
+ "args": ["@dayby/mcp-server"]
95
117
  }
96
118
  }
97
119
  }
98
120
  ```
99
121
 
122
+ ### 4. Configure Sanitizer (Optional but Recommended)
123
+
124
+ Create `~/.dayby/sanitizer.json`:
125
+
126
+ ```json
127
+ {
128
+ "blockedTerms": ["YourCompany", "ProjectCodename"],
129
+ "blockedDomains": ["internal.yourcompany.com"],
130
+ "blockedNames": ["Your Boss Name"],
131
+ "customPatterns": ["JIRA-\\d+", "INTERNAL-\\d+"]
132
+ }
133
+ ```
134
+
100
135
  ## Usage Examples
101
136
 
102
137
  **While coding:**
@@ -114,11 +149,37 @@ Claude will use `draft_post` to sanitize locally, show you a preview, and only p
114
149
 
115
150
  | Variable | Description | Default |
116
151
  |---|---|---|
117
- | `DAYBY_API_KEY` | Your DayBy API key | (required) |
152
+ | `DAYBY_API_KEY` | Your DayBy API key (alternative to `dayby-mcp auth`) | (none) |
118
153
  | `DAYBY_API_URL` | DayBy API URL | `https://dayby.dev` |
119
154
  | `DAYBY_BLOCKED_TERMS` | Comma-separated blocked terms | (none) |
120
155
  | `DAYBY_BLOCKED_DOMAINS` | Comma-separated blocked domains | (none) |
121
156
 
157
+ ## Troubleshooting
158
+
159
+ **`dayby-mcp: command not found` after global install**
160
+
161
+ Your npm global bin isn't in your PATH. Run:
162
+
163
+ ```bash
164
+ source ~/.bashrc
165
+ ```
166
+
167
+ Or add this to your `~/.bashrc` / `~/.zshrc`:
168
+
169
+ ```bash
170
+ export PATH="$(npm bin -g):$PATH"
171
+ ```
172
+
173
+ Then restart your terminal or run `source ~/.bashrc` again.
174
+
175
+ **MCP server not showing up in Claude**
176
+
177
+ Restart Claude Code / Claude Desktop after adding the MCP config.
178
+
179
+ **`Not authenticated` errors**
180
+
181
+ Run `dayby-mcp auth` to connect your account, or set `DAYBY_API_KEY` in your environment.
182
+
122
183
  ## License
123
184
 
124
185
  MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * DayBy MCP — device authorization flow.
3
+ *
4
+ * Usage:
5
+ * dayby-mcp auth → interactive OAuth-style flow, saves token
6
+ * dayby-mcp auth --logout → clears saved credentials
7
+ */
8
+ interface Credentials {
9
+ token: string;
10
+ api_url: string;
11
+ }
12
+ export declare function loadCredentials(): Credentials | null;
13
+ export declare function clearCredentials(): void;
14
+ export declare function getStoredToken(apiUrl?: string): string | null;
15
+ export declare function runAuthFlow(apiUrl?: string): Promise<void>;
16
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * DayBy MCP — device authorization flow.
4
+ *
5
+ * Usage:
6
+ * dayby-mcp auth → interactive OAuth-style flow, saves token
7
+ * dayby-mcp auth --logout → clears saved credentials
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.loadCredentials = loadCredentials;
44
+ exports.clearCredentials = clearCredentials;
45
+ exports.getStoredToken = getStoredToken;
46
+ exports.runAuthFlow = runAuthFlow;
47
+ const fs = __importStar(require("fs"));
48
+ const os = __importStar(require("os"));
49
+ const path = __importStar(require("path"));
50
+ const child_process_1 = require("child_process");
51
+ // --- Config ---
52
+ const DEFAULT_API_URL = 'https://dayby.dev';
53
+ const CREDENTIALS_DIR = path.join(os.homedir(), '.dayby');
54
+ const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
55
+ const POLL_INTERVAL_MS = 2000;
56
+ // --- Storage ---
57
+ function loadCredentials() {
58
+ try {
59
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ function saveCredentials(creds) {
66
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
67
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
68
+ }
69
+ function clearCredentials() {
70
+ try {
71
+ fs.unlinkSync(CREDENTIALS_FILE);
72
+ console.log('Logged out. Credentials cleared.');
73
+ }
74
+ catch {
75
+ console.log('No credentials found.');
76
+ }
77
+ }
78
+ function getStoredToken(apiUrl = DEFAULT_API_URL) {
79
+ const creds = loadCredentials();
80
+ if (!creds)
81
+ return null;
82
+ // If the stored token is for a different server, ignore it
83
+ if (creds.api_url !== apiUrl)
84
+ return null;
85
+ return creds.token;
86
+ }
87
+ // --- Browser ---
88
+ function openBrowser(url) {
89
+ const display = process.env.DISPLAY || ':0';
90
+ const cmd = process.platform === 'darwin' ? `open "${url}"` :
91
+ process.platform === 'win32' ? `start "" "${url}"` :
92
+ `DISPLAY=${display} xdg-open "${url}"`;
93
+ (0, child_process_1.exec)(cmd, (err) => {
94
+ if (err) {
95
+ console.error(`Could not open browser: ${err.message}`);
96
+ console.log(`Please open this URL manually:\n ${url}`);
97
+ }
98
+ });
99
+ }
100
+ // --- Poll ---
101
+ async function pollForToken(apiUrl, code) {
102
+ const url = `${apiUrl}/api/v2/auth/device/${code}`;
103
+ for (;;) {
104
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
105
+ const res = await fetch(url);
106
+ if (res.status === 200) {
107
+ const data = await res.json();
108
+ return data.token;
109
+ }
110
+ if (res.status === 410) {
111
+ throw new Error('Authorization expired. Please run `dayby-mcp auth` again.');
112
+ }
113
+ if (res.status !== 202) {
114
+ throw new Error(`Unexpected response while waiting for authorization (${res.status}).`);
115
+ }
116
+ // 202 = still pending, keep polling
117
+ }
118
+ }
119
+ // --- Main flow ---
120
+ async function runAuthFlow(apiUrl = DEFAULT_API_URL) {
121
+ console.log('\n🔐 DayBy Authentication\n');
122
+ console.log(`Connecting to ${apiUrl}...`);
123
+ // 1. Request a device code
124
+ const res = await fetch(`${apiUrl}/api/v2/auth/device`, { method: 'POST' });
125
+ if (!res.ok) {
126
+ console.error(`\n❌ Could not reach DayBy (HTTP ${res.status}). Check your connection.\n`);
127
+ process.exit(1);
128
+ }
129
+ const { code, verification_uri, expires_in } = await res.json();
130
+ const expiresMinutes = Math.round(expires_in / 60);
131
+ // 2. Open browser
132
+ console.log('Opening DayBy in your browser...\n');
133
+ console.log(` URL: ${verification_uri}`);
134
+ console.log(` Code: ${code}`);
135
+ console.log(` Expires in ${expiresMinutes} minutes\n`);
136
+ openBrowser(verification_uri);
137
+ // 3. Poll until the user clicks Authorize
138
+ console.log('Waiting for you to authorize in the browser...');
139
+ const token = await pollForToken(apiUrl, code);
140
+ // 4. Save
141
+ saveCredentials({ token, api_url: apiUrl });
142
+ console.log('\n✅ Authenticated! You can now use DayBy MCP.\n');
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ // We test the exported functions directly
40
+ const auth_js_1 = require("./auth.js");
41
+ const CREDENTIALS_DIR = path.join(process.env.HOME || '~', '.dayby');
42
+ const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
43
+ const BACKUP_FILE = CREDENTIALS_FILE + '.test-backup';
44
+ (0, vitest_1.describe)('Auth', () => {
45
+ let originalCredentials = null;
46
+ (0, vitest_1.beforeEach)(() => {
47
+ // Back up existing credentials
48
+ try {
49
+ originalCredentials = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
50
+ }
51
+ catch {
52
+ originalCredentials = null;
53
+ }
54
+ });
55
+ (0, vitest_1.afterEach)(() => {
56
+ // Restore original credentials
57
+ if (originalCredentials) {
58
+ fs.writeFileSync(CREDENTIALS_FILE, originalCredentials, { mode: 0o600 });
59
+ }
60
+ });
61
+ (0, vitest_1.describe)('loadCredentials', () => {
62
+ (0, vitest_1.it)('returns null when no credentials file exists', () => {
63
+ // Temporarily rename the file
64
+ const tempPath = CREDENTIALS_FILE + '.tmp';
65
+ try {
66
+ fs.renameSync(CREDENTIALS_FILE, tempPath);
67
+ }
68
+ catch { }
69
+ try {
70
+ const result = (0, auth_js_1.loadCredentials)();
71
+ (0, vitest_1.expect)(result).toBeNull();
72
+ }
73
+ finally {
74
+ try {
75
+ fs.renameSync(tempPath, CREDENTIALS_FILE);
76
+ }
77
+ catch { }
78
+ }
79
+ });
80
+ (0, vitest_1.it)('returns credentials when file exists', () => {
81
+ const creds = (0, auth_js_1.loadCredentials)();
82
+ // If credentials exist on this machine, they should have token and api_url
83
+ if (creds) {
84
+ (0, vitest_1.expect)(creds).toHaveProperty('token');
85
+ (0, vitest_1.expect)(creds).toHaveProperty('api_url');
86
+ (0, vitest_1.expect)(typeof creds.token).toBe('string');
87
+ (0, vitest_1.expect)(typeof creds.api_url).toBe('string');
88
+ }
89
+ });
90
+ });
91
+ (0, vitest_1.describe)('getStoredToken', () => {
92
+ (0, vitest_1.it)('returns null for a non-matching API URL', () => {
93
+ const token = (0, auth_js_1.getStoredToken)('https://not-the-right-server.com');
94
+ (0, vitest_1.expect)(token).toBeNull();
95
+ });
96
+ (0, vitest_1.it)('returns token for the correct API URL', () => {
97
+ const token = (0, auth_js_1.getStoredToken)('https://dayby.dev');
98
+ // Token exists on this dev machine
99
+ if (token) {
100
+ (0, vitest_1.expect)(typeof token).toBe('string');
101
+ (0, vitest_1.expect)(token.length).toBeGreaterThan(0);
102
+ }
103
+ });
104
+ });
105
+ });
@@ -28,7 +28,11 @@ export interface PostsListResponse {
28
28
  export declare class DayByClient {
29
29
  private apiUrl;
30
30
  private apiKey;
31
- constructor(config: DayByConfig);
31
+ private resolveKey?;
32
+ constructor(config: DayByConfig & {
33
+ resolveKey?: () => string;
34
+ });
35
+ private getApiKey;
32
36
  private request;
33
37
  listPosts(page?: number, perPage?: number): Promise<PostsListResponse>;
34
38
  getPost(slug: string): Promise<{
@@ -38,6 +42,7 @@ export declare class DayByClient {
38
42
  title: string;
39
43
  content: string;
40
44
  visibility?: string;
45
+ tags?: string[];
41
46
  }): Promise<{
42
47
  post: DayByPost;
43
48
  }>;
@@ -45,9 +50,14 @@ export declare class DayByClient {
45
50
  title?: string;
46
51
  content?: string;
47
52
  visibility?: string;
53
+ tags?: string[];
48
54
  }): Promise<{
49
55
  post: DayByPost;
50
56
  }>;
57
+ updateArticle(slug: string, htmlArticle: string): Promise<{
58
+ post: DayByPost;
59
+ message: string;
60
+ }>;
51
61
  deletePost(slug: string): Promise<{
52
62
  message: string;
53
63
  }>;
@@ -7,14 +7,22 @@ exports.DayByClient = void 0;
7
7
  class DayByClient {
8
8
  apiUrl;
9
9
  apiKey;
10
+ resolveKey;
10
11
  constructor(config) {
11
12
  this.apiUrl = config.apiUrl.replace(/\/$/, '');
12
13
  this.apiKey = config.apiKey;
14
+ this.resolveKey = config.resolveKey;
15
+ }
16
+ getApiKey() {
17
+ if (this.resolveKey)
18
+ return this.resolveKey();
19
+ return this.apiKey;
13
20
  }
14
21
  async request(method, path, body) {
15
22
  const url = `${this.apiUrl}${path}`;
23
+ const apiKey = this.getApiKey();
16
24
  const headers = {
17
- 'Authorization': `Bearer ${this.apiKey}`,
25
+ 'Authorization': `Bearer ${apiKey}`,
18
26
  'Content-Type': 'application/json',
19
27
  'Accept': 'application/json',
20
28
  };
@@ -45,6 +53,9 @@ class DayByClient {
45
53
  post: params,
46
54
  });
47
55
  }
56
+ async updateArticle(slug, htmlArticle) {
57
+ return this.request('PUT', `/api/v2/posts/${slug}/article`, { html_article: htmlArticle });
58
+ }
48
59
  async deletePost(slug) {
49
60
  return this.request('DELETE', `/api/v2/posts/${slug}`);
50
61
  }
package/dist/index.d.ts CHANGED
@@ -6,5 +6,9 @@
6
6
  * All content is sanitized locally before it ever touches the network.
7
7
  *
8
8
  * Flow: draft_post → review → publish_post (nothing leaves without approval)
9
+ *
10
+ * Auth:
11
+ * Run `dayby-mcp auth` once to authorize via OAuth (no API key needed).
12
+ * Or set DAYBY_API_KEY env var for the legacy API key flow.
9
13
  */
10
14
  export {};
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@
7
7
  * All content is sanitized locally before it ever touches the network.
8
8
  *
9
9
  * Flow: draft_post → review → publish_post (nothing leaves without approval)
10
+ *
11
+ * Auth:
12
+ * Run `dayby-mcp auth` once to authorize via OAuth (no API key needed).
13
+ * Or set DAYBY_API_KEY env var for the legacy API key flow.
10
14
  */
11
15
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
16
  if (k2 === undefined) k2 = k;
@@ -47,16 +51,33 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
47
51
  const zod_1 = require("zod");
48
52
  const sanitizer_js_1 = require("./sanitizer.js");
49
53
  const dayby_client_js_1 = require("./dayby-client.js");
54
+ const auth_js_1 = require("./auth.js");
55
+ const visibility_js_1 = require("./visibility.js");
50
56
  const fs = __importStar(require("fs"));
57
+ const os = __importStar(require("os"));
51
58
  const path = __importStar(require("path"));
59
+ // --- Auth subcommand ---
60
+ async function handleAuthCommand() {
61
+ const args = process.argv.slice(3); // after "auth"
62
+ if (args.includes('--logout')) {
63
+ (0, auth_js_1.clearCredentials)();
64
+ return;
65
+ }
66
+ const apiUrl = process.env.DAYBY_API_URL || 'https://dayby.dev';
67
+ await (0, auth_js_1.runAuthFlow)(apiUrl);
68
+ }
52
69
  function loadConfig() {
53
- // 1. Check env vars
54
70
  const apiUrl = process.env.DAYBY_API_URL || 'https://dayby.dev';
55
- const apiKey = process.env.DAYBY_API_KEY || '';
56
- // 2. Load sanitizer config from file if it exists
71
+ // 1. Env var takes priority (legacy)
72
+ let apiKey = process.env.DAYBY_API_KEY || '';
73
+ // 2. Fall back to stored token from `dayby-mcp auth`
74
+ if (!apiKey) {
75
+ apiKey = (0, auth_js_1.getStoredToken)(apiUrl) || '';
76
+ }
77
+ // 3. Load sanitizer config from file if it exists
57
78
  let sanitizerConfig = {};
58
79
  const configPaths = [
59
- path.join(process.env.HOME || '~', '.dayby', 'sanitizer.json'),
80
+ path.join(os.homedir(), '.dayby', 'sanitizer.json'),
60
81
  path.join(process.cwd(), '.dayby-sanitizer.json'),
61
82
  ];
62
83
  for (const configPath of configPaths) {
@@ -69,7 +90,7 @@ function loadConfig() {
69
90
  // File doesn't exist, that's fine
70
91
  }
71
92
  }
72
- // 3. Also load from env (comma-separated)
93
+ // 4. Also load from env (comma-separated)
73
94
  if (process.env.DAYBY_BLOCKED_TERMS) {
74
95
  sanitizerConfig.blockedTerms = [
75
96
  ...(sanitizerConfig.blockedTerms || []),
@@ -90,9 +111,21 @@ function generateDraftId() {
90
111
  }
91
112
  // --- Main ---
92
113
  async function main() {
114
+ // Handle `dayby-mcp auth [--logout]` subcommand
115
+ if (process.argv[2] === 'auth') {
116
+ await handleAuthCommand();
117
+ process.exit(0);
118
+ }
93
119
  const config = loadConfig();
94
120
  const sanitizer = new sanitizer_js_1.Sanitizer(config.sanitizer);
95
- const client = new dayby_client_js_1.DayByClient({ apiUrl: config.apiUrl, apiKey: config.apiKey });
121
+ const client = new dayby_client_js_1.DayByClient({
122
+ apiUrl: config.apiUrl,
123
+ apiKey: config.apiKey,
124
+ resolveKey: () => {
125
+ // Re-check env and stored credentials on every call
126
+ return process.env.DAYBY_API_KEY || (0, auth_js_1.getStoredToken)(config.apiUrl) || config.apiKey;
127
+ },
128
+ });
96
129
  const server = new mcp_js_1.McpServer({
97
130
  name: 'dayby',
98
131
  version: '0.1.0',
@@ -105,7 +138,8 @@ async function main() {
105
138
  title: zod_1.z.string().describe('Post title — focus on the technology/skill learned'),
106
139
  content: zod_1.z.string().describe('Post content — describe what you learned, built, or solved. The sanitizer will strip any sensitive data automatically.'),
107
140
  visibility: zod_1.z.enum(['published', 'draft']).default('published').describe('Post visibility on DayBy'),
108
- }, async ({ title, content, visibility }) => {
141
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe('Tags/project names for this post (e.g., ["playflow", "rust"]). Used to filter posts by project in the public API.'),
142
+ }, async ({ title, content, visibility, tags }) => {
109
143
  // Sanitize both title and content locally
110
144
  const titleResult = sanitizer.sanitize(title);
111
145
  const contentResult = sanitizer.sanitize(content);
@@ -118,6 +152,7 @@ async function main() {
118
152
  sanitizedContent: contentResult.clean,
119
153
  strippedItems: allStripped,
120
154
  visibility,
155
+ tags: tags || [],
121
156
  createdAt: new Date(),
122
157
  };
123
158
  drafts.set(draftId, draft);
@@ -126,6 +161,9 @@ async function main() {
126
161
  response += `**Title:** ${draft.sanitizedTitle}\n\n`;
127
162
  response += `**Content:**\n${draft.sanitizedContent}\n\n`;
128
163
  response += `**Visibility:** ${visibility}\n`;
164
+ if (draft.tags.length > 0) {
165
+ response += `**Tags:** ${draft.tags.join(', ')}\n`;
166
+ }
129
167
  if (allStripped.length > 0) {
130
168
  response += `\n⚠️ **Sanitizer removed ${allStripped.length} sensitive item(s):**\n`;
131
169
  for (const item of allStripped.slice(0, 10)) {
@@ -151,7 +189,8 @@ async function main() {
151
189
  title: zod_1.z.string().optional().describe('Updated title (will be re-sanitized)'),
152
190
  content: zod_1.z.string().optional().describe('Updated content (will be re-sanitized)'),
153
191
  visibility: zod_1.z.enum(['published', 'draft']).optional().describe('Updated visibility'),
154
- }, async ({ draft_id, title, content, visibility }) => {
192
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe('Updated tags/project names'),
193
+ }, async ({ draft_id, title, content, visibility, tags }) => {
155
194
  const draft = drafts.get(draft_id);
156
195
  if (!draft) {
157
196
  return {
@@ -172,6 +211,9 @@ async function main() {
172
211
  if (visibility) {
173
212
  draft.visibility = visibility;
174
213
  }
214
+ if (tags) {
215
+ draft.tags = tags;
216
+ }
175
217
  let response = `✏️ **Draft Updated** (ID: ${draft_id})\n\n`;
176
218
  response += `**Title:** ${draft.sanitizedTitle}\n\n`;
177
219
  response += `**Content:**\n${draft.sanitizedContent}\n\n`;
@@ -193,11 +235,12 @@ async function main() {
193
235
  content: [{ type: 'text', text: `❌ Draft not found: ${draft_id}. Use draft_post to create a new one.` }],
194
236
  };
195
237
  }
196
- if (!config.apiKey) {
238
+ const currentKey = process.env.DAYBY_API_KEY || (0, auth_js_1.getStoredToken)(config.apiUrl) || config.apiKey;
239
+ if (!currentKey) {
197
240
  return {
198
241
  content: [{
199
242
  type: 'text',
200
- text: '❌ No API key configured. Set DAYBY_API_KEY environment variable or add it to your MCP config.',
243
+ text: '❌ Not authenticated. Run `dayby-mcp auth` in your terminal to connect your DayBy account.',
201
244
  }],
202
245
  };
203
246
  }
@@ -206,7 +249,8 @@ async function main() {
206
249
  const result = await client.createPost({
207
250
  title: draft.sanitizedTitle,
208
251
  content: draft.sanitizedContent,
209
- visibility: draft.visibility,
252
+ visibility: (0, visibility_js_1.toApiVisibility)(draft.visibility),
253
+ tags: draft.tags.length > 0 ? draft.tags : undefined,
210
254
  });
211
255
  let response = `✅ **Published to DayBy!**\n\n`;
212
256
  response += `**Title:** ${result.post.title}\n`;
@@ -241,9 +285,9 @@ async function main() {
241
285
  page: zod_1.z.number().default(1).describe('Page number'),
242
286
  per_page: zod_1.z.number().default(10).describe('Posts per page'),
243
287
  }, async ({ page, per_page }) => {
244
- if (!config.apiKey) {
288
+ if (!(process.env.DAYBY_API_KEY || (0, auth_js_1.getStoredToken)(config.apiUrl) || config.apiKey)) {
245
289
  return {
246
- content: [{ type: 'text', text: '❌ No API key configured.' }],
290
+ content: [{ type: 'text', text: '❌ Not authenticated. Run `dayby-mcp auth` to connect your DayBy account.' }],
247
291
  };
248
292
  }
249
293
  try {
@@ -252,7 +296,7 @@ async function main() {
252
296
  for (const post of result.posts) {
253
297
  response += `• **${post.title}** — ${post.url}\n`;
254
298
  response += ` ${post.content.slice(0, 100)}${post.content.length > 100 ? '...' : ''}\n`;
255
- response += ` _${post.created_at.slice(0, 10)} · ${post.visibility}_\n\n`;
299
+ response += ` _${post.created_at.slice(0, 10)} · ${(0, visibility_js_1.fromApiVisibility)(post.visibility)}_\n\n`;
256
300
  }
257
301
  return { content: [{ type: 'text', text: response }] };
258
302
  }
@@ -271,8 +315,8 @@ async function main() {
271
315
  server.tool('get_post', 'Get a single DayBy post by its slug.', {
272
316
  slug: zod_1.z.string().describe('The post slug'),
273
317
  }, async ({ slug }) => {
274
- if (!config.apiKey) {
275
- return { content: [{ type: 'text', text: '❌ No API key configured.' }] };
318
+ if (!(process.env.DAYBY_API_KEY || (0, auth_js_1.getStoredToken)(config.apiUrl) || config.apiKey)) {
319
+ return { content: [{ type: 'text', text: '❌ Not authenticated. Run `dayby-mcp auth` to connect your DayBy account.' }] };
276
320
  }
277
321
  try {
278
322
  const result = await client.getPost(slug);
@@ -280,7 +324,7 @@ async function main() {
280
324
  let response = `📄 **${post.title}**\n\n`;
281
325
  response += `**Slug:** ${post.slug}\n`;
282
326
  response += `**URL:** ${post.url}\n`;
283
- response += `**Visibility:** ${post.visibility}\n`;
327
+ response += `**Visibility:** ${(0, visibility_js_1.fromApiVisibility)(post.visibility)}\n`;
284
328
  response += `**Created:** ${post.created_at.slice(0, 10)}\n\n`;
285
329
  response += post.content;
286
330
  return { content: [{ type: 'text', text: response }] };
@@ -299,9 +343,10 @@ async function main() {
299
343
  title: zod_1.z.string().optional().describe('New title (will be sanitized)'),
300
344
  content: zod_1.z.string().optional().describe('New content (will be sanitized)'),
301
345
  visibility: zod_1.z.enum(['published', 'draft']).optional().describe('New visibility'),
302
- }, async ({ slug, title, content, visibility }) => {
303
- if (!config.apiKey) {
304
- return { content: [{ type: 'text', text: '❌ No API key configured.' }] };
346
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe('Updated tags/project names'),
347
+ }, async ({ slug, title, content, visibility, tags }) => {
348
+ if (!(process.env.DAYBY_API_KEY || (0, auth_js_1.getStoredToken)(config.apiUrl) || config.apiKey)) {
349
+ return { content: [{ type: 'text', text: '❌ Not authenticated. Run `dayby-mcp auth` to connect your DayBy account.' }] };
305
350
  }
306
351
  const params = {};
307
352
  if (title)
@@ -309,9 +354,11 @@ async function main() {
309
354
  if (content)
310
355
  params.content = sanitizer.sanitize(content).clean;
311
356
  if (visibility)
312
- params.visibility = visibility;
357
+ params.visibility = (0, visibility_js_1.toApiVisibility)(visibility);
358
+ if (tags)
359
+ params.tags = tags;
313
360
  if (Object.keys(params).length === 0) {
314
- return { content: [{ type: 'text', text: '❌ Provide at least one field to update (title, content, or visibility).' }] };
361
+ return { content: [{ type: 'text', text: '❌ Provide at least one field to update (title, content, visibility, or tags).' }] };
315
362
  }
316
363
  try {
317
364
  const result = await client.updatePost(slug, params);
@@ -319,7 +366,7 @@ async function main() {
319
366
  let response = `✅ **Post Updated**\n\n`;
320
367
  response += `**Title:** ${post.title}\n`;
321
368
  response += `**URL:** ${post.url}\n`;
322
- response += `**Visibility:** ${post.visibility}\n`;
369
+ response += `**Visibility:** ${(0, visibility_js_1.fromApiVisibility)(post.visibility)}\n`;
323
370
  return { content: [{ type: 'text', text: response }] };
324
371
  }
325
372
  catch (e) {
@@ -329,13 +376,52 @@ async function main() {
329
376
  }
330
377
  });
331
378
  // ========================================
379
+ // Tool: update_article
380
+ // Let the user's AI generate or edit the HTML article directly.
381
+ // ========================================
382
+ server.tool('update_article', `Set or replace the HTML article for a DayBy post. Use this to write custom articles with CTAs, rich formatting, or any content the user wants.
383
+
384
+ DayBy article HTML rules:
385
+ - Return clean HTML fragments only (no wrapper divs, article tags, doctype, html, head, body)
386
+ - Use <p> tags for paragraphs (no classes needed)
387
+ - Use <h2> for section headings, <h3> for sub-sections
388
+ - Use <strong> for key terms, <em> for emphasis
389
+ - Use <blockquote> for pull quotes
390
+ - Use <pre><code> for code blocks, <code> inline for references
391
+ - Use <ul>/<ol> for lists (prefer prose over lists)
392
+ - Use <a href="..." target="_blank"> for links and CTAs
393
+ - No CSS classes on elements (the page stylesheet handles typography)
394
+ - Target length: 600-1000 words
395
+ - Keep the author's voice and perspective from the original post`, {
396
+ slug: zod_1.z.string().describe('The post slug to update'),
397
+ html_article: zod_1.z.string().describe('The HTML article content. Follow DayBy article HTML rules in the tool description.'),
398
+ }, async ({ slug, html_article }) => {
399
+ if (!(process.env.DAYBY_API_KEY || (0, auth_js_1.getStoredToken)(config.apiUrl) || config.apiKey)) {
400
+ return { content: [{ type: 'text', text: '❌ Not authenticated. Run `dayby-mcp auth` to connect your DayBy account.' }] };
401
+ }
402
+ try {
403
+ const result = await client.updateArticle(slug, html_article);
404
+ const post = result.post;
405
+ let response = `✅ **Article Updated**\n\n`;
406
+ response += `**Title:** ${post.title}\n`;
407
+ response += `**URL:** ${post.url}\n`;
408
+ response += `${result.message}\n`;
409
+ return { content: [{ type: 'text', text: response }] };
410
+ }
411
+ catch (e) {
412
+ return {
413
+ content: [{ type: 'text', text: `❌ Failed to update article: ${e instanceof Error ? e.message : 'Unknown error'}` }],
414
+ };
415
+ }
416
+ });
417
+ // ========================================
332
418
  // Tool: delete_post
333
419
  // ========================================
334
420
  server.tool('delete_post', 'Permanently delete a DayBy post by its slug.', {
335
421
  slug: zod_1.z.string().describe('The post slug to delete'),
336
422
  }, async ({ slug }) => {
337
- if (!config.apiKey) {
338
- return { content: [{ type: 'text', text: '❌ No API key configured.' }] };
423
+ if (!(process.env.DAYBY_API_KEY || (0, auth_js_1.getStoredToken)(config.apiUrl) || config.apiKey)) {
424
+ return { content: [{ type: 'text', text: '❌ Not authenticated. Run `dayby-mcp auth` to connect your DayBy account.' }] };
339
425
  }
340
426
  try {
341
427
  const result = await client.deletePost(slug);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const sanitizer_js_1 = require("./sanitizer.js");
5
+ (0, vitest_1.describe)('Sanitizer', () => {
6
+ const sanitizer = new sanitizer_js_1.Sanitizer();
7
+ (0, vitest_1.describe)('default patterns', () => {
8
+ (0, vitest_1.it)('strips API keys', () => {
9
+ const fakeKey = 'xk_test_' + 'a1b2c3d4e5f6g7h8i9j0k1l2';
10
+ const result = sanitizer.sanitize(`My api_key=${fakeKey}`);
11
+ (0, vitest_1.expect)(result.clean).not.toContain(fakeKey);
12
+ (0, vitest_1.expect)(result.stripped.length).toBeGreaterThan(0);
13
+ });
14
+ (0, vitest_1.it)('strips AWS access keys', () => {
15
+ const result = sanitizer.sanitize('Key: AKIAIOSFODNN7EXAMPLE');
16
+ (0, vitest_1.expect)(result.clean).not.toContain('AKIAIOSFODNN7EXAMPLE');
17
+ });
18
+ (0, vitest_1.it)('strips private IPs', () => {
19
+ const result = sanitizer.sanitize('Server at 192.168.1.100');
20
+ (0, vitest_1.expect)(result.clean).not.toContain('192.168.1.100');
21
+ (0, vitest_1.expect)(result.clean).toContain('[REDACTED]');
22
+ });
23
+ (0, vitest_1.it)('strips email addresses', () => {
24
+ const result = sanitizer.sanitize('Contact me at user@company.com');
25
+ (0, vitest_1.expect)(result.clean).not.toContain('user@company.com');
26
+ });
27
+ (0, vitest_1.it)('strips database URLs', () => {
28
+ const result = sanitizer.sanitize('postgres://user:pass@localhost/db');
29
+ (0, vitest_1.expect)(result.clean).not.toContain('postgres://');
30
+ });
31
+ (0, vitest_1.it)('strips file paths with usernames', () => {
32
+ const result = sanitizer.sanitize('File at /home/javier/project');
33
+ (0, vitest_1.expect)(result.clean).not.toContain('/home/javier');
34
+ });
35
+ (0, vitest_1.it)('strips JWT tokens', () => {
36
+ const result = sanitizer.sanitize('Token: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.abc123def456');
37
+ (0, vitest_1.expect)(result.clean).not.toContain('eyJhbGci');
38
+ });
39
+ (0, vitest_1.it)('strips GitHub tokens', () => {
40
+ const result = sanitizer.sanitize('ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl');
41
+ (0, vitest_1.expect)(result.clean).not.toContain('ghp_');
42
+ });
43
+ });
44
+ (0, vitest_1.describe)('safe content', () => {
45
+ (0, vitest_1.it)('leaves clean text untouched', () => {
46
+ const text = 'I built a feature using React and TypeScript today.';
47
+ const result = sanitizer.sanitize(text);
48
+ (0, vitest_1.expect)(result.clean).toBe(text);
49
+ (0, vitest_1.expect)(result.stripped).toHaveLength(0);
50
+ });
51
+ (0, vitest_1.it)('leaves public IPs alone', () => {
52
+ const result = sanitizer.sanitize('Server at 8.8.8.8');
53
+ (0, vitest_1.expect)(result.clean).toContain('8.8.8.8');
54
+ });
55
+ });
56
+ (0, vitest_1.describe)('blocked terms', () => {
57
+ (0, vitest_1.it)('strips configured blocked terms', () => {
58
+ const s = new sanitizer_js_1.Sanitizer({ blockedTerms: ['Acme Corp'] });
59
+ const result = s.sanitize('Working on the Acme Corp project');
60
+ (0, vitest_1.expect)(result.clean).not.toContain('Acme Corp');
61
+ });
62
+ (0, vitest_1.it)('strips blocked names', () => {
63
+ const s = new sanitizer_js_1.Sanitizer({ blockedNames: ['John Doe'] });
64
+ const result = s.sanitize('Paired with John Doe on the fix');
65
+ (0, vitest_1.expect)(result.clean).not.toContain('John Doe');
66
+ });
67
+ });
68
+ (0, vitest_1.describe)('blocked domains', () => {
69
+ (0, vitest_1.it)('strips blocked domain URLs', () => {
70
+ const s = new sanitizer_js_1.Sanitizer({ blockedDomains: ['internal.corp.com'] });
71
+ const result = s.sanitize('Check https://jira.internal.corp.com/browse/TICK-123');
72
+ (0, vitest_1.expect)(result.clean).not.toContain('internal.corp.com');
73
+ (0, vitest_1.expect)(result.clean).toContain('[REDACTED-URL]');
74
+ });
75
+ });
76
+ (0, vitest_1.describe)('check', () => {
77
+ (0, vitest_1.it)('returns safe for clean content', () => {
78
+ const result = sanitizer.check('Just a normal post about coding.');
79
+ (0, vitest_1.expect)(result.safe).toBe(true);
80
+ (0, vitest_1.expect)(result.issues).toHaveLength(0);
81
+ });
82
+ (0, vitest_1.it)('returns unsafe for sensitive content', () => {
83
+ const result = sanitizer.check('My api_key=super_secret_key_1234567890');
84
+ (0, vitest_1.expect)(result.safe).toBe(false);
85
+ (0, vitest_1.expect)(result.issues.length).toBeGreaterThan(0);
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Visibility mapping between MCP (user-facing) and DayBy API values.
3
+ *
4
+ * MCP uses "published"/"draft" (intuitive for users).
5
+ * DayBy API uses "publik"/"draft"/"hidden" (Rails enum).
6
+ */
7
+ export declare function toApiVisibility(v: string): string;
8
+ export declare function fromApiVisibility(v: string): string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ /**
3
+ * Visibility mapping between MCP (user-facing) and DayBy API values.
4
+ *
5
+ * MCP uses "published"/"draft" (intuitive for users).
6
+ * DayBy API uses "publik"/"draft"/"hidden" (Rails enum).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.toApiVisibility = toApiVisibility;
10
+ exports.fromApiVisibility = fromApiVisibility;
11
+ function toApiVisibility(v) {
12
+ return v === 'published' ? 'publik' : v;
13
+ }
14
+ function fromApiVisibility(v) {
15
+ return v === 'publik' ? 'published' : v;
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const visibility_js_1 = require("./visibility.js");
5
+ (0, vitest_1.describe)('Visibility mapping', () => {
6
+ (0, vitest_1.describe)('toApiVisibility', () => {
7
+ (0, vitest_1.it)('maps "published" to "publik"', () => {
8
+ (0, vitest_1.expect)((0, visibility_js_1.toApiVisibility)('published')).toBe('publik');
9
+ });
10
+ (0, vitest_1.it)('passes "draft" through unchanged', () => {
11
+ (0, vitest_1.expect)((0, visibility_js_1.toApiVisibility)('draft')).toBe('draft');
12
+ });
13
+ (0, vitest_1.it)('passes "hidden" through unchanged', () => {
14
+ (0, vitest_1.expect)((0, visibility_js_1.toApiVisibility)('hidden')).toBe('hidden');
15
+ });
16
+ });
17
+ (0, vitest_1.describe)('fromApiVisibility', () => {
18
+ (0, vitest_1.it)('maps "publik" to "published"', () => {
19
+ (0, vitest_1.expect)((0, visibility_js_1.fromApiVisibility)('publik')).toBe('published');
20
+ });
21
+ (0, vitest_1.it)('passes "draft" through unchanged', () => {
22
+ (0, vitest_1.expect)((0, visibility_js_1.fromApiVisibility)('draft')).toBe('draft');
23
+ });
24
+ (0, vitest_1.it)('passes "hidden" through unchanged', () => {
25
+ (0, vitest_1.expect)((0, visibility_js_1.fromApiVisibility)('hidden')).toBe('hidden');
26
+ });
27
+ });
28
+ (0, vitest_1.describe)('round-trip', () => {
29
+ (0, vitest_1.it)('published -> publik -> published', () => {
30
+ (0, vitest_1.expect)((0, visibility_js_1.fromApiVisibility)((0, visibility_js_1.toApiVisibility)('published'))).toBe('published');
31
+ });
32
+ (0, vitest_1.it)('draft -> draft -> draft', () => {
33
+ (0, vitest_1.expect)((0, visibility_js_1.fromApiVisibility)((0, visibility_js_1.toApiVisibility)('draft'))).toBe('draft');
34
+ });
35
+ });
36
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayby/mcp-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.2",
4
4
  "description": "DayBy MCP Server — Post your dev progress from Claude, Cursor, or any MCP client. Local sanitization built in.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,15 +13,25 @@
13
13
  "scripts": {
14
14
  "build": "tsc",
15
15
  "dev": "tsc --watch",
16
- "start": "node dist/index.js"
16
+ "start": "node dist/index.js",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
17
19
  },
18
- "keywords": ["mcp", "dayby", "developer", "journal", "claude"],
20
+ "keywords": [
21
+ "mcp",
22
+ "dayby",
23
+ "developer",
24
+ "journal",
25
+ "claude"
26
+ ],
19
27
  "license": "MIT",
20
28
  "dependencies": {
21
- "@modelcontextprotocol/sdk": "^1.0.0"
29
+ "@modelcontextprotocol/sdk": "^1.0.0",
30
+ "zod": "^3.22.0"
22
31
  },
23
32
  "devDependencies": {
33
+ "@types/node": "^20.0.0",
24
34
  "typescript": "^5.3.0",
25
- "@types/node": "^20.0.0"
35
+ "vitest": "^4.1.5"
26
36
  }
27
37
  }