@tiongcreative/assistant-mcp 0.2.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2026-10-19
4
+
5
+ ### Added
6
+
7
+ - One-command setup, status and removal for ChatGPT desktop on the same Mac. ChatGPT runs the existing local assistant server; app access still depends on each app's assistant setting.
8
+
9
+ ## [0.3.0] - 2026-10-19
10
+
11
+ ### Added
12
+
13
+ - Lists & Notes can now be changed, with Lists & Notes 1.23.0 or later: `lists_and_notes_add_items`, `lists_and_notes_update_items` (tick, untick, reword or annotate items named by their text), `lists_and_notes_add_list`, `lists_and_notes_add_note` and `lists_and_notes_update_note` (append, replace or rename). Changes to an existing list or note send the `modified` stamp last read; if the user changed it in the meantime the app refuses, and the tool re-reads and retries once. Item ids are always sent back. An older Lists & Notes gets a message saying which version writes need. No delete tool. End-to-end test against the real 1.23.0 server.
14
+
3
15
  ## [0.2.0] - 2026-10-19
4
16
 
5
17
  ### Added
package/README.md CHANGED
@@ -10,12 +10,12 @@ nothing else happens.
10
10
 
11
11
  ## Apps
12
12
 
13
- | App | Reads | Changes |
14
- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
15
- | Due Again 1.1.0 or later | items with status and next due date, an item's history | add an item, record that an item was done |
16
- | Gift Keeper 1.3.0 or later | people with interests, sizes and preferences; upcoming occasions with budget, spend and whether a gift is chosen | add a gift idea for a person, mark a gift as given |
17
- | Client Manager 1.6.0 or later | pending reminders, open tasks, invoices owing with totals and balance, one client with their jobs | add a client, a job, a reminder or a task, change a job, task or reminder, log an activity |
18
- | Lists & Notes 1.22.0 or later | lists with their items, notes with previews, the full text of a note | nothing yet |
13
+ | App | Reads | Changes |
14
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
15
+ | Due Again 1.1.0 or later | items with status and next due date, an item's history | add an item, record that an item was done |
16
+ | Gift Keeper 1.3.0 or later | people with interests, sizes and preferences; upcoming occasions with budget, spend and whether a gift is chosen | add a gift idea for a person, mark a gift as given |
17
+ | Client Manager 1.6.0 or later | pending reminders, open tasks, invoices owing with totals and balance, one client with their jobs | add a client, a job, a reminder or a task, change a job, task or reminder, log an activity |
18
+ | Lists & Notes 1.22.0 or later (changes need 1.23.0) | lists with their items, notes with previews, the full text of a note | add a list or a note, add items, tick off or reword items, add to or rename a note |
19
19
 
20
20
  More apps follow, and the assistant can also open an app for you, but only after you agree.
21
21
 
@@ -40,6 +40,20 @@ Desktop starts it, so after installing another app restart Claude Desktop once.
40
40
 
41
41
  To remove it: `npx @tiongcreative/assistant-mcp uninstall`. To check: `npx @tiongcreative/assistant-mcp status`.
42
42
 
43
+ ### ChatGPT desktop on the same Mac
44
+
45
+ For ChatGPT desktop, run this one command instead:
46
+
47
+ ```
48
+ npx @tiongcreative/assistant-mcp install chatgpt
49
+ ```
50
+
51
+ It registers the same local server in ChatGPT's MCP settings. Restart ChatGPT, turn on
52
+ **Allow assistant access** in each app, and type `/mcp` in ChatGPT to check the connection.
53
+ The Tiong Creative apps must be open on this Mac. This setup is for ChatGPT desktop, not
54
+ ChatGPT in a web browser. To check or remove it, run `npx @tiongcreative/assistant-mcp status chatgpt`
55
+ or `npx @tiongcreative/assistant-mcp uninstall chatgpt`.
56
+
43
57
  For Claude Code: `claude mcp add tiongcreative -- npx -y @tiongcreative/assistant-mcp`.
44
58
 
45
59
  ## How it stays on your Mac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiongcreative/assistant-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server that lets an AI assistant on your Mac use your Tiong Creative apps while they are open.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "prepare": "husky",
35
35
  "validate": "npm run lint && npm run format:check",
