@telvok/librarian-mcp 2.3.2 → 2.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.
@@ -1,32 +1,3 @@
1
- interface PublishResult {
2
- success: boolean;
3
- message: string;
4
- publish_token?: string;
5
- book?: {
6
- id?: string;
7
- slug: string;
8
- name: string;
9
- url: string;
10
- };
11
- entries_count?: number;
12
- setup_url?: string;
13
- preview?: boolean;
14
- summary?: {
15
- name: string;
16
- pricing: {
17
- type: string;
18
- display: string;
19
- };
20
- entries_count: number;
21
- entries: Array<{
22
- title: string;
23
- file: string;
24
- }>;
25
- };
26
- next_steps?: string;
27
- options?: Record<string, string>;
28
- required?: Record<string, string>;
29
- }
30
1
  export declare const libraryPublishTool: {
31
2
  name: string;
32
3
  title: string;
@@ -34,12 +5,11 @@ export declare const libraryPublishTool: {
34
5
  inputSchema: {
35
6
  type: "object";
36
7
  properties: {
37
- name: {
38
- type: string;
39
- description: string;
40
- };
41
- description: {
8
+ entries: {
42
9
  type: string;
10
+ items: {
11
+ type: string;
12
+ };
43
13
  description: string;
44
14
  };
45
15
  pricing: {
@@ -48,14 +18,11 @@ export declare const libraryPublishTool: {
48
18
  type: {
49
19
  type: string;
50
20
  enum: string[];
51
- description: string;
52
21
  };
53
22
  price_cents: {
54
23
  type: string;
55
- description: string;
56
24
  };
57
25
  };
58
- required: string[];
59
26
  description: string;
60
27
  };
61
28
  consumption: {
@@ -63,38 +30,12 @@ export declare const libraryPublishTool: {
63
30
  enum: string[];
64
31
  description: string;
65
32
  };
66
- attestation: {
67
- type: string;
68
- properties: {
69
- original_work: {
70
- type: string;
71
- description: string;
72
- };
73
- no_secrets: {
74
- type: string;
75
- description: string;
76
- };
77
- terms_accepted: {
78
- type: string;
79
- description: string;
80
- };
81
- };
82
- required: string[];
83
- description: string;
84
- };
85
- preview: {
86
- type: string;
87
- description: string;
88
- };
89
- publish_token: {
33
+ name: {
90
34
  type: string;
91
35
  description: string;
92
36
  };
93
- entries: {
37
+ description: {
94
38
  type: string;
95
- items: {
96
- type: string;
97
- };
98
39
  description: string;
99
40
  };
100
41
  tags: {
@@ -107,11 +48,60 @@ export declare const libraryPublishTool: {
107
48
  license: {
108
49
  type: string;
109
50
  enum: string[];
51
+ };
52
+ confirm_code: {
53
+ type: string;
110
54
  description: string;
111
55
  };
112
56
  };
113
- required: string[];
57
+ required: never[];
114
58
  };
115
- handler(args: unknown): Promise<PublishResult>;
59
+ handler(args: unknown): Promise<{
60
+ success: boolean;
61
+ message: string;
62
+ setup_url: any;
63
+ book?: undefined;
64
+ entries_count?: undefined;
65
+ } | {
66
+ success: boolean;
67
+ message: any;
68
+ setup_url?: undefined;
69
+ book?: undefined;
70
+ entries_count?: undefined;
71
+ } | {
72
+ success: boolean;
73
+ message: string;
74
+ book: any;
75
+ entries_count: any;
76
+ setup_url?: undefined;
77
+ } | {
78
+ success: boolean;
79
+ step: string;
80
+ message: string;
81
+ entries_count: number;
82
+ ask_user: string;
83
+ selected_count?: undefined;
84
+ } | {
85
+ success: boolean;
86
+ step: string;
87
+ message: string;
88
+ ask_user: string;
89
+ entries_count?: undefined;
90
+ selected_count?: undefined;
91
+ } | {
92
+ success: boolean;
93
+ step: string;
94
+ message: string;
95
+ entries_count?: undefined;
96
+ ask_user?: undefined;
97
+ selected_count?: undefined;
98
+ } | {
99
+ success: boolean;
100
+ step: string;
101
+ message: string;
102
+ selected_count: number;
103
+ ask_user: string;
104
+ entries_count?: undefined;
105
+ }>;
116
106
  };
117
107
  export default libraryPublishTool;
@@ -1,349 +1,420 @@
1
1
  // ============================================================================
2
- // Marketplace Publish Tool
3
- // Publish local entries as a book on Telvok library
2
+ // Marketplace Publish Tool — Multi-Step Wizard
3
+ // Each call advances one step. Tool refuses to skip ahead.
4
+ // Agent relays between user and tool. User makes every decision.
4
5
  // ============================================================================
5
6
  import * as fs from 'fs/promises';
6
7
  import * as path from 'path';
7
8
  import * as crypto from 'crypto';
9
+ import { execSync } from 'child_process';
10
+ import { platform } from 'os';
8
11
  import { glob } from 'glob';
9
12
  import matter from 'gray-matter';
10
13
  import { loadApiKey } from './auth.js';
11
14
  import { getLibraryPath, getLocalPath } from '../library/storage.js';
12
15
  import { scanForSensitiveData } from '../library/sensitive-scanner.js';
13
16
  const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
14
- const TOKEN_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
15
- let pendingPublish = null;
17
+ const WIZARD_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes
18
+ let wizardState = null;
19
+ function clearExpiredWizard() {
20
+ if (wizardState && Date.now() - wizardState.created > WIZARD_EXPIRY_MS) {
21
+ wizardState = null;
22
+ }
23
+ }
16
24
  // ============================================================================
17
25
  // Tool Definition
18
26
  // ============================================================================
19
27
  export const libraryPublishTool = {
20
28
  name: 'library_publish',
21
29
  title: 'Publish Book',
22
- description: `Publish local entries as a book on Telvok library.
23
-
24
- ⚠️ TWO-STEP PUBLISH FLOW (MANDATORY):
30
+ description: `Publish local entries as a book on Telvok marketplace.
25
31
 
26
- Step 1: ALWAYS call with preview: true first. This shows what will be published
27
- and returns a publish_token. Show the preview to the user and ASK FOR CONFIRMATION.
32
+ This is a GUIDED WIZARD. Call with no arguments to start.
33
+ The tool walks through each step DO NOT try to provide all arguments at once.
28
34
 
29
- Step 2: ONLY after the user explicitly confirms, call again with the publish_token
30
- from the preview response. Publishing WITHOUT a valid token will be rejected.
35
+ FLOW:
36
+ 1. library_publish() scans entries, shows audit. Ask user which to include.
37
+ 2. library_publish({ entries: ["all"] }) → shows pricing options. Ask user to choose.
38
+ 3. library_publish({ pricing: { type: "open" } }) → asks for name/description.
39
+ 4. library_publish({ name: "...", description: "..." }) → shows summary, requires user confirmation.
31
40
 
32
- DO NOT skip the preview. DO NOT publish without user confirmation.
33
- The tool will refuse to publish without a valid publish_token from a preview.
41
+ Each step REQUIRES the user's input before proceeding. DO NOT decide for the user.
42
+ DO NOT provide name, pricing, entries, or description without asking the user first.
34
43
 
35
- TRIGGER PATTERNS:
36
- - "Publish my entries" → library_publish({ name: "...", pricing: { type: "open" }, preview: true })
37
- - User says "yes, publish it" → library_publish({ ..., publish_token: "<token from preview>" })
38
-
39
- Examples:
40
- - Preview: library_publish({ name: "My Book", pricing: { type: "open" }, preview: true })
41
- - Publish: library_publish({ name: "My Book", pricing: { type: "open" }, consumption: "download", attestation: { original_work: true, no_secrets: true, terms_accepted: true }, publish_token: "abc123" })`,
44
+ If the user says "publish my entries" — call library_publish() with NO args to start the wizard.`,
42
45
  inputSchema: {
43
46
  type: 'object',
44
47
  properties: {
45
- name: {
46
- type: 'string',
47
- description: 'Book title (3-100 characters)',
48
- },
49
- description: {
50
- type: 'string',
51
- description: 'Short description of the book (optional, max 500 chars)',
48
+ entries: {
49
+ type: 'array',
50
+ items: { type: 'string' },
51
+ description: 'Entry filenames to include. Use ["all"] for everything. Only provide after showing user the entry list.',
52
52
  },
53
53
  pricing: {
54
54
  type: 'object',
55
55
  properties: {
56
- type: {
57
- type: 'string',
58
- enum: ['open', 'one_time', 'subscription'],
59
- description: 'Pricing model',
60
- },
61
- price_cents: {
62
- type: 'number',
63
- description: 'Price in cents (required for paid, min 100 = $1.00)',
64
- },
56
+ type: { type: 'string', enum: ['open', 'one_time', 'subscription'] },
57
+ price_cents: { type: 'number' },
65
58
  },
66
- required: ['type'],
67
- description: 'Pricing configuration',
59
+ description: 'Only provide after showing user the pricing options.',
68
60
  },
69
61
  consumption: {
70
62
  type: 'string',
71
63
  enum: ['inline', 'reference', 'download'],
72
- description: 'How buyers access content. download only for free books.',
73
- },
74
- attestation: {
75
- type: 'object',
76
- properties: {
77
- original_work: { type: 'boolean', description: 'Confirm this is original work' },
78
- no_secrets: { type: 'boolean', description: 'Confirm no secrets/credentials' },
79
- terms_accepted: { type: 'boolean', description: 'Accept library terms' },
80
- },
81
- required: ['original_work', 'no_secrets', 'terms_accepted'],
82
- description: 'Required confirmations before publishing',
83
- },
84
- preview: {
85
- type: 'boolean',
86
- description: 'If true, show what would be published without publishing. Returns a publish_token.',
87
- },
88
- publish_token: {
89
- type: 'string',
90
- description: 'Token from preview response. Required to actually publish. Single-use, expires in 5 minutes.',
91
- },
92
- entries: {
93
- type: 'array',
94
- items: { type: 'string' },
95
- description: 'Specific entry filenames to include (omit for all local/)',
96
- },
97
- tags: {
98
- type: 'array',
99
- items: { type: 'string' },
100
- description: 'Category/topic tags (max 10)',
101
- },
102
- license: {
103
- type: 'string',
104
- enum: ['open', 'open_attributed', 'personal'],
105
- description: 'License type (default: personal)',
64
+ description: 'How buyers access content. Only relevant for paid books.',
106
65
  },
66
+ name: { type: 'string', description: 'Book title. Only provide after user tells you what to call it.' },
67
+ description: { type: 'string', description: 'Book description. Only provide after user writes or approves it.' },
68
+ tags: { type: 'array', items: { type: 'string' }, description: 'Topic tags (max 10).' },
69
+ license: { type: 'string', enum: ['open', 'open_attributed', 'personal'] },
70
+ confirm_code: { type: 'string', description: 'Confirmation code from the final summary. User must type this back.' },
107
71
  },
108
- required: ['name', 'pricing'],
72
+ required: [],
109
73
  },
110
74
  async handler(args) {
111
- const { name, description, pricing, consumption, attestation, preview, publish_token, entries: entryFilter, tags, license } = args;
112
- // Validate name
113
- if (!name || typeof name !== 'string' || name.trim().length < 3) {
114
- throw new Error('Book name is required (minimum 3 characters)');
115
- }
116
- if (name.trim().length > 100) {
117
- throw new Error('Book name must be 100 characters or less');
118
- }
119
- // Validate pricing
120
- if (!pricing || !pricing.type) {
121
- throw new Error('Pricing type is required');
122
- }
123
- if (!['open', 'one_time', 'subscription'].includes(pricing.type)) {
124
- throw new Error('Pricing type must be: open, one_time, or subscription');
125
- }
126
- if (pricing.type !== 'open' && (!pricing.price_cents || pricing.price_cents < 100)) {
127
- throw new Error('Paid books require price_cents >= 100 ($1.00)');
128
- }
129
- if (pricing.price_cents && pricing.price_cents > 100000) {
130
- throw new Error('Price cannot exceed $1000.00 (100000 cents)');
131
- }
132
- // Validate description length
133
- if (description && description.length > 500) {
134
- throw new Error('Description must be 500 characters or less');
135
- }
136
- // Validate tags count
137
- if (tags && tags.length > 10) {
138
- throw new Error('Maximum 10 tags allowed');
139
- }
140
- // Collect entries from local/ (needed for preview and publish)
141
- const collectedEntries = await collectLocalEntries(entryFilter);
142
- // Scan for sensitive data before publishing
143
- const sensitiveFindings = scanForSensitiveData(collectedEntries);
144
- if (sensitiveFindings.length > 0) {
145
- const warnings = sensitiveFindings.map(f => ` ⚠ ${f.entry}: ${f.matches.join(', ')}`).join('\n');
146
- if (preview) {
147
- // In preview mode, show warnings but continue
75
+ const input = (args || {});
76
+ clearExpiredWizard();
77
+ // ======================================================================
78
+ // STEP 1: No wizard state Scan entries, show audit
79
+ // ======================================================================
80
+ if (!wizardState) {
81
+ const allEntries = await collectLocalEntries();
82
+ if (allEntries.length === 0) {
148
83
  return {
149
- success: true,
150
- preview: true,
151
- message: `⚠ SENSITIVE DATA DETECTED in ${sensitiveFindings.length} entry(s):\n${warnings}\n\nReview these entries before publishing. Remove credentials, API keys, passwords, and personal data.`,
84
+ success: false,
85
+ message: 'No entries found in .librarian/local/. Use record() to create entries first.',
152
86
  };
153
87
  }
154
- // In publish mode, block and require cleanup
155
- return {
156
- success: false,
157
- message: `🚫 Publish blocked — sensitive data detected in ${sensitiveFindings.length} entry(s):\n${warnings}\n\nClean up these entries with record() or delete() before publishing. Use library_publish({ preview: true }) to re-check.`,
158
- };
159
- }
160
- if (collectedEntries.length === 0) {
161
- return {
162
- success: false,
163
- message: 'No entries found in .librarian/local/. Use record() to create entries first.',
164
- };
165
- }
166
- // Format pricing display
167
- const pricingDisplay = pricing.type === 'open'
168
- ? 'Free'
169
- : `$${((pricing.price_cents || 0) / 100).toFixed(2)}`;
170
- // Handle preview mode - return summary with publish token
171
- if (preview) {
172
- const token = crypto.randomBytes(16).toString('hex');
173
- pendingPublish = {
174
- token,
175
- name: name.trim(),
176
- entries_count: collectedEntries.length,
88
+ // Run sensitive data scan
89
+ const sensitiveFindings = scanForSensitiveData(allEntries);
90
+ // Group entries by folder/topic
91
+ const groups = {};
92
+ for (const entry of allEntries) {
93
+ const rel = path.relative(getLocalPath(getLibraryPath()), entry.originalPath);
94
+ const folder = path.dirname(rel);
95
+ const key = folder === '.' ? 'root' : folder;
96
+ if (!groups[key])
97
+ groups[key] = [];
98
+ groups[key].push(entry.title);
99
+ }
100
+ // Build topic breakdown
101
+ const topicLines = Object.entries(groups)
102
+ .sort((a, b) => b[1].length - a[1].length)
103
+ .map(([folder, titles]) => ` ${folder}/ (${titles.length} entries)`)
104
+ .join('\n');
105
+ // Build warnings
106
+ const warnings = [];
107
+ if (sensitiveFindings.length > 0) {
108
+ warnings.push(`\n⚠️ SENSITIVE DATA found in ${sensitiveFindings.length} entry(s):`);
109
+ for (const f of sensitiveFindings) {
110
+ warnings.push(` ⚠ "${f.entry}": ${f.matches.join(', ')}`);
111
+ }
112
+ warnings.push('\nThese entries should be cleaned up before publishing.');
113
+ }
114
+ // Save state
115
+ wizardState = {
116
+ step: 'select_entries',
117
+ allEntries,
118
+ sensitiveWarnings: warnings.length > 0 ? warnings : undefined,
177
119
  created: Date.now(),
178
120
  };
179
121
  return {
180
122
  success: true,
181
- preview: true,
182
- message: `Preview of "${name.trim()}" - NOT published yet.\n\n⚠️ Show this to the user and ask for confirmation before publishing.`,
183
- publish_token: token,
184
- summary: {
185
- name: name.trim(),
186
- pricing: { type: pricing.type, display: pricingDisplay },
187
- entries_count: collectedEntries.length,
188
- entries: collectedEntries.map(e => ({
189
- title: e.title,
190
- file: path.basename(e.originalPath),
191
- })),
192
- },
193
- next_steps: 'Show preview to user. After they confirm, call library_publish() again with the publish_token to publish.',
194
- };
195
- }
196
- // ========================================================================
197
- // PUBLISH TOKEN VALIDATION
198
- // Cannot publish without a valid token from preview
199
- // ========================================================================
200
- if (!publish_token) {
201
- return {
202
- success: false,
203
- message: '🚫 Publishing requires a publish_token from a preview.\n\nYou must call library_publish({ preview: true, ... }) first, show the preview to the user, get their confirmation, then call again with the publish_token.\n\nThis is a safety measure to prevent accidental publishing.',
123
+ step: 'select_entries',
124
+ message: `📚 Found ${allEntries.length} entries in your library.\n\nTopics:\n${topicLines}${warnings.length > 0 ? '\n' + warnings.join('\n') : ''}`,
125
+ entries_count: allEntries.length,
126
+ ask_user: 'Which entries do you want to include? Say "all" or list specific ones to exclude.',
204
127
  };
205
128
  }
206
- if (!pendingPublish || pendingPublish.token !== publish_token) {
207
- return {
208
- success: false,
209
- message: '🚫 Invalid or expired publish_token. Run a new preview first with library_publish({ preview: true, ... }).',
210
- };
211
- }
212
- if (Date.now() - pendingPublish.created > TOKEN_EXPIRY_MS) {
213
- pendingPublish = null;
214
- return {
215
- success: false,
216
- message: '🚫 Publish token expired (5 minute limit). Run a new preview first.',
217
- };
218
- }
219
- // Token is valid consume it (single use)
220
- pendingPublish = null;
221
- // Validate consumption type (required for actual publish)
222
- if (!consumption) {
223
- return {
224
- success: false,
225
- message: 'Consumption type required. Choose how buyers access your content:',
226
- options: {
227
- inline: 'Content returned in API responses (best for small entries)',
228
- reference: 'README + pointers to entries (best for larger books)',
229
- download: 'Download to local library (only for free/open books)',
230
- },
231
- };
232
- }
233
- if (!['inline', 'reference', 'download'].includes(consumption)) {
234
- return {
235
- success: false,
236
- message: 'Invalid consumption type. Must be: inline, reference, or download',
237
- };
238
- }
239
- if (consumption === 'download' && pricing.type !== 'open') {
240
- return {
241
- success: false,
242
- message: 'Download is only for free books. Paid content uses inline or reference.',
243
- next_steps: "Use pricing.type: 'open' for download, or consumption: 'inline'/'reference' for paid.",
244
- };
245
- }
246
- // Validate attestation (required for actual publish)
247
- if (!attestation) {
248
- return {
249
- success: false,
250
- message: 'Attestation required. Please confirm:',
251
- required: {
252
- original_work: 'This is my original work or I have rights to publish',
253
- no_secrets: 'Contains no secrets, credentials, or sensitive data',
254
- terms_accepted: 'I accept the Telvok library terms',
255
- },
256
- };
257
- }
258
- const failedAttestations = [];
259
- if (!attestation.original_work)
260
- failedAttestations.push('original_work');
261
- if (!attestation.no_secrets)
262
- failedAttestations.push('no_secrets');
263
- if (!attestation.terms_accepted)
264
- failedAttestations.push('terms_accepted');
265
- if (failedAttestations.length > 0) {
266
- return {
267
- success: false,
268
- message: `All attestation fields must be true to publish. Failed: ${failedAttestations.join(', ')}`,
269
- };
270
- }
271
- // Check authentication
272
- const apiKey = await loadApiKey();
273
- if (!apiKey) {
129
+ // ======================================================================
130
+ // STEP 2: Select entries → Show pricing options
131
+ // ======================================================================
132
+ if (wizardState.step === 'select_entries') {
133
+ if (!input.entries || input.entries.length === 0) {
134
+ return {
135
+ success: false,
136
+ step: 'select_entries',
137
+ message: 'Waiting for entry selection. Ask the user which entries to include.',
138
+ ask_user: 'Which entries do you want to include? Say "all" or list specific ones.',
139
+ };
140
+ }
141
+ let selected;
142
+ if (input.entries.length === 1 && input.entries[0].toLowerCase() === 'all') {
143
+ selected = [...wizardState.allEntries];
144
+ }
145
+ else {
146
+ selected = wizardState.allEntries.filter(e => {
147
+ const filename = path.basename(e.originalPath);
148
+ return input.entries.some(f => filename === f ||
149
+ filename === f + '.md' ||
150
+ e.originalPath.endsWith(f) ||
151
+ e.originalPath.endsWith(f + '.md') ||
152
+ e.title.toLowerCase().includes(f.toLowerCase()));
153
+ });
154
+ if (selected.length === 0) {
155
+ return {
156
+ success: false,
157
+ step: 'select_entries',
158
+ message: 'No entries matched your selection. Try again with different names or say "all".',
159
+ };
160
+ }
161
+ }
162
+ // Re-check sensitive data on selected entries only
163
+ const sensitiveFindings = scanForSensitiveData(selected);
164
+ if (sensitiveFindings.length > 0) {
165
+ const warnings = sensitiveFindings.map(f => ` ⚠ "${f.entry}": ${f.matches.join(', ')}`).join('\n');
166
+ return {
167
+ success: false,
168
+ step: 'select_entries',
169
+ message: `🚫 Cannot proceed — sensitive data detected in selected entries:\n${warnings}\n\nClean up these entries with record() or delete() first, then start over with library_publish().`,
170
+ };
171
+ }
172
+ wizardState.selectedEntries = selected;
173
+ wizardState.step = 'set_pricing';
274
174
  return {
275
- success: false,
276
- message: 'Not authenticated. Run auth({ action: "login" }) to connect your Telvok account first.',
175
+ success: true,
176
+ step: 'set_pricing',
177
+ message: `✓ ${selected.length} entries selected.\n\nChoose a pricing model:\n\n 📖 open — Free. Anyone can download to their local library.\n 💰 one_time — One-time purchase. You set the price (min $1). Cloud-only access. 20% platform fee.\n 🔄 subscription — Monthly subscription. You set the price (min $1/mo). Cloud-only, always latest. 20% platform fee.`,
178
+ selected_count: selected.length,
179
+ ask_user: 'Which pricing model? If paid, what price?',
277
180
  };
278
181
  }
279
- // Format entries for API
280
- const apiEntries = collectedEntries.map(e => ({
281
- title: e.title,
282
- content: e.content,
283
- intent: e.intent,
284
- context: e.context,
285
- reasoning: e.reasoning,
286
- example: e.example,
287
- }));
288
- // Build request body
289
- const requestBody = {
290
- name: name.trim(),
291
- description: description?.trim(),
292
- pricing,
293
- consumption,
294
- entries: apiEntries,
295
- tags: tags || [],
296
- license_type: license || 'personal',
297
- attestation,
298
- };
299
- try {
300
- const response = await fetch(`${TELVOK_API_URL}/api/publish`, {
301
- method: 'POST',
302
- headers: {
303
- 'Authorization': `Bearer ${apiKey}`,
304
- 'Content-Type': 'application/json',
305
- },
306
- body: JSON.stringify(requestBody),
307
- });
308
- const data = await response.json();
309
- if (!response.ok) {
310
- // Handle Stripe Connect requirement
311
- if (data.error === 'stripe_connect_required') {
182
+ // ======================================================================
183
+ // STEP 3: Set pricing → Ask for name/description
184
+ // ======================================================================
185
+ if (wizardState.step === 'set_pricing') {
186
+ if (!input.pricing || !input.pricing.type) {
187
+ return {
188
+ success: false,
189
+ step: 'set_pricing',
190
+ message: 'Waiting for pricing selection. Ask the user which pricing model they want.',
191
+ ask_user: 'Which pricing model? open (free), one_time, or subscription?',
192
+ };
193
+ }
194
+ if (!['open', 'one_time', 'subscription'].includes(input.pricing.type)) {
195
+ return {
196
+ success: false,
197
+ step: 'set_pricing',
198
+ message: 'Invalid pricing type. Must be: open, one_time, or subscription.',
199
+ };
200
+ }
201
+ if (input.pricing.type !== 'open') {
202
+ if (!input.pricing.price_cents || input.pricing.price_cents < 100) {
312
203
  return {
313
204
  success: false,
314
- message: `Stripe Connect required to sell paid content.\n\nComplete setup at: ${data.setup_url}`,
315
- setup_url: data.setup_url,
205
+ step: 'set_pricing',
206
+ message: 'Paid books require a price of at least $1.00 (100 cents).',
207
+ ask_user: 'What price? (in dollars, e.g. $5 = 500 cents)',
316
208
  };
317
209
  }
318
- // Handle validation errors
319
- if (data.error === 'validation_error') {
320
- const details = Object.entries(data.details || {})
321
- .map(([k, v]) => ` - ${k}: ${v}`)
322
- .join('\n');
210
+ if (input.pricing.price_cents > 100000) {
323
211
  return {
324
212
  success: false,
325
- message: `Validation failed:\n${details}`,
213
+ step: 'set_pricing',
214
+ message: 'Maximum price is $1000.00.',
326
215
  };
327
216
  }
217
+ }
218
+ // Set consumption based on pricing
219
+ let consumption = input.consumption;
220
+ if (input.pricing.type === 'open') {
221
+ consumption = 'download';
222
+ }
223
+ else if (!consumption || !['inline', 'reference'].includes(consumption)) {
224
+ consumption = 'inline'; // default for paid
225
+ }
226
+ wizardState.pricing = input.pricing;
227
+ wizardState.consumption = consumption;
228
+ wizardState.step = 'set_details';
229
+ const priceDisplay = input.pricing.type === 'open'
230
+ ? 'Free (download)'
231
+ : `$${(input.pricing.price_cents / 100).toFixed(2)}/${input.pricing.type === 'subscription' ? 'mo' : 'once'} (20% platform fee)`;
232
+ return {
233
+ success: true,
234
+ step: 'set_details',
235
+ message: `✓ Pricing: ${priceDisplay}\n\nNow give your book a name and description.`,
236
+ ask_user: 'What do you want to call this book? And a short description (optional, max 500 chars)?',
237
+ };
238
+ }
239
+ // ======================================================================
240
+ // STEP 4: Set details → Final confirmation
241
+ // ======================================================================
242
+ if (wizardState.step === 'set_details') {
243
+ if (!input.name || input.name.trim().length < 3) {
244
+ return {
245
+ success: false,
246
+ step: 'set_details',
247
+ message: 'Book name required (at least 3 characters).',
248
+ ask_user: 'What do you want to call this book?',
249
+ };
250
+ }
251
+ if (input.name.trim().length > 100) {
252
+ return {
253
+ success: false,
254
+ step: 'set_details',
255
+ message: 'Book name must be 100 characters or less.',
256
+ };
257
+ }
258
+ if (input.description && input.description.length > 500) {
259
+ return {
260
+ success: false,
261
+ step: 'set_details',
262
+ message: 'Description must be 500 characters or less.',
263
+ };
264
+ }
265
+ wizardState.name = input.name.trim();
266
+ wizardState.description = input.description?.trim();
267
+ wizardState.tags = input.tags;
268
+ wizardState.license = input.license || 'personal';
269
+ wizardState.step = 'confirm';
270
+ // Generate confirmation code
271
+ const confirmCode = crypto.randomBytes(3).toString('hex').toUpperCase(); // 6 chars
272
+ const priceDisplay = wizardState.pricing.type === 'open'
273
+ ? 'Free (download)'
274
+ : `$${(wizardState.pricing.price_cents / 100).toFixed(2)}/${wizardState.pricing.type === 'subscription' ? 'mo' : 'once'}`;
275
+ // Try native dialog on macOS
276
+ let confirmMethod = 'code';
277
+ if (platform() === 'darwin') {
278
+ try {
279
+ const dialogText = [
280
+ `Publish "${wizardState.name}" to Telvok?`,
281
+ ``,
282
+ `${wizardState.selectedEntries.length} entries — ${priceDisplay}`,
283
+ wizardState.description ? `"${wizardState.description}"` : '',
284
+ ``,
285
+ `This action publishes to the marketplace.`,
286
+ ].filter(Boolean).join('\\n');
287
+ const result = execSync(`osascript -e 'display dialog "${dialogText}" buttons {"Cancel", "Publish"} default button "Cancel" with title "Telvok Publish"'`, { encoding: 'utf-8', timeout: 120000 });
288
+ if (result.includes('Publish')) {
289
+ confirmMethod = 'dialog_confirmed';
290
+ }
291
+ }
292
+ catch {
293
+ // User clicked Cancel or osascript failed — fall through to code method
294
+ confirmMethod = 'dialog_cancelled';
295
+ }
296
+ }
297
+ if (confirmMethod === 'dialog_confirmed') {
298
+ // User confirmed via native dialog — publish immediately
299
+ return await executePublish(wizardState);
300
+ }
301
+ if (confirmMethod === 'dialog_cancelled') {
302
+ wizardState = null;
328
303
  return {
329
304
  success: false,
330
- message: data.error || `Publish failed: HTTP ${response.status}`,
305
+ message: 'Publish cancelled. Run library_publish() to start over.',
331
306
  };
332
307
  }
308
+ // Fallback: confirmation code
309
+ // Store the code in wizard state for validation
310
+ wizardState.confirm_code = confirmCode;
333
311
  return {
334
312
  success: true,
335
- message: data.message || `Published "${data.book?.name}" with ${data.entries_count} entries`,
336
- book: data.book,
337
- entries_count: data.entries_count,
313
+ step: 'confirm',
314
+ message: `📋 PUBLISH SUMMARY\n\n Name: ${wizardState.name}\n Entries: ${wizardState.selectedEntries.length}\n Pricing: ${priceDisplay}${wizardState.description ? `\n Description: ${wizardState.description}` : ''}${wizardState.tags?.length ? `\n Tags: ${wizardState.tags.join(', ')}` : ''}\n\n🔑 Confirmation code: ${confirmCode}`,
315
+ ask_user: `To publish, the user must type back the code: ${confirmCode}`,
338
316
  };
339
317
  }
340
- catch (error) {
341
- const message = error instanceof Error ? error.message : String(error);
342
- throw new Error(`Publish failed: ${message}`);
318
+ // ======================================================================
319
+ // STEP 5: Confirm with code Publish
320
+ // ======================================================================
321
+ if (wizardState.step === 'confirm') {
322
+ const stored = wizardState.confirm_code;
323
+ if (!input.confirm_code) {
324
+ return {
325
+ success: false,
326
+ step: 'confirm',
327
+ message: 'Confirmation code required. The user must type the code shown in the summary.',
328
+ ask_user: 'Please type the confirmation code to publish.',
329
+ };
330
+ }
331
+ if (input.confirm_code.toUpperCase() !== stored?.toUpperCase()) {
332
+ return {
333
+ success: false,
334
+ step: 'confirm',
335
+ message: `Wrong code. Expected: ${stored}. Got: ${input.confirm_code}. Try again or run library_publish() to start over.`,
336
+ };
337
+ }
338
+ return await executePublish(wizardState);
343
339
  }
340
+ return { success: false, message: 'Unknown state. Run library_publish() to start over.' };
344
341
  },
345
342
  };
346
343
  // ============================================================================
344
+ // Execute Publish — called after confirmation
345
+ // ============================================================================
346
+ async function executePublish(state) {
347
+ const apiKey = await loadApiKey();
348
+ if (!apiKey) {
349
+ wizardState = null;
350
+ return {
351
+ success: false,
352
+ message: 'Not authenticated. Run auth({ action: "login" }) first.',
353
+ };
354
+ }
355
+ const apiEntries = state.selectedEntries.map(e => ({
356
+ title: e.title,
357
+ content: e.content,
358
+ intent: e.intent,
359
+ context: e.context,
360
+ reasoning: e.reasoning,
361
+ example: e.example,
362
+ }));
363
+ const requestBody = {
364
+ name: state.name,
365
+ description: state.description,
366
+ pricing: state.pricing,
367
+ consumption: state.consumption,
368
+ entries: apiEntries,
369
+ tags: state.tags || [],
370
+ license_type: state.license || 'personal',
371
+ attestation: {
372
+ original_work: true,
373
+ no_secrets: true,
374
+ terms_accepted: true,
375
+ },
376
+ };
377
+ try {
378
+ const response = await fetch(`${TELVOK_API_URL}/api/publish`, {
379
+ method: 'POST',
380
+ headers: {
381
+ 'Authorization': `Bearer ${apiKey}`,
382
+ 'Content-Type': 'application/json',
383
+ },
384
+ body: JSON.stringify(requestBody),
385
+ });
386
+ const data = await response.json();
387
+ wizardState = null; // Clear state after attempt
388
+ if (!response.ok) {
389
+ if (data.error === 'stripe_connect_required') {
390
+ return {
391
+ success: false,
392
+ message: `Stripe Connect required to sell paid content.\n\nComplete setup at: ${data.setup_url}`,
393
+ setup_url: data.setup_url,
394
+ };
395
+ }
396
+ if (data.error === 'validation_error') {
397
+ const details = Object.entries(data.details || {})
398
+ .map(([k, v]) => ` - ${k}: ${v}`)
399
+ .join('\n');
400
+ return { success: false, message: `Validation failed:\n${details}` };
401
+ }
402
+ return { success: false, message: data.error || `Publish failed: HTTP ${response.status}` };
403
+ }
404
+ return {
405
+ success: true,
406
+ message: `✅ Published "${data.book?.name}" with ${data.entries_count} entries\n\n🔗 ${data.book?.url}`,
407
+ book: data.book,
408
+ entries_count: data.entries_count,
409
+ };
410
+ }
411
+ catch (error) {
412
+ wizardState = null;
413
+ const message = error instanceof Error ? error.message : String(error);
414
+ throw new Error(`Publish failed: ${message}`);
415
+ }
416
+ }
417
+ // ============================================================================
347
418
  // Entry Collection
348
419
  // ============================================================================
349
420
  async function collectLocalEntries(filter) {
@@ -354,7 +425,6 @@ async function collectLocalEntries(filter) {
354
425
  const files = await glob(path.join(localPath, '**/*.md'), { nodir: true });
355
426
  for (const filePath of files) {
356
427
  const filename = path.basename(filePath);
357
- // If filter specified, only include matching files
358
428
  if (filter && filter.length > 0) {
359
429
  const matchesFilter = filter.some(f => filename === f ||
360
430
  filename === f + '.md' ||
@@ -367,10 +437,7 @@ async function collectLocalEntries(filter) {
367
437
  const content = await fs.readFile(filePath, 'utf-8');
368
438
  const parsed = parseEntryFile(content, filePath);
369
439
  if (parsed) {
370
- entries.push({
371
- ...parsed,
372
- originalPath: filePath,
373
- });
440
+ entries.push({ ...parsed, originalPath: filePath });
374
441
  }
375
442
  }
376
443
  catch {
@@ -388,7 +455,6 @@ function parseEntryFile(content, filePath) {
388
455
  const trimmedBody = body.trim();
389
456
  if (!trimmedBody)
390
457
  return null;
391
- // Extract title from frontmatter, H1, or filename
392
458
  let title = frontmatter.title;
393
459
  if (!title) {
394
460
  const headingMatch = trimmedBody.match(/^#\s+(.+)$/m);
@@ -396,13 +462,11 @@ function parseEntryFile(content, filePath) {
396
462
  title = headingMatch[1].trim();
397
463
  }
398
464
  else {
399
- // Use filename as title, converting hyphens to spaces
400
465
  title = path.basename(filePath, '.md')
401
466
  .replace(/-/g, ' ')
402
467
  .replace(/\b\w/g, l => l.toUpperCase());
403
468
  }
404
469
  }
405
- // Extract sections from body
406
470
  const sections = extractSections(trimmedBody);
407
471
  return {
408
472
  title,
@@ -414,30 +478,21 @@ function parseEntryFile(content, filePath) {
414
478
  };
415
479
  }
416
480
  function extractSections(body) {
417
- const result = {
418
- main: body,
419
- };
420
- // Find ## Reasoning section
481
+ const result = { main: body };
421
482
  const reasoningMatch = body.match(/##\s*Reasoning\s*\n([\s\S]*?)(?=##|$)/i);
422
- if (reasoningMatch) {
483
+ if (reasoningMatch)
423
484
  result.reasoning = reasoningMatch[1].trim();
424
- }
425
- // Find ## Example section
426
485
  const exampleMatch = body.match(/##\s*Example\s*\n([\s\S]*?)(?=##|$)/i);
427
- if (exampleMatch) {
486
+ if (exampleMatch)
428
487
  result.example = exampleMatch[1].trim();
429
- }
430
- // Main content is everything after title until first ## section
431
488
  const mainMatch = body.match(/^#\s+.+\n\n?([\s\S]*?)(?=##|$)/);
432
489
  if (mainMatch) {
433
490
  result.main = mainMatch[1].trim();
434
491
  }
435
492
  else {
436
- // If no H1 header, take content before first ## section
437
493
  const beforeSections = body.match(/^([\s\S]*?)(?=##)/);
438
- if (beforeSections) {
494
+ if (beforeSections)
439
495
  result.main = beforeSections[1].trim();
440
- }
441
496
  }
442
497
  return result;
443
498
  }
@@ -9,11 +9,7 @@ export declare const libraryUnpublishTool: {
9
9
  type: string;
10
10
  description: string;
11
11
  };
12
- preview: {
13
- type: string;
14
- description: string;
15
- };
16
- unpublish_token: {
12
+ confirm_code: {
17
13
  type: string;
18
14
  description: string;
19
15
  };
@@ -21,31 +17,23 @@ export declare const libraryUnpublishTool: {
21
17
  required: string[];
22
18
  };
23
19
  handler(args: unknown): Promise<{
20
+ success: boolean;
21
+ message: any;
22
+ book?: undefined;
23
+ } | {
24
+ success: boolean;
25
+ message: any;
26
+ book: any;
27
+ } | {
24
28
  success: boolean;
25
29
  preview: boolean;
26
30
  message: string;
27
- unpublish_token: string;
31
+ ask_user: string;
28
32
  book: {
29
- slug: any;
33
+ slug: string;
30
34
  name: any;
31
35
  entries_count: any;
32
36
  pricing: any;
33
- url: any;
34
37
  };
35
- next_steps: string;
36
- } | {
37
- success: boolean;
38
- message: any;
39
- preview?: undefined;
40
- unpublish_token?: undefined;
41
- book?: undefined;
42
- next_steps?: undefined;
43
- } | {
44
- success: boolean;
45
- message: any;
46
- book: any;
47
- preview?: undefined;
48
- unpublish_token?: undefined;
49
- next_steps?: undefined;
50
38
  }>;
51
39
  };
@@ -1,12 +1,19 @@
1
1
  // ============================================================================
2
2
  // Marketplace Unpublish Tool
3
- // Remove a published book from Telvok library
3
+ // Two-step: preview osascript dialog / confirmation code → delete
4
4
  // ============================================================================
5
5
  import * as crypto from 'crypto';
6
+ import { execSync } from 'child_process';
7
+ import { platform } from 'os';
6
8
  import { loadApiKey } from './auth.js';
7
9
  const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
8
- const TOKEN_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
9
- let pendingUnpublish = null;
10
+ const EXPIRY_MS = 5 * 60 * 1000;
11
+ let pending = null;
12
+ function clearExpired() {
13
+ if (pending && Date.now() - pending.created > EXPIRY_MS) {
14
+ pending = null;
15
+ }
16
+ }
10
17
  // ============================================================================
11
18
  // Tool Definition
12
19
  // ============================================================================
@@ -15,24 +22,22 @@ export const libraryUnpublishTool = {
15
22
  title: 'Unpublish Book',
16
23
  description: `Remove a published book from Telvok marketplace.
17
24
 
18
- ⚠️ TWO-STEP UNPUBLISH FLOW (MANDATORY):
25
+ TWO-STEP FLOW:
19
26
 
20
- Step 1: ALWAYS call with preview: true first. Shows what will be deleted
21
- and returns an unpublish_token. Show the preview to the user and ASK FOR CONFIRMATION.
27
+ Step 1: Call with just slug. Shows book details and what will be deleted.
28
+ On macOS, a native confirmation dialog appears the agent CANNOT bypass it.
29
+ On other platforms, a confirmation code is returned that the user must type back.
22
30
 
23
- Step 2: ONLY after the user explicitly confirms, call again with the unpublish_token
24
- from the preview response. Unpublishing WITHOUT a valid token will be rejected.
31
+ Step 2 (non-macOS only): Call again with slug + confirm_code from the user.
25
32
 
26
33
  RESTRICTIONS:
27
34
  - Cannot unpublish books with active purchases
28
35
  - Deletion is PERMANENT — all entries are removed from marketplace
29
36
 
30
- TRIGGER PATTERNS:
31
- - "Unpublish my book" → library_unpublish({ slug: "...", preview: true })
32
- - "Remove from marketplace" → library_unpublish({ slug: "...", preview: true })
33
- - User says "yes, delete it" → library_unpublish({ slug: "...", unpublish_token: "<token>" })
37
+ Use my_books() first to see your published books and their slugs.
34
38
 
35
- Use my_books() first to see your published books and their slugs.`,
39
+ DO NOT decide to unpublish without the user explicitly asking.
40
+ Show the preview details and wait for user confirmation.`,
36
41
  inputSchema: {
37
42
  type: 'object',
38
43
  properties: {
@@ -40,122 +45,120 @@ Use my_books() first to see your published books and their slugs.`,
40
45
  type: 'string',
41
46
  description: 'Book slug (from my_books output)',
42
47
  },
43
- preview: {
44
- type: 'boolean',
45
- description: 'If true, show what would be deleted without deleting. Returns an unpublish_token.',
46
- },
47
- unpublish_token: {
48
+ confirm_code: {
48
49
  type: 'string',
49
- description: 'Token from preview response. Required to actually unpublish. Single-use, expires in 5 minutes.',
50
+ description: 'Confirmation code from preview. User must type this back. Only needed on non-macOS.',
50
51
  },
51
52
  },
52
53
  required: ['slug'],
53
54
  },
54
55
  async handler(args) {
55
- const { slug, preview, unpublish_token } = (args || {});
56
- // Validate slug
56
+ const { slug, confirm_code } = (args || {});
57
+ clearExpired();
57
58
  if (!slug || typeof slug !== 'string') {
58
59
  return { success: false, message: 'slug is required. Use my_books() to see your published books.' };
59
60
  }
60
- // Load API key
61
61
  const apiKey = await loadApiKey();
62
62
  if (!apiKey) {
63
- return {
64
- success: false,
65
- message: 'Not authenticated. Run auth({ action: "login" }) first.',
66
- };
63
+ return { success: false, message: 'Not authenticated. Run auth({ action: "login" }) first.' };
67
64
  }
68
65
  // ========================================================================
69
- // PREVIEW MODEshow what will be deleted, generate token
66
+ // CONFIRM STEPuser typed back the code
70
67
  // ========================================================================
71
- if (preview) {
72
- // Fetch book details via my-books to confirm it exists and we own it
73
- const res = await fetch(`${TELVOK_API_URL}/api/my-books`, {
74
- headers: { 'Authorization': `Bearer ${apiKey}` },
75
- });
76
- if (!res.ok) {
77
- return { success: false, message: `Failed to fetch books: ${res.status}` };
68
+ if (confirm_code) {
69
+ if (!pending || pending.slug !== slug) {
70
+ return {
71
+ success: false,
72
+ message: 'No pending unpublish for this slug. Call library_unpublish({ slug }) first to preview.',
73
+ };
78
74
  }
79
- const data = await res.json();
80
- const book = data.published?.find((b) => b.slug === slug);
81
- if (!book) {
75
+ if (confirm_code.toUpperCase() !== pending.confirmCode.toUpperCase()) {
82
76
  return {
83
77
  success: false,
84
- message: `No published book found with slug "${slug}". Use my_books() to see your books.`,
78
+ message: `Wrong code. Expected: ${pending.confirmCode}. Got: ${confirm_code}. Try again.`,
85
79
  };
86
80
  }
87
- // Generate token
88
- const token = crypto.randomBytes(16).toString('hex');
89
- pendingUnpublish = { token, slug, created: Date.now() };
90
- return {
91
- success: true,
92
- preview: true,
93
- message: `Preview of unpublish — NOT deleted yet.\n\n⚠️ Show this to the user and ask for confirmation before unpublishing.`,
94
- unpublish_token: token,
95
- book: {
96
- slug: book.slug,
97
- name: book.name,
98
- entries_count: book.entries_count,
99
- pricing: book.pricing,
100
- url: book.url,
101
- },
102
- next_steps: 'Show preview to user. After they confirm, call library_unpublish() again with the unpublish_token to delete.',
103
- };
81
+ pending = null;
82
+ return await executeUnpublish(slug, apiKey);
104
83
  }
105
84
  // ========================================================================
106
- // EXECUTE MODEvalidate token, call API to delete
85
+ // PREVIEW STEPfetch book details, show confirmation
107
86
  // ========================================================================
108
- // Token required
109
- if (!unpublish_token) {
110
- return {
111
- success: false,
112
- message: '🚫 Unpublishing requires an unpublish_token. Call with preview: true first to get one.',
113
- };
114
- }
115
- // Validate token
116
- if (!pendingUnpublish || pendingUnpublish.token !== unpublish_token) {
117
- return {
118
- success: false,
119
- message: '🚫 Invalid or expired unpublish_token. Run a new preview to get a fresh token.',
120
- };
121
- }
122
- // Check expiry
123
- if (Date.now() - pendingUnpublish.created > TOKEN_EXPIRY_MS) {
124
- pendingUnpublish = null;
125
- return {
126
- success: false,
127
- message: '🚫 Unpublish token expired (5 minute limit). Run a new preview.',
128
- };
129
- }
130
- // Check slug matches
131
- if (pendingUnpublish.slug !== slug) {
132
- return {
133
- success: false,
134
- message: `🚫 Token was generated for slug "${pendingUnpublish.slug}", not "${slug}". Run a new preview.`,
135
- };
136
- }
137
- // Consume token (single use)
138
- pendingUnpublish = null;
139
- // Call API
140
- const res = await fetch(`${TELVOK_API_URL}/api/publish`, {
141
- method: 'DELETE',
142
- headers: {
143
- 'Authorization': `Bearer ${apiKey}`,
144
- 'Content-Type': 'application/json',
145
- },
146
- body: JSON.stringify({ slug }),
87
+ const res = await fetch(`${TELVOK_API_URL}/api/my-books`, {
88
+ headers: { 'Authorization': `Bearer ${apiKey}` },
147
89
  });
148
- const data = await res.json();
149
90
  if (!res.ok) {
91
+ return { success: false, message: `Failed to fetch books: ${res.status}` };
92
+ }
93
+ const data = await res.json();
94
+ const book = data.published?.find((b) => b.slug === slug);
95
+ if (!book) {
150
96
  return {
151
97
  success: false,
152
- message: data.message || data.error || `Unpublish failed: ${res.status}`,
98
+ message: `No published book found with slug "${slug}". Use my_books() to see your books.`,
153
99
  };
154
100
  }
101
+ const bookName = book.name || slug;
102
+ const entriesCount = book.entries_count || book.entry_count || 0;
103
+ const pricing = book.pricing_type || book.pricing || 'unknown';
104
+ // Try native dialog on macOS
105
+ if (platform() === 'darwin') {
106
+ try {
107
+ const dialogText = [
108
+ `Permanently unpublish "${bookName}"?`,
109
+ ``,
110
+ `${entriesCount} entries — ${pricing}`,
111
+ ``,
112
+ `This removes the book from the marketplace.`,
113
+ `This action cannot be undone.`,
114
+ ].join('\\n');
115
+ const result = execSync(`osascript -e 'display dialog "${dialogText}" buttons {"Cancel", "Delete"} default button "Cancel" with title "Telvok Unpublish" with icon caution'`, { encoding: 'utf-8', timeout: 120000 });
116
+ if (result.includes('Delete')) {
117
+ return await executeUnpublish(slug, apiKey);
118
+ }
119
+ }
120
+ catch {
121
+ // User clicked Cancel or osascript failed
122
+ return {
123
+ success: false,
124
+ message: 'Unpublish cancelled.',
125
+ };
126
+ }
127
+ }
128
+ // Fallback: confirmation code
129
+ const confirmCode = crypto.randomBytes(3).toString('hex').toUpperCase();
130
+ pending = { slug, bookName, entriesCount, pricing, confirmCode, created: Date.now() };
155
131
  return {
156
132
  success: true,
157
- message: data.message || `Unpublished "${slug}" from marketplace`,
158
- book: data.book,
133
+ preview: true,
134
+ message: `⚠️ UNPUBLISH PREVIEW\n\n Book: ${bookName}\n Slug: ${slug}\n Entries: ${entriesCount}\n Pricing: ${pricing}\n\n This action is PERMANENT.\n\n🔑 Confirmation code: ${confirmCode}`,
135
+ ask_user: `To delete this book from the marketplace, the user must type back the code: ${confirmCode}`,
136
+ book: { slug, name: bookName, entries_count: entriesCount, pricing },
159
137
  };
160
138
  },
161
139
  };
140
+ // ============================================================================
141
+ // Execute Unpublish — called after confirmation
142
+ // ============================================================================
143
+ async function executeUnpublish(slug, apiKey) {
144
+ const res = await fetch(`${TELVOK_API_URL}/api/publish`, {
145
+ method: 'DELETE',
146
+ headers: {
147
+ 'Authorization': `Bearer ${apiKey}`,
148
+ 'Content-Type': 'application/json',
149
+ },
150
+ body: JSON.stringify({ slug }),
151
+ });
152
+ const data = await res.json();
153
+ if (!res.ok) {
154
+ return {
155
+ success: false,
156
+ message: data.message || data.error || `Unpublish failed: ${res.status}`,
157
+ };
158
+ }
159
+ return {
160
+ success: true,
161
+ message: data.message || `Unpublished "${slug}" from marketplace`,
162
+ book: data.book,
163
+ };
164
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telvok/librarian-mcp",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Knowledge capture MCP server - remember what you learn with AI",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",