@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.
- package/dist/tools/library-publish.d.ts +57 -67
- package/dist/tools/library-publish.js +353 -298
- package/dist/tools/library-unpublish.d.ts +11 -23
- package/dist/tools/library-unpublish.js +102 -99
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
57
|
+
required: never[];
|
|
114
58
|
};
|
|
115
|
-
handler(args: unknown): Promise<
|
|
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
|
-
//
|
|
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
|
|
15
|
-
let
|
|
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
|
|
23
|
-
|
|
24
|
-
⚠️ TWO-STEP PUBLISH FLOW (MANDATORY):
|
|
30
|
+
description: `Publish local entries as a book on Telvok marketplace.
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
type: '
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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: [
|
|
72
|
+
required: [],
|
|
109
73
|
},
|
|
110
74
|
async handler(args) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
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:
|
|
150
|
-
|
|
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
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
182
|
-
message:
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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:
|
|
276
|
-
|
|
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
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
ask_user: string;
|
|
28
32
|
book: {
|
|
29
|
-
slug:
|
|
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
|
-
//
|
|
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
|
|
9
|
-
let
|
|
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
|
-
|
|
25
|
+
TWO-STEP FLOW:
|
|
19
26
|
|
|
20
|
-
Step 1:
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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,
|
|
56
|
-
|
|
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
|
-
//
|
|
66
|
+
// CONFIRM STEP — user typed back the code
|
|
70
67
|
// ========================================================================
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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: `
|
|
78
|
+
message: `Wrong code. Expected: ${pending.confirmCode}. Got: ${confirm_code}. Try again.`,
|
|
85
79
|
};
|
|
86
80
|
}
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
//
|
|
85
|
+
// PREVIEW STEP — fetch book details, show confirmation
|
|
107
86
|
// ========================================================================
|
|
108
|
-
|
|
109
|
-
|
|
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:
|
|
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
|
-
|
|
158
|
-
|
|
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
|
+
}
|