36
36
  "quality": "npm run validate && npm run sast && npm run security && npm run secrets && npm run licenses && npm run deps:check && npm run review",
37
- "test:unit": "node --test tests/dates.test.js tests/install.test.js",
37
+ "test:unit": "node --test tests/dates.test.js tests/install.test.js tests/chatgpt-install.test.js",
38
38
  "test:integration": "node --test tests/due-again.test.js tests/gift-keeper.test.js tests/client-manager.test.js tests/lists-and-notes.test.js"
39
39
  },
40
40
  "license": "MIT",
@@ -1,9 +1,12 @@
1
- // Lists & Notes: checklists and markdown notes. Read-only for now: the app's list
2
- // endpoint replaces the whole file from the request, so writes wait until that is
3
- // hardened (see the plan). Talks to the app's local API with the per-launch token it
4
- // writes while "Allow assistant access" is on (list-manager/assistant-access.js).
1
+ // Lists & Notes: checklists and markdown notes. Writes need Lists & Notes 1.23.0 or later,
2
+ // which gives the assistant its own per-list and per-note routes (the whole-file save is
3
+ // window-only). Changing an existing list or note sends the `modified` stamp last read
4
+ // (`baseModified`); if the user changed it since, the app refuses and the tool re-reads and
5
+ // retries once. Item ids are always sent back, or the app would treat items as new. There
6
+ // is no delete tool. Talks to the app's local API with the per-launch token it writes while
7
+ // "Allow assistant access" is on (list-manager/assistant-access.js).
5
8
  import { z } from 'zod';
6
- import { appConnection, apiCall, notAvailable } from '../connection.js';
9
+ import { ApiError, appConnection, apiCall, notAvailable } from '../connection.js';
7
10
 
8
11
  export const app = {
9
12
  id: 'lists-and-notes',
@@ -47,6 +50,63 @@ function matches(text, q) {
47
50
  return typeof text === 'string' && text.toLowerCase().includes(q);
48
51
  }
49
52
 
53
+ const WRITES_FROM = '1.23.0';
54
+
55
+ function olderThan(version, min) {
56
+ const a = String(version ?? '0')
57
+ .split('.')
58
+ .map(Number);
59
+ const b = min.split('.').map(Number);
60
+ for (let i = 0; i < 3; i++) {
61
+ if ((a[i] || 0) !== b[i]) return (a[i] || 0) < b[i];
62
+ }
63
+ return false;
64
+ }
65
+
66
+ /** The app's version must support writes; returns a result to send back, or null. */
67
+ function needsUpdate(conn) {
68
+ if (!olderThan(conn.version, WRITES_FROM)) return null;
69
+ return {
70
+ app: app.name,
71
+ changed: false,
72
+ message: `Changing lists and notes needs Lists & Notes ${WRITES_FROM} or later (this Mac has ${conn.version ?? 'an older version'}). Reading still works.`,
73
+ };
74
+ }
75
+
76
+ async function findList(conn, name) {
77
+ const lists = await apiCall(conn, 'GET', '/api/data/lists');
78
+ const wanted = name.trim().toLowerCase();
79
+ return {
80
+ list: lists.find((l) => l.name.toLowerCase() === wanted),
81
+ names: lists.map((l) => l.name),
82
+ };
83
+ }
84
+
85
+ async function findNote(conn, title) {
86
+ const notes = await apiCall(conn, 'GET', '/api/data/notes');
87
+ const wanted = title.trim().toLowerCase();
88
+ return {
89
+ note: notes.find((n) => n.title.toLowerCase() === wanted),
90
+ titles: notes.map((n) => n.title),
91
+ };
92
+ }
93
+
94
+ /** Runs a change against the latest copy; on a 409 (changed in the window) re-reads once. */
95
+ async function withRetry(load, change) {
96
+ let current = await load();
97
+ try {
98
+ return await change(current);
99
+ } catch (err) {
100
+ if (!(err instanceof ApiError) || err.status !== 409) throw err;
101
+ current = await load();
102
+ return change(current);
103
+ }
104
+ }
105
+
106
+ function itemMatches(item, text) {
107
+ return item.text.trim().toLowerCase() === text.trim().toLowerCase();
108
+ }
109
+
50
110
  export const tools = [
51
111
  {
52
112
  name: 'lists_and_notes_lists',
@@ -156,6 +216,248 @@ export const tools = [
156
216
  };
157
217
  },
158
218
  },
219
+ {
220
+ name: 'lists_and_notes_add_items',
221
+ description:
222
+ 'Add items to an existing list in Lists & Notes, by list name. Items are added unticked at the end. If the list does not exist, the reply lists the lists.',
223
+ input: z.object({
224
+ list: z.string().min(1).max(75),
225
+ items: z.array(z.string().trim().min(1).max(2000)).min(1).max(100),
226
+ }),
227
+ write: true,
228
+ async run(input) {
229
+ const conn = await appConnection(app);
230
+ if (!conn.ok) return notAvailable(app, conn);
231
+ const old = needsUpdate(conn);
232
+ if (old) return old;
233
+ const { list, names } = await findList(conn, input.list);
234
+ if (!list)
235
+ return {
236
+ app: app.name,
237
+ changed: false,
238
+ message: `No list called "${input.list}".`,
239
+ lists: names,
240
+ };
241
+ const updated = await apiCall(
242
+ conn,
243
+ 'POST',
244
+ `/api/data/lists/${encodeURIComponent(list.id)}/items`,
245
+ {
246
+ items: input.items.map((text) => ({ text })),
247
+ },
248
+ );
249
+ return { app: app.name, changed: true, list: summariseList(updated), added: input.items };
250
+ },
251
+ },
252
+ {
253
+ name: 'lists_and_notes_update_items',
254
+ description:
255
+ "Tick off, untick, reword or add notes to items on a list in Lists & Notes. Name the list and each item by its current text. Items not mentioned are left as they are. If an item's text does not match, the reply lists the items.",
256
+ input: z.object({
257
+ list: z.string().min(1).max(75),
258
+ changes: z
259
+ .array(
260
+ z.object({
261
+ item: z.string().min(1).max(2000).describe('The item as it reads now'),
262
+ done: z.boolean().optional(),
263
+ text: z.string().trim().min(1).max(2000).optional().describe('New wording'),
264
+ notes: z.string().max(2000).optional(),
265
+ }),
266
+ )
267
+ .min(1)
268
+ .max(100),
269
+ }),
270
+ write: true,
271
+ async run(input) {
272
+ const conn = await appConnection(app);
273
+ if (!conn.ok) return notAvailable(app, conn);
274
+ const old = needsUpdate(conn);
275
+ if (old) return old;
276
+ const first = await findList(conn, input.list);
277
+ if (!first.list)
278
+ return {
279
+ app: app.name,
280
+ changed: false,
281
+ message: `No list called "${input.list}".`,
282
+ lists: first.names,
283
+ };
284
+ const id = first.list.id;
285
+ const load = async () =>
286
+ (await apiCall(conn, 'GET', '/api/data/lists')).find((l) => l.id === id);
287
+ return withRetry(load, async (list) => {
288
+ if (!list)
289
+ return { app: app.name, changed: false, message: 'That list has just been removed.' };
290
+ const items = (list.items ?? []).map((i) => ({ ...i }));
291
+ const missing = input.changes
292
+ .filter((c) => !items.some((i) => itemMatches(i, c.item)))
293
+ .map((c) => c.item);
294
+ if (missing.length) {
295
+ return {
296
+ app: app.name,
297
+ changed: false,
298
+ message: `No item reading ${missing.map((m) => `"${m}"`).join(', ')} on ${list.name}.`,
299
+ items: items.map((i) => i.text),
300
+ };
301
+ }
302
+ for (const c of input.changes) {
303
+ const item = items.find((i) => itemMatches(i, c.item));
304
+ if (c.done !== undefined) item.completed = c.done;
305
+ if (c.text !== undefined) item.text = c.text;
306
+ if (c.notes !== undefined) item.notes = c.notes;
307
+ }
308
+ const updated = await apiCall(conn, 'PATCH', `/api/data/lists/${encodeURIComponent(id)}`, {
309
+ items,
310
+ baseModified: list.metadata?.modified,
311
+ });
312
+ return { app: app.name, changed: true, list: summariseList(updated) };
313
+ });
314
+ },
315
+ },
316
+ {
317
+ name: 'lists_and_notes_add_list',
318
+ description:
319
+ 'Create a new list in Lists & Notes with optional first items. The category must already exist in the app; if it does not, the reply lists the categories.',
320
+ input: z.object({
321
+ name: z.string().trim().min(1).max(75),
322
+ items: z.array(z.string().trim().min(1).max(2000)).max(100).optional(),
323
+ category: z.string().max(100).optional().describe('Defaults to personal'),
324
+ tags: z.array(z.string().max(50)).max(10).optional(),
325
+ }),
326
+ write: true,
327
+ async run(input) {
328
+ const conn = await appConnection(app);
329
+ if (!conn.ok) return notAvailable(app, conn);
330
+ const old = needsUpdate(conn);
331
+ if (old) return old;
332
+ const [{ list: existing }, categories] = await Promise.all([
333
+ findList(conn, input.name),
334
+ apiCall(conn, 'GET', '/api/data/categories'),
335
+ ]);
336
+ if (existing)
337
+ return {
338
+ app: app.name,
339
+ added: false,
340
+ message: `There is already a list called "${input.name}".`,
341
+ };
342
+ const category = input.category
343
+ ? categories.find((c) => c.toLowerCase() === input.category.trim().toLowerCase())
344
+ : 'personal';
345
+ if (!category)
346
+ return {
347
+ app: app.name,
348
+ added: false,
349
+ message: `No category called "${input.category}".`,
350
+ categories,
351
+ };
352
+ const list = await apiCall(conn, 'POST', '/api/data/lists/add', {
353
+ name: input.name,
354
+ category,
355
+ ...(input.tags ? { tags: input.tags } : {}),
356
+ items: (input.items ?? []).map((text) => ({ text, completed: false, notes: '' })),
357
+ });
358
+ return { app: app.name, added: true, list: summariseList(list) };
359
+ },
360
+ },
361
+ {
362
+ name: 'lists_and_notes_add_note',
363
+ description:
364
+ 'Create a note in Lists & Notes with a title and markdown content. The category must already exist; if it does not, the reply lists the categories.',
365
+ input: z.object({
366
+ title: z.string().trim().min(1).max(75),
367
+ content: z.string().max(200000).default(''),
368
+ category: z.string().max(100).optional().describe('Defaults to personal'),
369
+ tags: z.array(z.string().max(50)).max(10).optional(),
370
+ }),
371
+ write: true,
372
+ async run(input) {
373
+ const conn = await appConnection(app);
374
+ if (!conn.ok) return notAvailable(app, conn);
375
+ const old = needsUpdate(conn);
376
+ if (old) return old;
377
+ const categories = await apiCall(conn, 'GET', '/api/data/categories');
378
+ const category = input.category
379
+ ? categories.find((c) => c.toLowerCase() === input.category.trim().toLowerCase())
380
+ : 'personal';
381
+ if (!category)
382
+ return {
383
+ app: app.name,
384
+ added: false,
385
+ message: `No category called "${input.category}".`,
386
+ categories,
387
+ };
388
+ const note = await apiCall(conn, 'POST', '/api/data/notes', {
389
+ title: input.title,
390
+ category,
391
+ content: input.content,
392
+ ...(input.tags ? { tags: input.tags } : {}),
393
+ });
394
+ return { app: app.name, added: true, note: summariseNote(note) };
395
+ },
396
+ },
397
+ {
398
+ name: 'lists_and_notes_update_note',
399
+ description:
400
+ "Change a note in Lists & Notes, by title: add text to the end (append), replace its text, or rename it. If the user edits the note at the same moment, the change is applied to their latest version; the app keeps the previous text in the note's history.",
401
+ input: z.object({
402
+ title: z.string().min(1).max(75).describe('The note as it is titled now'),
403
+ append: z.string().max(200000).optional().describe('Text added on a new line at the end'),
404
+ content: z.string().max(200000).optional().describe('Replaces the whole text'),
405
+ newTitle: z.string().trim().min(1).max(75).optional(),
406
+ }),
407
+ write: true,
408
+ async run(input) {
409
+ const conn = await appConnection(app);
410
+ if (!conn.ok) return notAvailable(app, conn);
411
+ const old = needsUpdate(conn);
412
+ if (old) return old;
413
+ if (
414
+ input.append === undefined &&
415
+ input.content === undefined &&
416
+ input.newTitle === undefined
417
+ ) {
418
+ return {
419
+ app: app.name,
420
+ changed: false,
421
+ message: 'Nothing to change: give text to append, new content or a new title.',
422
+ };
423
+ }
424
+ if (input.append !== undefined && input.content !== undefined) {
425
+ return {
426
+ app: app.name,
427
+ changed: false,
428
+ message: 'Give either append or content, not both.',
429
+ };
430
+ }
431
+ const first = await findNote(conn, input.title);
432
+ if (!first.note)
433
+ return {
434
+ app: app.name,
435
+ changed: false,
436
+ message: `No note called "${input.title}".`,
437
+ titles: first.titles,
438
+ };
439
+ const id = first.note.id;
440
+ const load = () => apiCall(conn, 'GET', `/api/data/notes/${encodeURIComponent(id)}`);
441
+ return withRetry(load, async (note) => {
442
+ const body = { baseModified: note.metadata?.modified };
443
+ if (input.content !== undefined) body.content = input.content;
444
+ if (input.append !== undefined) {
445
+ const current = typeof note.content === 'string' ? note.content : '';
446
+ body.content = current
447
+ ? `${current.replace(/\s+$/, '')}\n\n${input.append}`
448
+ : input.append;
449
+ }
450
+ if (input.newTitle !== undefined) body.title = input.newTitle;
451
+ const updated = await apiCall(
452
+ conn,
453
+ 'PUT',
454
+ `/api/data/notes/${encodeURIComponent(id)}`,
455
+ body,
456
+ );
457
+ return { app: app.name, changed: true, note: summariseNote(updated) };
458
+ });
459
+ },
460
+ },
159
461
  ];
160
462
 
161
463
  /** Lists & Notes has no dated items, so it adds nothing to "what's on". */
@@ -0,0 +1,78 @@
1
+ // ChatGPT desktop and Codex share MCP configuration. Use the CLI shipped with
2
+ // ChatGPT to edit it, so other servers and unrelated settings stay intact.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { npxPath, SERVER_KEY } from './install.js';
8
+
9
+ const PACKAGE = '@tiongcreative/assistant-mcp';
10
+
11
+ function cliPath() {
12
+ const relative = path.join('Contents', 'Resources', 'codex-cli', 'bin', 'codex');
13
+ for (const apps of ['/Applications', path.join(os.homedir(), 'Applications')]) {
14
+ const candidate = path.join(apps, 'ChatGPT.app', relative);
15
+ if (fs.existsSync(candidate)) return candidate;
16
+ }
17
+ throw new Error(
18
+ 'ChatGPT desktop was not found in Applications. Install it before running this command.',
19
+ );
20
+ }
21
+
22
+ function run(cli, args) {
23
+ try {
24
+ return execFileSync(cli, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
25
+ } catch (err) {
26
+ const detail = err.stderr?.toString().trim() || err.message;
27
+ throw new Error(`ChatGPT could not update its MCP settings: ${detail}`);
28
+ }
29
+ }
30
+
31
+ function current(cli) {
32
+ const servers = JSON.parse(run(cli, ['mcp', 'list', '--json']));
33
+ if (!Array.isArray(servers)) throw new Error('ChatGPT returned an unexpected MCP server list.');
34
+ return servers.find((server) => server.name === SERVER_KEY) ?? null;
35
+ }
36
+
37
+ function matches(server, env) {
38
+ const transport = server?.transport;
39
+ return (
40
+ transport?.type === 'stdio' &&
41
+ transport.command === npxPath() &&
42
+ JSON.stringify(transport.args) === JSON.stringify(['-y', PACKAGE]) &&
43
+ JSON.stringify(transport.env ?? {}) === JSON.stringify(env)
44
+ );
45
+ }
46
+
47
+ export function chatGptStatus(cli = cliPath()) {
48
+ const server = current(cli);
49
+ return { installed: !!server, managed: matches(server, server?.transport?.env ?? {}) };
50
+ }
51
+
52
+ export function installChatGpt({ allowFresh = false, cli = cliPath() } = {}) {
53
+ const env = allowFresh ? { npm_config_min_release_age: '0' } : {};
54
+ const server = current(cli);
55
+ if (matches(server, env)) return { already: true };
56
+ if (server) {
57
+ throw new Error(
58
+ 'An MCP server named "tiongcreative" already exists in ChatGPT. Remove it in ChatGPT settings before installing this one.',
59
+ );
60
+ }
61
+ const args = ['mcp', 'add'];
62
+ for (const [key, value] of Object.entries(env)) args.push('--env', `${key}=${value}`);
63
+ args.push(SERVER_KEY, '--', npxPath(), '-y', PACKAGE);
64
+ run(cli, args);
65
+ return { already: false };
66
+ }
67
+
68
+ export function uninstallChatGpt(cli = cliPath()) {
69
+ const server = current(cli);
70
+ if (!server) return { removed: false };
71
+ if (!matches(server, server.transport?.env ?? {})) {
72
+ throw new Error(
73
+ 'The "tiongcreative" entry in ChatGPT does not match this package; nothing was removed.',
74
+ );
75
+ }
76
+ run(cli, ['mcp', 'remove', SERVER_KEY]);
77
+ return { removed: true };
78
+ }
package/src/install.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
+ import { chatGptStatus, installChatGpt, uninstallChatGpt } from './chatgpt-install.js';
10
11
 
11
12
  export const SERVER_KEY = 'tiongcreative';
12
13
  const PACKAGE = '@tiongcreative/assistant-mcp';
@@ -109,8 +110,45 @@ const NEXT_STEPS = [
109
110
 
110
111
  /** Runs the command and returns the process exit code. Prints with the given writer. */
111
112
  export function runCli(args, print = console.log) {
112
- const [command] = args;
113
+ const [command, client] = args;
113
114
  try {
115
+ if (client === 'chatgpt' && ['install', 'uninstall', 'status'].includes(command)) {
116
+ if (command === 'install') {
117
+ const result = installChatGpt({ allowFresh: args.includes('--allow-fresh') });
118
+ print(
119
+ result.already
120
+ ? 'The Tiong Creative assistant is already set up in ChatGPT.'
121
+ : 'Added the Tiong Creative assistant to ChatGPT.',
122
+ );
123
+ print(
124
+ '\nNext:\n1. Restart the ChatGPT desktop app.\n2. Open each Tiong Creative app and turn on "Allow assistant access" in Settings.\n3. Type /mcp in ChatGPT to check the connection.',
125
+ );
126
+ } else if (command === 'uninstall') {
127
+ const result = uninstallChatGpt();
128
+ print(
129
+ result.removed
130
+ ? 'Removed the Tiong Creative assistant from ChatGPT. Restart ChatGPT to finish.'
131
+ : 'The Tiong Creative assistant was not set up in ChatGPT.',
132
+ );
133
+ } else {
134
+ const result = chatGptStatus();
135
+ print(
136
+ result.managed
137
+ ? 'Set up in ChatGPT.'
138
+ : result.installed
139
+ ? 'ChatGPT has a different MCP server named "tiongcreative".'
140
+ : 'Not set up in ChatGPT.',
141
+ );
142
+ }
143
+ return 0;
144
+ }
145
+ if (
146
+ client &&
147
+ client !== '--allow-fresh' &&
148
+ ['install', 'uninstall', 'status'].includes(command)
149
+ ) {
150
+ throw new Error('Choose "chatgpt" as the client, or omit it for Claude Desktop.');
151
+ }
114
152
  if (command === 'install') {
115
153
  const env = {};
116
154
  // Developer convenience: `install --allow-fresh` passes npm's release-age override
@@ -146,7 +184,7 @@ export function runCli(args, print = console.log) {
146
184
  return 0;
147
185
  }
148
186
  print(
149
- `Usage:\n npx ${PACKAGE} install add the assistant to Claude Desktop\n npx ${PACKAGE} uninstall remove it\n npx ${PACKAGE} status say whether it is set up\n\nWith no command, this runs the MCP server itself (Claude Desktop does that for you).`,
187
+ `Usage:\n npx ${PACKAGE} install add the assistant to Claude Desktop\n npx ${PACKAGE} install chatgpt add it to ChatGPT desktop\n npx ${PACKAGE} uninstall [chatgpt] remove it from the chosen client\n npx ${PACKAGE} status [chatgpt] check the chosen client\n\nWith no command, this runs the MCP server itself.`,
150
188
  );
151
189
  return command === 'help' || command === '--help' ? 0 : 2;
152
190
  } catch (err) {