idiolect-mcp 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -9
- package/package.json +3 -3
- package/progressive-catalog.json +6595 -0
- package/server.mjs +1351 -130
package/server.mjs
CHANGED
|
@@ -24,13 +24,54 @@
|
|
|
24
24
|
// }
|
|
25
25
|
// }
|
|
26
26
|
|
|
27
|
-
import { readFileSync } from "node:fs";
|
|
27
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
28
29
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
29
30
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
30
31
|
import {
|
|
31
32
|
CallToolRequestSchema,
|
|
32
33
|
ListToolsRequestSchema,
|
|
33
34
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
35
|
+
import {
|
|
36
|
+
publicMcpState,
|
|
37
|
+
} from "../lib/mcp/public-contract.js";
|
|
38
|
+
|
|
39
|
+
const progressiveCatalog = JSON.parse(
|
|
40
|
+
readFileSync(new URL("./progressive-catalog.json", import.meta.url), "utf8"),
|
|
41
|
+
);
|
|
42
|
+
const PROGRESSIVE_ACCOUNT_TOOL_NAMES = new Set(
|
|
43
|
+
progressiveCatalog.tools.map((tool) => tool.name),
|
|
44
|
+
);
|
|
45
|
+
/** Cached tool names stay callable through the remote compatibility adapter. */
|
|
46
|
+
export const accountRuntimeCompatibilityToolNames = Object.freeze([
|
|
47
|
+
"feedback",
|
|
48
|
+
"refine",
|
|
49
|
+
"profile",
|
|
50
|
+
"manage_profile",
|
|
51
|
+
"learn_writing_style",
|
|
52
|
+
"get_my_voice",
|
|
53
|
+
"create_my_voice",
|
|
54
|
+
"draft_in_voice",
|
|
55
|
+
"rewrite_in_voice",
|
|
56
|
+
"score_voice",
|
|
57
|
+
"set_up_my_voice",
|
|
58
|
+
"record_voice_feedback",
|
|
59
|
+
"write_as_me",
|
|
60
|
+
"rewrite_as_me",
|
|
61
|
+
"list_writing_profiles",
|
|
62
|
+
"list_writing_profile_samples",
|
|
63
|
+
"add_samples_to_writing_profile",
|
|
64
|
+
"replace_samples_in_writing_profile",
|
|
65
|
+
"remove_sample_from_writing_profile",
|
|
66
|
+
"apply_writing_template",
|
|
67
|
+
"clear_default_writing_template",
|
|
68
|
+
"add_writing_preference",
|
|
69
|
+
"remove_writing_preference",
|
|
70
|
+
"remove_learned_writing_preference",
|
|
71
|
+
"list_style_contributions",
|
|
72
|
+
"remove_style_contribution",
|
|
73
|
+
]);
|
|
74
|
+
const REMOTE_COMPATIBILITY_TOOL_NAMES = new Set(accountRuntimeCompatibilityToolNames);
|
|
34
75
|
|
|
35
76
|
const BASE = process.env.IDIOLECT_BASE || "https://idiolect.app";
|
|
36
77
|
const ACCOUNT_BASE = BASE.replace(/\/+$/, "");
|
|
@@ -75,27 +116,247 @@ async function scoreVoice(candidates, region) {
|
|
|
75
116
|
return res.json();
|
|
76
117
|
}
|
|
77
118
|
|
|
78
|
-
|
|
119
|
+
export function formatLegacyScoreVoiceResult(result) {
|
|
120
|
+
const scores = Array.isArray(result?.scores) ? result.scores : [];
|
|
121
|
+
const lines = scores
|
|
122
|
+
.map((score, index) => {
|
|
123
|
+
const text = typeof score?.text === "string" ? score.text : "";
|
|
124
|
+
const excerpt = `${text.slice(0, 60)}${text.length > 60 ? "…" : ""}`;
|
|
125
|
+
const measured = score?.scored !== false && Number.isFinite(score?.voiceMatch);
|
|
126
|
+
const reportable = measured && score?.absoluteScoreReportable !== false;
|
|
127
|
+
const withheld = score?.scored === true && score?.absoluteScoreReportable === false;
|
|
128
|
+
const match = reportable
|
|
129
|
+
? `Voice Match ${score.voiceMatch}/100`
|
|
130
|
+
: withheld
|
|
131
|
+
? `Voice Match not calibrated${score?.reason ? ` — ${score.reason}` : ""}`
|
|
132
|
+
: `Voice Match unavailable${score?.reason ? ` — ${score.reason}` : ""}`;
|
|
133
|
+
return `#${index + 1} · ${match} — ${excerpt}`;
|
|
134
|
+
})
|
|
135
|
+
.join("\n");
|
|
136
|
+
const provenance = result?.metric?.validated === true
|
|
137
|
+
? "Relative Voice Match on the validated instrument"
|
|
138
|
+
: result?.metric?.validated === false
|
|
139
|
+
? "Relative Voice Match from a quick estimate"
|
|
140
|
+
: "Relative Voice Match; metric provenance unavailable";
|
|
141
|
+
|
|
142
|
+
return `${lines}\n\n(${provenance} — higher = reads more like you; not a forensic identity claim.)`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const CREDIT_STATES = new Set([
|
|
146
|
+
"free_beta",
|
|
147
|
+
"balance_available",
|
|
148
|
+
"low_balance",
|
|
149
|
+
"zero_balance",
|
|
150
|
+
"legacy_unlimited",
|
|
151
|
+
"accounting_unavailable",
|
|
152
|
+
]);
|
|
153
|
+
const CREDIT_MODES = new Set(["free", "credits"]);
|
|
154
|
+
|
|
155
|
+
function plainObject(value) {
|
|
156
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function safeManageUrl(value) {
|
|
160
|
+
if (value === null) return null;
|
|
161
|
+
if (typeof value !== "string") return undefined;
|
|
79
162
|
try {
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
163
|
+
const parsed = new URL(value);
|
|
164
|
+
const accountOrigin = new URL(ACCOUNT_BASE).origin;
|
|
165
|
+
const firstParty = parsed.hostname === "idiolect.app" || parsed.hostname.endsWith(".idiolect.app");
|
|
166
|
+
if (
|
|
167
|
+
!["https:", "http:"].includes(parsed.protocol)
|
|
168
|
+
|| (parsed.origin !== accountOrigin && !firstParty)
|
|
169
|
+
|| parsed.pathname !== "/settings"
|
|
170
|
+
|| parsed.username
|
|
171
|
+
|| parsed.password
|
|
172
|
+
|| parsed.search !== "?section=credits"
|
|
173
|
+
|| parsed.hash
|
|
174
|
+
) return undefined;
|
|
175
|
+
return `${parsed.origin}/settings?section=credits`;
|
|
85
176
|
} catch {
|
|
86
|
-
return
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function creditMessage(credits) {
|
|
182
|
+
const { state, balance, charged, manageUrl, mode } = credits;
|
|
183
|
+
if (state === "free_beta") {
|
|
184
|
+
return charged === 0
|
|
185
|
+
? "Free during beta. No credits are being used."
|
|
186
|
+
: "Completed free during beta. No credit was used.";
|
|
187
|
+
}
|
|
188
|
+
if (state === "legacy_unlimited") return "Unlimited generation access is active. Credits are not used.";
|
|
189
|
+
if (state === "accounting_unavailable") {
|
|
190
|
+
return mode === "free"
|
|
191
|
+
? "Free during beta. The credit balance is temporarily unavailable; generation remains available."
|
|
192
|
+
: "The credit balance is temporarily unavailable. Generation is paused; no credit was used.";
|
|
193
|
+
}
|
|
194
|
+
if (state === "zero_balance") {
|
|
195
|
+
return charged === 1
|
|
196
|
+
? "This request completed with the last credit."
|
|
197
|
+
: `The credit balance is 0. A draft or rewrite needs 1 credit. Credit management: ${manageUrl}.`;
|
|
198
|
+
}
|
|
199
|
+
if (charged === 1) return `1 credit used. ${balance} ${balance === 1 ? "remains" : "remain"}.`;
|
|
200
|
+
if (state === "low_balance") return `${balance} credit${balance === 1 ? "" : "s"} remain. Credit management: ${manageUrl}.`;
|
|
201
|
+
return `${balance} credits available. Each draft or rewrite uses 1 credit.`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function validCreditInvariant(credits) {
|
|
205
|
+
const {
|
|
206
|
+
state,
|
|
207
|
+
mode,
|
|
208
|
+
balance,
|
|
209
|
+
balancePreview,
|
|
210
|
+
unitCost,
|
|
211
|
+
charged,
|
|
212
|
+
unlimited,
|
|
213
|
+
accountingAvailable,
|
|
214
|
+
generationAvailable,
|
|
215
|
+
purchaseRequired,
|
|
216
|
+
manageUrl,
|
|
217
|
+
} = credits;
|
|
218
|
+
if (state === "free_beta") {
|
|
219
|
+
return mode === "free" && balance !== null && balancePreview && unitCost === 0 && charged === 0
|
|
220
|
+
&& !unlimited && accountingAvailable && generationAvailable && !purchaseRequired && manageUrl === null;
|
|
221
|
+
}
|
|
222
|
+
if (state === "legacy_unlimited") {
|
|
223
|
+
return mode === "credits" && balance === null && !balancePreview && unitCost === 0 && charged === 0
|
|
224
|
+
&& unlimited && accountingAvailable && generationAvailable && !purchaseRequired && manageUrl === null;
|
|
225
|
+
}
|
|
226
|
+
if (state === "accounting_unavailable") {
|
|
227
|
+
return balance === null && !balancePreview && charged === 0 && !unlimited && !accountingAvailable
|
|
228
|
+
&& !purchaseRequired && manageUrl === null && unitCost === (mode === "free" ? 0 : 1)
|
|
229
|
+
&& generationAvailable === (mode === "free");
|
|
230
|
+
}
|
|
231
|
+
if (mode !== "credits" || balance === null || balancePreview || unitCost !== 1 || unlimited || !accountingAvailable) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (state === "balance_available") {
|
|
235
|
+
return balance > 5 && generationAvailable && !purchaseRequired && manageUrl === null;
|
|
236
|
+
}
|
|
237
|
+
if (state === "low_balance") {
|
|
238
|
+
return balance >= 1 && balance <= 5 && generationAvailable && !purchaseRequired && manageUrl !== null;
|
|
87
239
|
}
|
|
240
|
+
return state === "zero_balance" && balance === 0 && !generationAvailable && purchaseRequired && manageUrl !== null;
|
|
88
241
|
}
|
|
89
242
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Rebuild the public credit envelope from known primitives only. Never copy an
|
|
245
|
+
* upstream message or unknown field into an MCP response.
|
|
246
|
+
*/
|
|
247
|
+
export function safeCreditPresentation(value) {
|
|
248
|
+
if (!plainObject(value)) return null;
|
|
249
|
+
const balance = value.balance;
|
|
250
|
+
const manageUrl = safeManageUrl(value.manageUrl);
|
|
251
|
+
if (
|
|
252
|
+
!CREDIT_STATES.has(value.state)
|
|
253
|
+
|| !CREDIT_MODES.has(value.mode)
|
|
254
|
+
|| !(balance === null || (Number.isSafeInteger(balance) && balance >= 0))
|
|
255
|
+
|| typeof value.balancePreview !== "boolean"
|
|
256
|
+
|| ![0, 1].includes(value.unitCost)
|
|
257
|
+
|| ![0, 1].includes(value.charged)
|
|
258
|
+
|| typeof value.unlimited !== "boolean"
|
|
259
|
+
|| typeof value.accountingAvailable !== "boolean"
|
|
260
|
+
|| typeof value.generationAvailable !== "boolean"
|
|
261
|
+
|| typeof value.purchaseRequired !== "boolean"
|
|
262
|
+
|| manageUrl === undefined
|
|
263
|
+
|| typeof value.replayed !== "boolean"
|
|
264
|
+
) return null;
|
|
265
|
+
|
|
266
|
+
const credits = {
|
|
267
|
+
state: value.state,
|
|
268
|
+
mode: value.mode,
|
|
269
|
+
balance,
|
|
270
|
+
balancePreview: value.balancePreview,
|
|
271
|
+
unitCost: value.unitCost,
|
|
272
|
+
charged: value.charged,
|
|
273
|
+
unlimited: value.unlimited,
|
|
274
|
+
accountingAvailable: value.accountingAvailable,
|
|
275
|
+
generationAvailable: value.generationAvailable,
|
|
276
|
+
purchaseRequired: value.purchaseRequired,
|
|
277
|
+
manageUrl,
|
|
278
|
+
replayed: value.replayed,
|
|
279
|
+
};
|
|
280
|
+
if (!validCreditInvariant(credits)) return null;
|
|
281
|
+
return { ...credits, message: creditMessage(credits) };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function safeCreditToolErrorPayload(value) {
|
|
285
|
+
if (!plainObject(value) || !plainObject(value.error) || !plainObject(value.retry)) return null;
|
|
286
|
+
const credits = safeCreditPresentation(value.credits);
|
|
287
|
+
if (!credits) return null;
|
|
288
|
+
const purchaseRequired = value.status === "purchase_required"
|
|
289
|
+
&& value.error.code === "insufficient_credits"
|
|
290
|
+
&& credits.state === "zero_balance"
|
|
291
|
+
&& credits.mode === "credits"
|
|
292
|
+
&& credits.balance === 0
|
|
293
|
+
&& credits.charged === 0
|
|
294
|
+
&& value.retry.after === "balance_updated";
|
|
295
|
+
const accountingUnavailable = value.status === "accounting_unavailable"
|
|
296
|
+
&& value.error.code === "credit_accounting_unavailable"
|
|
297
|
+
&& credits.state === "accounting_unavailable"
|
|
298
|
+
&& credits.mode === "credits"
|
|
299
|
+
&& credits.charged === 0
|
|
300
|
+
&& value.retry.after === "accounting_available";
|
|
301
|
+
if (
|
|
302
|
+
(!purchaseRequired && !accountingUnavailable)
|
|
303
|
+
|| value.error.retryable !== true
|
|
304
|
+
|| value.retry.safe !== true
|
|
305
|
+
|| value.retry.idempotent !== true
|
|
306
|
+
|| value.retry.scope !== "pre_generation"
|
|
307
|
+
) return null;
|
|
308
|
+
|
|
309
|
+
const message = purchaseRequired
|
|
310
|
+
? `This request needs 1 credit; the balance is 0. Nothing was charged. Credit management: ${credits.manageUrl}. The same request remains safe to retry after the balance changes.`
|
|
311
|
+
: "The credit balance is temporarily unavailable. No generation started and no credit was used. The same request remains safe to retry after accounting recovers.";
|
|
312
|
+
return {
|
|
313
|
+
status: value.status,
|
|
314
|
+
message,
|
|
315
|
+
error: { code: value.error.code, retryable: true },
|
|
316
|
+
credits: { ...credits, message },
|
|
317
|
+
retry: {
|
|
318
|
+
safe: true,
|
|
319
|
+
idempotent: true,
|
|
320
|
+
scope: "pre_generation",
|
|
321
|
+
after: value.retry.after,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function accountRequestError(res, toolName) {
|
|
327
|
+
if ((res.headers.get("content-type") || "").includes("application/json")) {
|
|
328
|
+
try {
|
|
329
|
+
const payload = await res.json();
|
|
330
|
+
const creditContent = safeCreditToolErrorPayload(payload);
|
|
331
|
+
if (creditContent) {
|
|
332
|
+
return new AccountRequestError(creditContent.message, res.status, creditContent);
|
|
333
|
+
}
|
|
334
|
+
const creationContent = toolName === "create_my_voice" ? safeCreationFailurePayload(payload) : null;
|
|
335
|
+
if (creationContent) {
|
|
336
|
+
return new AccountRequestError(creationContent.message, res.status, creationContent);
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// The bounded status fallback below deliberately ignores malformed bodies.
|
|
340
|
+
}
|
|
93
341
|
}
|
|
94
|
-
|
|
95
|
-
|
|
342
|
+
|
|
343
|
+
const message = res.status === 401
|
|
344
|
+
? "Idiolect AI API key is invalid or expired (401). Check IDIOLECT_API_KEY."
|
|
345
|
+
: res.status === 402
|
|
346
|
+
? `${toolName}: generation access is unavailable for this Idiolect AI account (402).`
|
|
347
|
+
: res.status === 429
|
|
348
|
+
? `${toolName}: Idiolect AI is temporarily busy (429). Try again shortly.`
|
|
349
|
+
: `${toolName}: Idiolect AI account API failed (${res.status}).`;
|
|
350
|
+
return new AccountRequestError(message, res.status);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
class AccountRequestError extends Error {
|
|
354
|
+
constructor(message, status, structuredContent = null) {
|
|
355
|
+
super(message);
|
|
356
|
+
this.name = "AccountRequestError";
|
|
357
|
+
this.status = status;
|
|
358
|
+
this.structuredContent = structuredContent;
|
|
96
359
|
}
|
|
97
|
-
const body = await readErrorBody(res);
|
|
98
|
-
return `${toolName}: Idiolect AI account API failed (${res.status} ${res.statusText})${body ? `: ${body}` : ""}`;
|
|
99
360
|
}
|
|
100
361
|
|
|
101
362
|
async function accountRequest(path, { method = "GET", body, toolName }) {
|
|
@@ -107,10 +368,26 @@ async function accountRequest(path, { method = "GET", body, toolName }) {
|
|
|
107
368
|
},
|
|
108
369
|
body: body ? JSON.stringify(body) : undefined,
|
|
109
370
|
});
|
|
110
|
-
if (!res.ok) throw
|
|
371
|
+
if (!res.ok) throw await accountRequestError(res, toolName);
|
|
111
372
|
return res.json();
|
|
112
373
|
}
|
|
113
374
|
|
|
375
|
+
async function accountMcpRequest(name, args) {
|
|
376
|
+
const res = await fetch(`${ACCOUNT_BASE}/api/mcp`, {
|
|
377
|
+
method: "POST",
|
|
378
|
+
headers: {
|
|
379
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
380
|
+
accept: "application/json, text/event-stream",
|
|
381
|
+
"content-type": "application/json",
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }),
|
|
384
|
+
});
|
|
385
|
+
if (!res.ok) throw await accountRequestError(res, name);
|
|
386
|
+
const body = await res.json();
|
|
387
|
+
if (body.error || !body.result) throw new Error(`${name}: Idiolect AI returned an invalid MCP response.`);
|
|
388
|
+
return body.result;
|
|
389
|
+
}
|
|
390
|
+
|
|
114
391
|
function formatValue(value) {
|
|
115
392
|
if (value === undefined || value === null) return "";
|
|
116
393
|
if (typeof value === "string") return value;
|
|
@@ -118,17 +395,18 @@ function formatValue(value) {
|
|
|
118
395
|
return JSON.stringify(value, null, 2);
|
|
119
396
|
}
|
|
120
397
|
|
|
121
|
-
function
|
|
122
|
-
if (!Array.isArray(alternatives) || alternatives.length === 0) return "";
|
|
123
|
-
return `\n\nAlternatives:\n${alternatives
|
|
124
|
-
.map((alternative, i) => `${i + 1}. ${formatValue(alternative)}`)
|
|
125
|
-
.join("\n")}`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function scoreSummary(r) {
|
|
398
|
+
export function scoreSummary(r) {
|
|
129
399
|
const lines = [];
|
|
130
400
|
if (r.voiceMatch !== undefined) {
|
|
131
|
-
|
|
401
|
+
const withheld = r.measured === true
|
|
402
|
+
&& r.absoluteScoreReportable === false;
|
|
403
|
+
lines.push(
|
|
404
|
+
r.voiceMatch !== null && !withheld
|
|
405
|
+
? `Voice Match: ${formatValue(r.voiceMatch)}`
|
|
406
|
+
: withheld
|
|
407
|
+
? `Voice Match: not calibrated — ${r.reason || "insufficient evidence"}.`
|
|
408
|
+
: "Voice Match: unavailable",
|
|
409
|
+
);
|
|
132
410
|
}
|
|
133
411
|
if (r.delta !== undefined) lines.push(r.delta === null ? "Delta: unavailable" : `Delta: ${formatValue(r.delta)}`);
|
|
134
412
|
if (r.candidateDelta !== undefined && r.candidateDelta !== r.delta) {
|
|
@@ -136,6 +414,10 @@ function scoreSummary(r) {
|
|
|
136
414
|
}
|
|
137
415
|
if (r.validated !== undefined) lines.push(`Validated: ${formatValue(r.validated)}`);
|
|
138
416
|
if (r.measured !== undefined) lines.push(`Measured: ${formatValue(r.measured)}`);
|
|
417
|
+
if (r.scoreConfidence) lines.push(`Score confidence: ${formatValue(r.scoreConfidence)}`);
|
|
418
|
+
if (r.absoluteScoreReportable !== undefined) {
|
|
419
|
+
lines.push(`Absolute score reportable: ${formatValue(r.absoluteScoreReportable)}`);
|
|
420
|
+
}
|
|
139
421
|
if (r.reason) lines.push(`Reason: ${formatValue(r.reason)}`);
|
|
140
422
|
if (r.instrument) lines.push(`Instrument: ${formatValue(r.instrument)}`);
|
|
141
423
|
if (r.register) lines.push(`Calibration register: ${formatValue(r.register)}`);
|
|
@@ -145,14 +427,110 @@ function scoreSummary(r) {
|
|
|
145
427
|
|
|
146
428
|
// Mirrors REWRITE_REGISTERS in lib/connectors.ts. This file ships as a standalone npm
|
|
147
429
|
// package with no build step, so it cannot import from the app — keep the two in sync.
|
|
148
|
-
const REWRITE_REGISTERS = ["email", "slack", "essay"];
|
|
430
|
+
const REWRITE_REGISTERS = ["email", "slack", "social", "essay"];
|
|
149
431
|
const REGISTER_SET = new Set(REWRITE_REGISTERS);
|
|
432
|
+
const CHANGE_SCOPES = ["light_edit", "rewrite", "transform"];
|
|
433
|
+
const CHANGE_SCOPE_SET = new Set([...CHANGE_SCOPES, "polish"]);
|
|
434
|
+
const EFFECT_KINDS = ["register", "tone", "humour", "candour", "energy", "voice", "other"];
|
|
435
|
+
const EFFECT_KIND_SET = new Set(EFFECT_KINDS);
|
|
436
|
+
const EFFECT_STRENGTHS = ["light", "full"];
|
|
437
|
+
const EFFECT_STRENGTH_SET = new Set(EFFECT_STRENGTHS);
|
|
438
|
+
const EFFECT_PROPERTY_SET = new Set(["kind", "ask", "strength"]);
|
|
439
|
+
// Who wrote the source or brief; mirrors lib/generation/change-request.ts's SOURCE_AUTHORS.
|
|
440
|
+
const SOURCE_AUTHORS = ["user", "host", "other", "unknown"];
|
|
441
|
+
const SOURCE_AUTHOR_SET = new Set(SOURCE_AUTHORS);
|
|
442
|
+
|
|
443
|
+
function backtickedValues(values) {
|
|
444
|
+
return values.map((value) => `\`${value}\``).join(", ");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const CHANGE_SCOPE_SCHEMA = {
|
|
448
|
+
type: "string",
|
|
449
|
+
enum: CHANGE_SCOPES,
|
|
450
|
+
description: "`changeScope` carries how much the person wants changed, from their own words.",
|
|
451
|
+
};
|
|
452
|
+
const EFFECTS_SCHEMA = {
|
|
453
|
+
type: "array",
|
|
454
|
+
maxItems: 20,
|
|
455
|
+
items: {
|
|
456
|
+
type: "object",
|
|
457
|
+
properties: {
|
|
458
|
+
kind: {
|
|
459
|
+
type: "string",
|
|
460
|
+
enum: EFFECT_KINDS,
|
|
461
|
+
description: "`kind` names the tone, register or effect the requester identified.",
|
|
462
|
+
},
|
|
463
|
+
ask: {
|
|
464
|
+
type: "string",
|
|
465
|
+
minLength: 1,
|
|
466
|
+
maxLength: 2000,
|
|
467
|
+
description: "`ask` preserves the requester's own words for this effect.",
|
|
468
|
+
},
|
|
469
|
+
strength: {
|
|
470
|
+
type: "string",
|
|
471
|
+
enum: EFFECT_STRENGTHS,
|
|
472
|
+
description: "`strength` records whether the requester asked for the effect to be `light` or `full`.",
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
required: ["kind", "ask"],
|
|
476
|
+
additionalProperties: false,
|
|
477
|
+
},
|
|
478
|
+
description: "`effects` carries each named tone, register or effect.",
|
|
479
|
+
};
|
|
480
|
+
const SOURCE_AUTHOR_SCHEMA = {
|
|
481
|
+
type: "string",
|
|
482
|
+
enum: SOURCE_AUTHORS,
|
|
483
|
+
description:
|
|
484
|
+
"Who wrote this text. `user` only when the person wrote it themselves; `host` when you (the assistant) drafted it; `other` for a third party's text. Leave it out if unsure.",
|
|
485
|
+
};
|
|
486
|
+
const BRIEF_AUTHOR_SCHEMA = {
|
|
487
|
+
type: "string",
|
|
488
|
+
enum: SOURCE_AUTHORS,
|
|
489
|
+
description:
|
|
490
|
+
"Who wrote the brief. `user` only when the person wrote it themselves; `host` when you (the assistant) drafted it; `other` for a third party's text. Leave it out if unsure.",
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/** Validates `sourceAuthor`/`briefAuthor` and defaults an absent one to `"unknown"` on the
|
|
494
|
+
* returned copy of `args`. Returns a tool error, never throws. */
|
|
495
|
+
export function authorProvenanceError(args, toolName, key) {
|
|
496
|
+
if (args[key] === undefined) return null;
|
|
497
|
+
if (typeof args[key] !== "string" || !SOURCE_AUTHOR_SET.has(args[key])) {
|
|
498
|
+
return toolError(`${toolName}: \`${key}\` must be one of ${backtickedValues(SOURCE_AUTHORS)}.`);
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function changeRequestError(args, toolName) {
|
|
504
|
+
if (args.changeScope !== undefined
|
|
505
|
+
&& (typeof args.changeScope !== "string" || !CHANGE_SCOPE_SET.has(args.changeScope))) {
|
|
506
|
+
return toolError(`${toolName}: \`changeScope\` must be one of ${backtickedValues(CHANGE_SCOPES)}.`);
|
|
507
|
+
}
|
|
508
|
+
if (args.effects === undefined) return null;
|
|
509
|
+
if (!Array.isArray(args.effects)) return toolError(`${toolName}: \`effects\` must be a list.`);
|
|
510
|
+
if (args.effects.length > 20) return toolError(`${toolName}: \`effects\` accepts at most 20 entries.`);
|
|
511
|
+
for (const effect of args.effects) {
|
|
512
|
+
if (!plainObject(effect) || Object.keys(effect).some((key) => !EFFECT_PROPERTY_SET.has(key))) {
|
|
513
|
+
return toolError(`${toolName}: each \`effects\` entry must contain only \`kind\`, \`ask\`, and \`strength\`.`);
|
|
514
|
+
}
|
|
515
|
+
if (typeof effect.kind !== "string" || !EFFECT_KIND_SET.has(effect.kind)) {
|
|
516
|
+
return toolError(`${toolName}: \`effects[].kind\` must be one of ${backtickedValues(EFFECT_KINDS)}.`);
|
|
517
|
+
}
|
|
518
|
+
if (typeof effect.ask !== "string" || effect.ask.length < 1 || effect.ask.length > 2000) {
|
|
519
|
+
return toolError(`${toolName}: \`effects[].ask\` must be 1 to 2,000 characters.`);
|
|
520
|
+
}
|
|
521
|
+
if (effect.strength !== undefined
|
|
522
|
+
&& (typeof effect.strength !== "string" || !EFFECT_STRENGTH_SET.has(effect.strength))) {
|
|
523
|
+
return toolError(`${toolName}: \`effects[].strength\` must be one of ${backtickedValues(EFFECT_STRENGTHS)}.`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
150
528
|
|
|
151
529
|
const legacyTools = [
|
|
152
530
|
{
|
|
153
531
|
name: "get_my_voice",
|
|
154
532
|
description:
|
|
155
|
-
"Load the user's Idiolect AI Voice Card
|
|
533
|
+
"Load the user's Idiolect AI Voice Card when they ask for personal outward-facing prose, such as an email, message, post, or reply, to match their established voice. Do not use it for ordinary factual writing or repository artifacts. Returns a style guide describing how the user actually writes.",
|
|
156
534
|
inputSchema: {
|
|
157
535
|
type: "object",
|
|
158
536
|
properties: {
|
|
@@ -215,14 +593,95 @@ const PROVENANCE_OUTPUT_PROPERTIES = {
|
|
|
215
593
|
validated: { type: "boolean", description: "True only when this result was measured by the validated LUAR instrument." },
|
|
216
594
|
instrument: { type: ["string", "null"] },
|
|
217
595
|
reason: { type: ["string", "null"] },
|
|
218
|
-
register: { type: "string" },
|
|
219
|
-
approximate: { type: "boolean" },
|
|
220
596
|
voice: SELECTED_VOICE_SCHEMA,
|
|
221
597
|
evidence: EVIDENCE_OUTPUT_SCHEMA,
|
|
222
598
|
};
|
|
599
|
+
const CREDIT_OUTPUT_SCHEMA = {
|
|
600
|
+
type: "object",
|
|
601
|
+
additionalProperties: false,
|
|
602
|
+
properties: {
|
|
603
|
+
state: {
|
|
604
|
+
type: "string",
|
|
605
|
+
enum: ["free_beta", "balance_available", "low_balance", "zero_balance", "legacy_unlimited", "accounting_unavailable"],
|
|
606
|
+
},
|
|
607
|
+
mode: { type: "string", enum: ["free", "credits"] },
|
|
608
|
+
balance: { type: ["integer", "null"], minimum: 0 },
|
|
609
|
+
balancePreview: { type: "boolean" },
|
|
610
|
+
unitCost: { type: "number", enum: [0, 1] },
|
|
611
|
+
charged: { type: "number", enum: [0, 1] },
|
|
612
|
+
unlimited: { type: "boolean" },
|
|
613
|
+
accountingAvailable: { type: "boolean" },
|
|
614
|
+
generationAvailable: { type: "boolean" },
|
|
615
|
+
purchaseRequired: { type: "boolean" },
|
|
616
|
+
manageUrl: { type: ["string", "null"] },
|
|
617
|
+
replayed: { type: "boolean" },
|
|
618
|
+
message: { type: "string" },
|
|
619
|
+
},
|
|
620
|
+
required: [
|
|
621
|
+
"state",
|
|
622
|
+
"mode",
|
|
623
|
+
"balance",
|
|
624
|
+
"balancePreview",
|
|
625
|
+
"unitCost",
|
|
626
|
+
"charged",
|
|
627
|
+
"unlimited",
|
|
628
|
+
"accountingAvailable",
|
|
629
|
+
"generationAvailable",
|
|
630
|
+
"purchaseRequired",
|
|
631
|
+
"manageUrl",
|
|
632
|
+
"replayed",
|
|
633
|
+
"message",
|
|
634
|
+
],
|
|
635
|
+
};
|
|
636
|
+
const CREDIT_ERROR_SCHEMA = {
|
|
637
|
+
type: "object",
|
|
638
|
+
additionalProperties: false,
|
|
639
|
+
properties: {
|
|
640
|
+
code: { type: "string", enum: ["insufficient_credits", "credit_accounting_unavailable"] },
|
|
641
|
+
retryable: { type: "boolean", enum: [true] },
|
|
642
|
+
},
|
|
643
|
+
required: ["code", "retryable"],
|
|
644
|
+
};
|
|
645
|
+
const CREDIT_RETRY_SCHEMA = {
|
|
646
|
+
type: "object",
|
|
647
|
+
additionalProperties: false,
|
|
648
|
+
properties: {
|
|
649
|
+
safe: { type: "boolean", enum: [true] },
|
|
650
|
+
idempotent: { type: "boolean", enum: [true] },
|
|
651
|
+
scope: { type: "string", enum: ["pre_generation"] },
|
|
652
|
+
after: { type: "string", enum: ["balance_updated", "accounting_available"] },
|
|
653
|
+
},
|
|
654
|
+
required: ["safe", "idempotent", "scope", "after"],
|
|
655
|
+
};
|
|
656
|
+
// Carried by a credit-gate refusal only; a completed draft or rewrite no longer reports
|
|
657
|
+
// credits (13 constants in free mode) or a status beside `state` (IDI-381).
|
|
658
|
+
const CREDIT_GENERATION_OUTPUT_PROPERTIES = {
|
|
659
|
+
status: { type: "string", enum: ["purchase_required", "accounting_unavailable"] },
|
|
660
|
+
message: { type: "string" },
|
|
661
|
+
credits: CREDIT_OUTPUT_SCHEMA,
|
|
662
|
+
error: CREDIT_ERROR_SCHEMA,
|
|
663
|
+
retry: CREDIT_RETRY_SCHEMA,
|
|
664
|
+
};
|
|
665
|
+
const NEXT_ACTION_SCHEMA = {
|
|
666
|
+
type: "object",
|
|
667
|
+
additionalProperties: false,
|
|
668
|
+
properties: {
|
|
669
|
+
action: { type: "string", enum: ["collect_evidence", "reload_voice", "continue_writing", "ask_feedback", "retry", "done"] },
|
|
670
|
+
tool: { type: ["string", "null"], enum: ["create_my_voice", "get_my_voice", "record_voice_feedback", null] },
|
|
671
|
+
},
|
|
672
|
+
required: ["action", "tool"],
|
|
673
|
+
};
|
|
674
|
+
const PUBLIC_STATE_PROPERTIES = {
|
|
675
|
+
state: { type: "string", enum: ["ready", "missing", "thin", "stale", "complete", "failed"] },
|
|
676
|
+
message: { type: "string" },
|
|
677
|
+
next: NEXT_ACTION_SCHEMA,
|
|
678
|
+
};
|
|
223
679
|
const VOICE_OUTPUT_SCHEMA = {
|
|
224
680
|
type: "object",
|
|
225
681
|
properties: {
|
|
682
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
683
|
+
status: { type: "string", enum: ["ready", "setup_required"] },
|
|
684
|
+
message: { type: "string" },
|
|
226
685
|
id: { type: "string" },
|
|
227
686
|
name: { type: ["string", "null"] },
|
|
228
687
|
isDefault: { type: "boolean" },
|
|
@@ -235,58 +694,330 @@ const VOICE_OUTPUT_SCHEMA = {
|
|
|
235
694
|
taboos: { type: "array", items: { type: "string" } },
|
|
236
695
|
positions: { type: "array", items: { type: "string" }, description: "Real takes the user has expressed; never invent new ones." },
|
|
237
696
|
evidence: EVIDENCE_OUTPUT_SCHEMA,
|
|
238
|
-
register: { type: "string" },
|
|
239
|
-
approximate: { type: "boolean" },
|
|
240
697
|
},
|
|
241
698
|
};
|
|
242
699
|
const SCORE_OUTPUT_SCHEMA = {
|
|
243
700
|
type: "object",
|
|
244
701
|
properties: {
|
|
702
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
245
703
|
voiceMatch: { type: ["number", "null"], description: "0–100; higher = closer to the user's own writing. Relative, not a forensic identity claim." },
|
|
246
704
|
measured: { type: "boolean", description: "False when the candidate or corpus is in the unscored band, or the scorer is unavailable." },
|
|
247
705
|
...PROVENANCE_OUTPUT_PROPERTIES,
|
|
706
|
+
register: { type: "string", description: "Calibration register the anchors come from; the absolute is approximate outside it." },
|
|
707
|
+
approximate: { type: "boolean" },
|
|
248
708
|
},
|
|
249
709
|
};
|
|
710
|
+
// A draft or rewrite is the writing, the profile that wrote it and its evidence state.
|
|
711
|
+
// The score cluster (voiceMatch, measured, deltas, candidate fields, crossFit, the
|
|
712
|
+
// calibration constants) was constant on every call and is gone from the account API
|
|
713
|
+
// and from here (IDI-381). Voice Match is measured on score_voice.
|
|
714
|
+
// IDI-522 step 2: present only for a soft language-policy pass through the profile-less
|
|
715
|
+
// shared engine — mirrors app/api/mcp/schemas.ts's `caveatsOutputShape` so this stdio
|
|
716
|
+
// schema stays in step with every other writing-tool surface.
|
|
717
|
+
const CAVEATS_OUTPUT_PROPERTY = { caveats: { type: "array", items: { type: "string", enum: ["language"] } } };
|
|
250
718
|
const REWRITE_OUTPUT_SCHEMA = {
|
|
251
719
|
type: "object",
|
|
252
720
|
properties: {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
candidateDelta: { type: ["number", "null"] },
|
|
261
|
-
improved: { type: ["boolean", "null"] },
|
|
262
|
-
regressed: { type: ["boolean", "null"] },
|
|
263
|
-
...PROVENANCE_OUTPUT_PROPERTIES,
|
|
721
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
722
|
+
rewritten: { type: "string", description: "The rewritten text." },
|
|
723
|
+
reason: { type: ["string", "null"], description: "Present only when scoring is enabled and the engine did not measure; says why." },
|
|
724
|
+
voice: SELECTED_VOICE_SCHEMA,
|
|
725
|
+
evidence: EVIDENCE_OUTPUT_SCHEMA,
|
|
726
|
+
...CREDIT_GENERATION_OUTPUT_PROPERTIES,
|
|
727
|
+
...CAVEATS_OUTPUT_PROPERTY,
|
|
264
728
|
},
|
|
265
729
|
};
|
|
266
730
|
const DRAFT_OUTPUT_SCHEMA = {
|
|
267
731
|
type: "object",
|
|
268
732
|
properties: {
|
|
733
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
269
734
|
draft: { type: "string", description: "New prose drafted in the user's voice." },
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
...
|
|
735
|
+
reason: { type: ["string", "null"], description: "Present only when scoring is enabled and the engine did not measure; says why." },
|
|
736
|
+
voice: SELECTED_VOICE_SCHEMA,
|
|
737
|
+
evidence: EVIDENCE_OUTPUT_SCHEMA,
|
|
738
|
+
...CREDIT_GENERATION_OUTPUT_PROPERTIES,
|
|
739
|
+
...CAVEATS_OUTPUT_PROPERTY,
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
const CREATE_VOICE_OUTPUT_SCHEMA = {
|
|
743
|
+
type: "object",
|
|
744
|
+
properties: {
|
|
745
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
746
|
+
status: { type: "string", enum: ["ready", "more_evidence_required", "build_in_progress", "failed"] },
|
|
747
|
+
message: { type: "string" },
|
|
748
|
+
reason: { type: ["string", "null"] },
|
|
749
|
+
voiceId: { type: ["string", "null"] },
|
|
750
|
+
neededChars: { type: "number", description: "Present only when more writing is needed." },
|
|
751
|
+
neededBlocks: { type: "number", description: "Present only when more writing is needed." },
|
|
752
|
+
replay: { type: "boolean" },
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
const FEEDBACK_OUTPUT_SCHEMA = {
|
|
756
|
+
type: "object",
|
|
757
|
+
properties: {
|
|
758
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
759
|
+
needsFinalText: { type: "boolean" },
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
const PROFILE_REF_OPTIONAL_SCHEMA = {
|
|
763
|
+
type: "string", minLength: 1,
|
|
764
|
+
description: "The Writing Profile's id, or its exact name. Omit to use the current profile.",
|
|
765
|
+
};
|
|
766
|
+
const PROFILE_REF_REQUIRED_SCHEMA = {
|
|
767
|
+
type: "string", minLength: 1,
|
|
768
|
+
description: "The Writing Profile's id, or its exact name.",
|
|
769
|
+
};
|
|
770
|
+
const PROFILE_ID_SCHEMA = { type: "string", format: "uuid" };
|
|
771
|
+
const PROFILE_SAMPLE_SCHEMA = {
|
|
772
|
+
type: "array",
|
|
773
|
+
minItems: 1,
|
|
774
|
+
maxItems: 500,
|
|
775
|
+
items: { type: "string", maxLength: 20000 },
|
|
776
|
+
};
|
|
777
|
+
const OPTIONAL_PROFILE_SAMPLE_SCHEMA = {
|
|
778
|
+
type: "array",
|
|
779
|
+
maxItems: 500,
|
|
780
|
+
items: { type: "string", maxLength: 20000 },
|
|
781
|
+
};
|
|
782
|
+
const PROFILE_SAMPLE_STATE_SCHEMA = {
|
|
783
|
+
type: "object",
|
|
784
|
+
properties: {
|
|
785
|
+
profileId: PROFILE_ID_SCHEMA,
|
|
786
|
+
generation: { type: "integer", minimum: 0 },
|
|
787
|
+
samples: {
|
|
788
|
+
type: "array",
|
|
789
|
+
items: {
|
|
790
|
+
type: "object",
|
|
791
|
+
properties: {
|
|
792
|
+
id: PROFILE_ID_SCHEMA,
|
|
793
|
+
text: { type: "string" },
|
|
794
|
+
addedAt: { type: "string" },
|
|
795
|
+
},
|
|
796
|
+
required: ["id", "text", "addedAt"],
|
|
797
|
+
additionalProperties: false,
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
needsWriting: { type: "boolean" },
|
|
801
|
+
heldNotUsable: { type: "integer", minimum: 0 },
|
|
802
|
+
},
|
|
803
|
+
required: ["profileId", "generation", "samples", "needsWriting", "heldNotUsable"],
|
|
804
|
+
additionalProperties: false,
|
|
805
|
+
};
|
|
806
|
+
/** What the caller must change to succeed; present only on evidence failures. */
|
|
807
|
+
const PROFILE_SHORTFALL_SCHEMA = {
|
|
808
|
+
type: "object",
|
|
809
|
+
properties: {
|
|
810
|
+
reason: { type: "string", enum: ["insufficient_input", "unsupported_language"] },
|
|
811
|
+
message: { type: "string" },
|
|
812
|
+
neededChars: { type: "integer", minimum: 0 },
|
|
813
|
+
neededBlocks: { type: "integer", minimum: 0 },
|
|
814
|
+
droppedBlocks: { type: "integer", minimum: 0 },
|
|
815
|
+
minimumChars: { type: "integer", minimum: 0 },
|
|
816
|
+
minimumBlocks: { type: "integer", minimum: 0 },
|
|
274
817
|
},
|
|
818
|
+
required: ["message"],
|
|
819
|
+
additionalProperties: false,
|
|
275
820
|
};
|
|
276
821
|
|
|
277
|
-
|
|
822
|
+
function profileActionOutput(value, codes) {
|
|
823
|
+
return {
|
|
824
|
+
type: "object",
|
|
825
|
+
properties: {
|
|
826
|
+
ok: { type: "boolean" },
|
|
827
|
+
value,
|
|
828
|
+
error: {
|
|
829
|
+
type: "object",
|
|
830
|
+
properties: {
|
|
831
|
+
code: { type: "string", enum: [...codes, "unexpected_error"] },
|
|
832
|
+
detail: PROFILE_SHORTFALL_SCHEMA,
|
|
833
|
+
},
|
|
834
|
+
required: ["code"],
|
|
835
|
+
additionalProperties: false,
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
required: ["ok"],
|
|
839
|
+
additionalProperties: false,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const accountToolDefinitions = [
|
|
278
844
|
{
|
|
279
845
|
name: "get_my_voice",
|
|
280
846
|
description:
|
|
281
|
-
"Use this when
|
|
847
|
+
"Use this when the user asks for personal outward-facing prose, such as an email, message, post, or reply, to match their established voice. Do not use it for ordinary factual writing or repository artifacts. Reports whether setup is complete, and what is still needed when it is not.",
|
|
282
848
|
inputSchema: { type: "object", properties: {} },
|
|
283
849
|
outputSchema: VOICE_OUTPUT_SCHEMA,
|
|
284
850
|
annotations: { readOnlyHint: true, openWorldHint: false, destructiveHint: false, title: "Load Idiolect AI Voice" },
|
|
285
851
|
},
|
|
852
|
+
{
|
|
853
|
+
name: "create_my_voice",
|
|
854
|
+
description:
|
|
855
|
+
"Compatibility setup action for creating a first Writing Profile, from writing samples the user authored or from short answers about how they write. Use create_writing_profile for explicit multi-profile creation.",
|
|
856
|
+
inputSchema: {
|
|
857
|
+
type: "object",
|
|
858
|
+
properties: {
|
|
859
|
+
profileId: {
|
|
860
|
+
...PROFILE_ID_SCHEMA,
|
|
861
|
+
description:
|
|
862
|
+
"Exact owned Writing Profile to receive this evidence. Omit only when creating the account's first profile or adding to the current profile.",
|
|
863
|
+
},
|
|
864
|
+
samples: {
|
|
865
|
+
type: "array",
|
|
866
|
+
maxItems: 500,
|
|
867
|
+
items: { type: "string", maxLength: 20000 },
|
|
868
|
+
description: "Distinct blocks of writing supplied or explicitly approved by the user.",
|
|
869
|
+
},
|
|
870
|
+
answers: {
|
|
871
|
+
type: "array",
|
|
872
|
+
maxItems: 20,
|
|
873
|
+
items: {
|
|
874
|
+
type: "object",
|
|
875
|
+
properties: {
|
|
876
|
+
question: { type: "string", maxLength: 500 },
|
|
877
|
+
answer: { type: "string", maxLength: 5000 },
|
|
878
|
+
},
|
|
879
|
+
required: ["question", "answer"],
|
|
880
|
+
additionalProperties: false,
|
|
881
|
+
},
|
|
882
|
+
description: "Optional guided questions and the user's own answers.",
|
|
883
|
+
},
|
|
884
|
+
name: { type: "string", maxLength: 60, description: "Optional private name for the Writing Profile." },
|
|
885
|
+
},
|
|
886
|
+
additionalProperties: false,
|
|
887
|
+
},
|
|
888
|
+
outputSchema: CREATE_VOICE_OUTPUT_SCHEMA,
|
|
889
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Create My Writing Profile" },
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
name: "list_writing_profiles",
|
|
893
|
+
description: "List the authenticated user's Writing Profiles and identify the current profile.",
|
|
894
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
895
|
+
outputSchema: profileActionOutput({ type: "array" }, []),
|
|
896
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "List Writing Profiles" },
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: "list_writing_profile_samples",
|
|
900
|
+
description: "List the writing samples and stable sample IDs held by the exact Writing Profile identified by profileId. This does not change the profile.",
|
|
901
|
+
inputSchema: {
|
|
902
|
+
type: "object",
|
|
903
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA },
|
|
904
|
+
required: ["profileId"],
|
|
905
|
+
additionalProperties: false,
|
|
906
|
+
},
|
|
907
|
+
outputSchema: profileActionOutput(PROFILE_SAMPLE_STATE_SCHEMA, ["profile_not_found"]),
|
|
908
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "List Writing Profile Samples" },
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
name: "create_writing_profile",
|
|
912
|
+
description: "Create a new Writing Profile from approved writing. This never changes an existing profile.",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
type: "object",
|
|
915
|
+
properties: {
|
|
916
|
+
name: { type: "string", maxLength: 60 },
|
|
917
|
+
samples: OPTIONAL_PROFILE_SAMPLE_SCHEMA,
|
|
918
|
+
answers: {
|
|
919
|
+
type: "array",
|
|
920
|
+
maxItems: 20,
|
|
921
|
+
items: {
|
|
922
|
+
type: "object",
|
|
923
|
+
properties: {
|
|
924
|
+
question: { type: "string", maxLength: 500 },
|
|
925
|
+
answer: { type: "string", maxLength: 5000 },
|
|
926
|
+
},
|
|
927
|
+
required: ["question", "answer"],
|
|
928
|
+
additionalProperties: false,
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
additionalProperties: false,
|
|
933
|
+
},
|
|
934
|
+
outputSchema: profileActionOutput({ type: "object" }, [
|
|
935
|
+
"insufficient_evidence", "rate_limited", "request_conflict", "build_in_progress",
|
|
936
|
+
]),
|
|
937
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "Create Writing Profile" },
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: "add_samples_to_writing_profile",
|
|
941
|
+
description: "Add approved writing samples to the exact Writing Profile identified by profileId.",
|
|
942
|
+
inputSchema: {
|
|
943
|
+
type: "object",
|
|
944
|
+
properties: { profileId: PROFILE_REF_OPTIONAL_SCHEMA, samples: PROFILE_SAMPLE_SCHEMA },
|
|
945
|
+
required: ["samples"],
|
|
946
|
+
additionalProperties: false,
|
|
947
|
+
},
|
|
948
|
+
outputSchema: profileActionOutput({ type: "object" }, [
|
|
949
|
+
"insufficient_evidence", "request_conflict", "build_in_progress", "profile_not_found",
|
|
950
|
+
]),
|
|
951
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "Add Samples to Writing Profile" },
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
name: "replace_samples_in_writing_profile",
|
|
955
|
+
description: "Replace the writing sample set for the exact Writing Profile identified by profileId. Displaced writing is revoked, not deleted. Refuses when the profile holds writing this tool cannot list.",
|
|
956
|
+
inputSchema: {
|
|
957
|
+
type: "object",
|
|
958
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA, samples: OPTIONAL_PROFILE_SAMPLE_SCHEMA },
|
|
959
|
+
required: ["profileId", "samples"],
|
|
960
|
+
additionalProperties: false,
|
|
961
|
+
},
|
|
962
|
+
outputSchema: profileActionOutput(PROFILE_SAMPLE_STATE_SCHEMA, ["profile_not_found"]),
|
|
963
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true, title: "Replace Samples in Writing Profile" },
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: "remove_sample_from_writing_profile",
|
|
967
|
+
description: "Remove the exact writing sample identified by sampleId from the Writing Profile identified by profileId. The sample stops being used; it is revoked, not erased.",
|
|
968
|
+
inputSchema: {
|
|
969
|
+
type: "object",
|
|
970
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA, sampleId: PROFILE_ID_SCHEMA },
|
|
971
|
+
required: ["profileId", "sampleId"],
|
|
972
|
+
additionalProperties: false,
|
|
973
|
+
},
|
|
974
|
+
outputSchema: profileActionOutput(PROFILE_SAMPLE_STATE_SCHEMA, ["profile_not_found", "sample_not_found"]),
|
|
975
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true, title: "Remove Sample from Writing Profile" },
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
name: "set_current_writing_profile",
|
|
979
|
+
description: "Make the exact Writing Profile identified by profileId current for future writing.",
|
|
980
|
+
inputSchema: {
|
|
981
|
+
type: "object",
|
|
982
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA },
|
|
983
|
+
required: ["profileId"],
|
|
984
|
+
additionalProperties: false,
|
|
985
|
+
},
|
|
986
|
+
outputSchema: profileActionOutput({ type: "object" }, ["profile_not_found"]),
|
|
987
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "Set Current Writing Profile" },
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "delete_writing_profile",
|
|
991
|
+
description: "Delete the exact Writing Profile identified by profileId. A current profile may require a replacementProfileId.",
|
|
992
|
+
inputSchema: {
|
|
993
|
+
type: "object",
|
|
994
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA, replacementProfileId: PROFILE_REF_REQUIRED_SCHEMA },
|
|
995
|
+
required: ["profileId"],
|
|
996
|
+
additionalProperties: false,
|
|
997
|
+
},
|
|
998
|
+
outputSchema: profileActionOutput({ type: "object" }, [
|
|
999
|
+
"profile_not_found", "replacement_required", "invalid_replacement",
|
|
1000
|
+
]),
|
|
1001
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true, title: "Delete Writing Profile" },
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name: "get_credit_status",
|
|
1005
|
+
description:
|
|
1006
|
+
"Check the account's current generation-credit state. During the free beta, this confirms that no credits are being used.",
|
|
1007
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1008
|
+
outputSchema: CREDIT_OUTPUT_SCHEMA,
|
|
1009
|
+
annotations: {
|
|
1010
|
+
readOnlyHint: true,
|
|
1011
|
+
idempotentHint: true,
|
|
1012
|
+
openWorldHint: false,
|
|
1013
|
+
destructiveHint: false,
|
|
1014
|
+
title: "Check Credit Status",
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
286
1017
|
{
|
|
287
1018
|
name: "score_voice",
|
|
288
1019
|
description:
|
|
289
|
-
"Use this when
|
|
1020
|
+
"Use this only when the user explicitly asks to measure or check how close a piece of text is to their own writing. Returns a relative Voice Match with visible provenance, or an honest reason measurement is unavailable; never a forensic identity claim.",
|
|
290
1021
|
inputSchema: {
|
|
291
1022
|
type: "object",
|
|
292
1023
|
properties: { text: { type: "string", description: "Text to score against the user's voice." } },
|
|
@@ -298,7 +1029,7 @@ const accountTools = [
|
|
|
298
1029
|
{
|
|
299
1030
|
name: "rewrite_in_voice",
|
|
300
1031
|
description:
|
|
301
|
-
"Rewrite supplied prose in the user's voice. Returns an in-voice alternative with
|
|
1032
|
+
"Rewrite supplied prose in the user's voice. Returns an in-voice alternative with measurement details and a before→after delta when measured; absolute Voice Match stays withheld unless independent held-out evidence and the target register support it. Use it only when it preserves the user's meaning and fits their request.",
|
|
302
1033
|
inputSchema: {
|
|
303
1034
|
type: "object",
|
|
304
1035
|
properties: {
|
|
@@ -311,6 +1042,17 @@ const accountTools = [
|
|
|
311
1042
|
description:
|
|
312
1043
|
"Optional target register. Adapts the output format while keeping the user's voice; omitted, the input's own format is kept.",
|
|
313
1044
|
},
|
|
1045
|
+
// The ask, kept out of `text`. /api/v1/rewrite has accepted this all along;
|
|
1046
|
+
// only the tool schema was missing it.
|
|
1047
|
+
instructions: {
|
|
1048
|
+
type: "string",
|
|
1049
|
+
description:
|
|
1050
|
+
"The user's actual change request — length, tone, audience, what to cut or keep, make it sound less AI-written, roughen it up, or make it sound like I am talking. Kept separate from `text`: put the request here, never inside the prose to rewrite. `changeScope` carries magnitude; `instructions` carries the requested change.",
|
|
1051
|
+
},
|
|
1052
|
+
changeScope: CHANGE_SCOPE_SCHEMA,
|
|
1053
|
+
effects: EFFECTS_SCHEMA,
|
|
1054
|
+
sourceAuthor: SOURCE_AUTHOR_SCHEMA,
|
|
1055
|
+
profileId: { type: "string", format: "uuid", description: "Use this owned Writing Profile for this piece only. Select an exact profileId; this does not change the account default." },
|
|
314
1056
|
},
|
|
315
1057
|
required: ["text"],
|
|
316
1058
|
},
|
|
@@ -320,31 +1062,495 @@ const accountTools = [
|
|
|
320
1062
|
{
|
|
321
1063
|
name: "draft_in_voice",
|
|
322
1064
|
description:
|
|
323
|
-
"Draft new prose in the user's voice from a supplied brief
|
|
1065
|
+
"Draft new prose in the user's voice from a supplied `brief`, with the user's direction in the optional `instructions`. Returns a draft with measurement details for review; absolute Voice Match stays withheld unless independent held-out evidence and the target register support it.",
|
|
324
1066
|
inputSchema: {
|
|
325
1067
|
type: "object",
|
|
326
1068
|
properties: {
|
|
327
|
-
brief: {
|
|
1069
|
+
brief: {
|
|
1070
|
+
type: "string",
|
|
1071
|
+
description:
|
|
1072
|
+
"`brief` is the material: what the piece is about and what it must contain — its audience, its goal, and the source facts and specifics it has to carry. Never paste directions into `brief`; how to write it goes in `instructions`.",
|
|
1073
|
+
},
|
|
328
1074
|
format: {
|
|
329
1075
|
type: "string",
|
|
330
1076
|
enum: ["linkedin", "x_post", "x_thread", "newsletter", "generic"],
|
|
331
|
-
description: "Optional output format (default: generic).",
|
|
1077
|
+
description: "Optional output format (default: `generic`).",
|
|
332
1078
|
},
|
|
1079
|
+
// The direction, kept out of `brief`. /api/v1/draft accepts it; this schema is the
|
|
1080
|
+
// stdio package's own copy because it cannot import lib/connectors.ts. The bounds
|
|
1081
|
+
// mirror GENERATION_INPUT_LIMITS.draftInstructions, including the nonempty minimum
|
|
1082
|
+
// the remote catalog advertises — an optional field that accepts "" is a third
|
|
1083
|
+
// state neither side means.
|
|
1084
|
+
instructions: {
|
|
1085
|
+
type: "string",
|
|
1086
|
+
minLength: 1,
|
|
1087
|
+
maxLength: 8000,
|
|
1088
|
+
description:
|
|
1089
|
+
"`instructions` is the user's actual direction: how to write the piece — tone, length, format, structure, and what to leave out. Optional; omit it when the user gave none. Never paste directions into `brief`, which carries the material, and never put material here. A named output shape goes in `format`.",
|
|
1090
|
+
},
|
|
1091
|
+
effects: EFFECTS_SCHEMA,
|
|
1092
|
+
briefAuthor: BRIEF_AUTHOR_SCHEMA,
|
|
1093
|
+
profileId: { type: "string", format: "uuid", description: "Use this owned Writing Profile for this piece only. Select an exact profileId; this does not change the account default." },
|
|
333
1094
|
},
|
|
334
1095
|
required: ["brief"],
|
|
335
1096
|
},
|
|
336
1097
|
outputSchema: DRAFT_OUTPUT_SCHEMA,
|
|
337
1098
|
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Draft In Voice" },
|
|
338
1099
|
},
|
|
1100
|
+
{
|
|
1101
|
+
name: "learn_writing_style",
|
|
1102
|
+
description:
|
|
1103
|
+
"Teach Idiolect AI one lasting thing about how the user writes. Send authored writing only with the user's consent, preferences in their words, or observations grounded in their own writing. This stores learning and never writes or revises prose.",
|
|
1104
|
+
inputSchema: {
|
|
1105
|
+
type: "object",
|
|
1106
|
+
additionalProperties: false,
|
|
1107
|
+
properties: {
|
|
1108
|
+
kind: {
|
|
1109
|
+
type: "string",
|
|
1110
|
+
enum: ["authored_example", "preference", "observation"],
|
|
1111
|
+
description: "Whether this is the user's authored writing, a preference they stated, or an observation grounded in their writing.",
|
|
1112
|
+
},
|
|
1113
|
+
text: { type: "string", minLength: 1, maxLength: 12000 },
|
|
1114
|
+
consent: {
|
|
1115
|
+
type: "boolean",
|
|
1116
|
+
const: true,
|
|
1117
|
+
description: "Required only for authored_example: the user wrote it and agreed it may be stored.",
|
|
1118
|
+
},
|
|
1119
|
+
basis: { type: "string", minLength: 1, maxLength: 2000 },
|
|
1120
|
+
scope: { type: "string", enum: ["general", "occasion", "piece"] },
|
|
1121
|
+
occasion: { type: "string", minLength: 1, maxLength: 500 },
|
|
1122
|
+
profileId: { type: "string", format: "uuid" },
|
|
1123
|
+
writingJobVersionId: { type: "string", format: "uuid" },
|
|
1124
|
+
},
|
|
1125
|
+
required: ["kind", "text"],
|
|
1126
|
+
},
|
|
1127
|
+
outputSchema: {
|
|
1128
|
+
type: "object",
|
|
1129
|
+
additionalProperties: false,
|
|
1130
|
+
properties: {
|
|
1131
|
+
status: { type: "string", enum: ["saved", "pending", "ready", "failed"] },
|
|
1132
|
+
message: { type: "string" },
|
|
1133
|
+
next: {
|
|
1134
|
+
type: "object",
|
|
1135
|
+
additionalProperties: false,
|
|
1136
|
+
properties: {
|
|
1137
|
+
action: { type: "string", enum: ["done", "retry", "continue_writing"] },
|
|
1138
|
+
tool: { type: ["string", "null"], enum: ["refine", null] },
|
|
1139
|
+
},
|
|
1140
|
+
required: ["action", "tool"],
|
|
1141
|
+
},
|
|
1142
|
+
contribution: {
|
|
1143
|
+
type: "object",
|
|
1144
|
+
additionalProperties: false,
|
|
1145
|
+
properties: {
|
|
1146
|
+
id: { type: "string", format: "uuid" },
|
|
1147
|
+
kind: { type: "string", enum: ["authored_example", "preference", "observation"] },
|
|
1148
|
+
status: { type: "string", enum: ["pending", "waiting", "applied", "not_used", "piece_only"] },
|
|
1149
|
+
},
|
|
1150
|
+
required: ["id", "kind", "status"],
|
|
1151
|
+
},
|
|
1152
|
+
profileId: { type: "string", format: "uuid" },
|
|
1153
|
+
writingJobVersionId: { type: "string", format: "uuid" },
|
|
1154
|
+
neededWriting: {
|
|
1155
|
+
type: "object",
|
|
1156
|
+
additionalProperties: false,
|
|
1157
|
+
properties: {
|
|
1158
|
+
pieces: { type: "integer", minimum: 0 },
|
|
1159
|
+
characters: { type: "integer", minimum: 0 },
|
|
1160
|
+
},
|
|
1161
|
+
required: ["pieces", "characters"],
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
required: ["status", "message", "next"],
|
|
1165
|
+
},
|
|
1166
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Learn Writing Style" },
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: "record_voice_feedback",
|
|
1170
|
+
description:
|
|
1171
|
+
"Record bounded feedback for an Idiolect AI output. Send a verdict every time the user responds to one; a verdict on its own is worth having. Put what you observed in critique. Send correctionText only when you have the user's final text verbatim.",
|
|
1172
|
+
inputSchema: {
|
|
1173
|
+
type: "object",
|
|
1174
|
+
additionalProperties: false,
|
|
1175
|
+
properties: {
|
|
1176
|
+
outputId: { type: "string", format: "uuid" },
|
|
1177
|
+
// Hand-written mirror of the HTTP route's vocabulary — this file cannot import
|
|
1178
|
+
// `lib/feedback/outcomes.ts`. `superseded` means the person changed the brief or
|
|
1179
|
+
// direction rather than judging the text (IDI-446); keep it in step by hand.
|
|
1180
|
+
status: { type: "string", enum: ["accepted", "corrected_and_accepted", "rejected", "superseded"] },
|
|
1181
|
+
correctionText: {
|
|
1182
|
+
type: "string",
|
|
1183
|
+
minLength: 1,
|
|
1184
|
+
maxLength: 20000,
|
|
1185
|
+
description: "The user's final text, verbatim and complete. Never an excerpt, a diff, or your own description of what changed — if you do not have their exact words, omit this field and describe the change in critique instead.",
|
|
1186
|
+
},
|
|
1187
|
+
critique: {
|
|
1188
|
+
type: "string",
|
|
1189
|
+
minLength: 1,
|
|
1190
|
+
maxLength: 2000,
|
|
1191
|
+
description: "What you observed about how the output landed, in the user's words or your own: what was wrong, what they changed, or why they kept it. Free text, and the right home for any description of an edit you cannot quote verbatim.",
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
required: ["outputId", "status"],
|
|
1195
|
+
},
|
|
1196
|
+
outputSchema: FEEDBACK_OUTPUT_SCHEMA,
|
|
1197
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Record Voice Feedback" },
|
|
1198
|
+
},
|
|
339
1199
|
];
|
|
340
1200
|
|
|
1201
|
+
export const accountTools = progressiveCatalog.tools;
|
|
1202
|
+
export const accountInstructions = progressiveCatalog.instructions;
|
|
1203
|
+
/** Hidden schemas retained for cached stdio callers and compatibility tests. */
|
|
1204
|
+
export const accountCompatibilityTools = accountToolDefinitions;
|
|
1205
|
+
|
|
1206
|
+
function toolError(message) {
|
|
1207
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function accountToolError(error) {
|
|
1211
|
+
if (error instanceof AccountRequestError && error.structuredContent) {
|
|
1212
|
+
return {
|
|
1213
|
+
content: [{ type: "text", text: error.structuredContent.message }],
|
|
1214
|
+
structuredContent: error.structuredContent,
|
|
1215
|
+
isError: true,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
if (error instanceof AccountRequestError) return toolError(error.message);
|
|
1219
|
+
return toolError("Idiolect AI account API failed before a safe response was available.");
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function createVoiceBody(args) {
|
|
1223
|
+
const samples = args.samples ?? [];
|
|
1224
|
+
const answers = args.answers ?? [];
|
|
1225
|
+
if (!Array.isArray(samples) || samples.length > 500 || samples.some((sample) => typeof sample !== "string" || sample.length > 20000)) {
|
|
1226
|
+
return { error: "create_my_voice: `samples` must contain at most 500 text blocks of at most 20,000 characters each." };
|
|
1227
|
+
}
|
|
1228
|
+
if (
|
|
1229
|
+
!Array.isArray(answers)
|
|
1230
|
+
|| answers.length > 20
|
|
1231
|
+
|| answers.some((answer) => (
|
|
1232
|
+
!answer
|
|
1233
|
+
|| typeof answer !== "object"
|
|
1234
|
+
|| Array.isArray(answer)
|
|
1235
|
+
|| typeof answer.question !== "string"
|
|
1236
|
+
|| answer.question.length > 500
|
|
1237
|
+
|| typeof answer.answer !== "string"
|
|
1238
|
+
|| answer.answer.length > 5000
|
|
1239
|
+
))
|
|
1240
|
+
) {
|
|
1241
|
+
return { error: "create_my_voice: `answers` must contain at most 20 question/answer pairs within the documented limits." };
|
|
1242
|
+
}
|
|
1243
|
+
if (args.name !== undefined && (typeof args.name !== "string" || args.name.length > 60)) {
|
|
1244
|
+
return { error: "create_my_voice: `name` must be text of at most 60 characters." };
|
|
1245
|
+
}
|
|
1246
|
+
if (args.profileId !== undefined && typeof args.profileId !== "string") {
|
|
1247
|
+
return { error: "create_my_voice: `profileId` must be a Writing Profile UUID." };
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
body: {
|
|
1251
|
+
...(args.profileId !== undefined ? { profileId: args.profileId } : {}),
|
|
1252
|
+
samples,
|
|
1253
|
+
answers: answers.map(({ question, answer }) => ({ question, answer })),
|
|
1254
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
1255
|
+
},
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function safeCreationResult(r) {
|
|
1260
|
+
const status = r.status ?? "more_evidence_required";
|
|
1261
|
+
const state = status === "ready" ? "complete" : status === "build_in_progress" ? "stale" : status === "failed" ? "failed" : "thin";
|
|
1262
|
+
const next = status === "ready"
|
|
1263
|
+
? { action: "reload_voice", tool: "get_my_voice" }
|
|
1264
|
+
: status === "build_in_progress"
|
|
1265
|
+
? { action: "retry", tool: "create_my_voice" }
|
|
1266
|
+
: status === "failed" ? { action: "done", tool: null } : { action: "collect_evidence", tool: "create_my_voice" };
|
|
1267
|
+
const neededChars = Number.isFinite(r.neededChars) ? r.neededChars : 0;
|
|
1268
|
+
const neededBlocks = Number.isFinite(r.neededBlocks) ? r.neededBlocks : 0;
|
|
1269
|
+
const message = state === "thin"
|
|
1270
|
+
? `More of the user's own writing is needed: add ${neededBlocks} distinct block${neededBlocks === 1 ? "" : "s"} and ${neededChars} characters.`
|
|
1271
|
+
: r.message ?? (state === "complete" ? "Writing Profile ready." : "Writing Profile creation is still in progress.");
|
|
1272
|
+
return publicMcpState(state, message, next, {
|
|
1273
|
+
status,
|
|
1274
|
+
reason: r.reason ?? null,
|
|
1275
|
+
voiceId: r.voiceId ?? null,
|
|
1276
|
+
// The shortfall appears only when there is one; the fixed minimums are gone (IDI-381).
|
|
1277
|
+
...(neededChars > 0 || neededBlocks > 0 ? { neededChars, neededBlocks } : {}),
|
|
1278
|
+
replay: r.replay === true,
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Preserve a server-proven refinement conflict without copying unknown fields. */
|
|
1283
|
+
function safeCreationFailurePayload(value) {
|
|
1284
|
+
if (
|
|
1285
|
+
!plainObject(value)
|
|
1286
|
+
|| value.status !== "failed"
|
|
1287
|
+
|| typeof value.message !== "string"
|
|
1288
|
+
|| !(value.reason === null || typeof value.reason === "string")
|
|
1289
|
+
|| !(value.voiceId === null || typeof value.voiceId === "string")
|
|
1290
|
+
|| typeof value.replay !== "boolean"
|
|
1291
|
+
) return null;
|
|
1292
|
+
return safeCreationResult(value);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const WRITING_PROFILE_ERROR_CODES = {
|
|
1296
|
+
list_writing_profiles: new Set(["unexpected_error"]),
|
|
1297
|
+
list_writing_profile_samples: new Set(["profile_not_found", "unexpected_error"]),
|
|
1298
|
+
create_writing_profile: new Set([
|
|
1299
|
+
"insufficient_evidence", "rate_limited", "request_conflict", "build_in_progress", "unexpected_error",
|
|
1300
|
+
]),
|
|
1301
|
+
add_samples_to_writing_profile: new Set([
|
|
1302
|
+
"insufficient_evidence", "request_conflict", "build_in_progress", "profile_not_found", "unexpected_error",
|
|
1303
|
+
]),
|
|
1304
|
+
replace_samples_in_writing_profile: new Set([
|
|
1305
|
+
"insufficient_evidence", "profile_not_found", "samples_not_fully_visible", "unexpected_error",
|
|
1306
|
+
]),
|
|
1307
|
+
remove_sample_from_writing_profile: new Set([
|
|
1308
|
+
"profile_not_found", "sample_not_found", "unexpected_error",
|
|
1309
|
+
]),
|
|
1310
|
+
set_current_writing_profile: new Set(["profile_not_found", "unexpected_error"]),
|
|
1311
|
+
delete_writing_profile: new Set([
|
|
1312
|
+
"profile_not_found", "replacement_required", "invalid_replacement", "unexpected_error",
|
|
1313
|
+
]),
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
function writingProfileRequest(name, args) {
|
|
1317
|
+
if (name === "list_writing_profiles") return { path: "/api/writing-profiles", method: "GET" };
|
|
1318
|
+
if (name === "create_writing_profile") {
|
|
1319
|
+
return {
|
|
1320
|
+
path: "/api/writing-profiles",
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
body: {
|
|
1323
|
+
samples: args.samples ?? [],
|
|
1324
|
+
answers: args.answers ?? [],
|
|
1325
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
const id = encodeURIComponent(args.profileId ?? '');
|
|
1330
|
+
if (name === "list_writing_profile_samples") {
|
|
1331
|
+
return { path: `/api/writing-profiles/${id}/samples`, method: "GET" };
|
|
1332
|
+
}
|
|
1333
|
+
if (name === "add_samples_to_writing_profile") {
|
|
1334
|
+
// Absent means the current profile; the account API resolves it, this only forwards.
|
|
1335
|
+
const path = args.profileId === undefined
|
|
1336
|
+
? "/api/writing-profiles/current/samples"
|
|
1337
|
+
: `/api/writing-profiles/${id}/samples`;
|
|
1338
|
+
return { path, method: "POST", body: { samples: args.samples } };
|
|
1339
|
+
}
|
|
1340
|
+
if (name === "replace_samples_in_writing_profile") {
|
|
1341
|
+
return { path: `/api/writing-profiles/${id}/samples`, method: "PUT", body: { samples: args.samples } };
|
|
1342
|
+
}
|
|
1343
|
+
if (name === "remove_sample_from_writing_profile") {
|
|
1344
|
+
const sampleId = encodeURIComponent(args.sampleId);
|
|
1345
|
+
return { path: `/api/writing-profiles/${id}/samples/${sampleId}`, method: "DELETE" };
|
|
1346
|
+
}
|
|
1347
|
+
if (name === "set_current_writing_profile") {
|
|
1348
|
+
return { path: `/api/writing-profiles/${id}/current`, method: "PUT" };
|
|
1349
|
+
}
|
|
1350
|
+
const replacement = args.replacementProfileId === undefined
|
|
1351
|
+
? ""
|
|
1352
|
+
: `?replacementProfileId=${encodeURIComponent(args.replacementProfileId)}`;
|
|
1353
|
+
return { path: `/api/writing-profiles/${id}${replacement}`, method: "DELETE" };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** The shortfall is re-validated field by field: an unchecked passthrough would let the
|
|
1357
|
+
* account API widen this tool's declared output schema without the stdio server knowing. */
|
|
1358
|
+
function safeProfileShortfall(value) {
|
|
1359
|
+
if (!plainObject(value) || typeof value.message !== "string") return null;
|
|
1360
|
+
const detail = { message: value.message.slice(0, 2000) };
|
|
1361
|
+
if (value.reason === "insufficient_input" || value.reason === "unsupported_language") {
|
|
1362
|
+
detail.reason = value.reason;
|
|
1363
|
+
}
|
|
1364
|
+
// Counts travel only when the evidence gate produced them; a profile that could not be
|
|
1365
|
+
// found has no shortfall to count.
|
|
1366
|
+
for (const key of ["neededChars", "neededBlocks", "droppedBlocks", "minimumChars", "minimumBlocks"]) {
|
|
1367
|
+
if (Number.isInteger(value[key]) && value[key] >= 0) detail[key] = value[key];
|
|
1368
|
+
}
|
|
1369
|
+
return detail;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function safeWritingProfileResult(name, value) {
|
|
1373
|
+
if (!plainObject(value) || typeof value.ok !== "boolean") return null;
|
|
1374
|
+
if (value.ok === true && Object.hasOwn(value, "value")) return { ok: true, value: value.value };
|
|
1375
|
+
const code = plainObject(value.error) ? value.error.code : null;
|
|
1376
|
+
if (typeof code !== "string" || !WRITING_PROFILE_ERROR_CODES[name]?.has(code)) return null;
|
|
1377
|
+
const detail = safeProfileShortfall(plainObject(value.error) ? value.error.detail : null);
|
|
1378
|
+
return { ok: false, error: detail ? { code, detail } : { code } };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function writingProfileSuccessMessage(name, value, title) {
|
|
1382
|
+
if (name === "list_writing_profiles" && Array.isArray(value)) {
|
|
1383
|
+
if (value.length === 0) return "No Writing Profiles found.";
|
|
1384
|
+
return ["Writing Profiles:", ...value.map((profile) => {
|
|
1385
|
+
const label = typeof profile?.name === "string" && profile.name.length > 0
|
|
1386
|
+
? profile.name
|
|
1387
|
+
: "Unnamed profile";
|
|
1388
|
+
const current = profile?.isCurrent === true ? " (current)" : "";
|
|
1389
|
+
return `- ${label}: ${String(profile?.id ?? "unknown")}${current}`;
|
|
1390
|
+
})].join("\n");
|
|
1391
|
+
}
|
|
1392
|
+
if (
|
|
1393
|
+
["list_writing_profile_samples", "replace_samples_in_writing_profile", "remove_sample_from_writing_profile"].includes(name)
|
|
1394
|
+
&& plainObject(value)
|
|
1395
|
+
&& Array.isArray(value.samples)
|
|
1396
|
+
) {
|
|
1397
|
+
const samples = value.samples.map((sample) => {
|
|
1398
|
+
const id = plainObject(sample) && typeof sample.id === "string" ? sample.id : "unknown";
|
|
1399
|
+
const writing = plainObject(sample) && typeof sample.text === "string" ? sample.text : "";
|
|
1400
|
+
return `- sampleId ${id}: ${JSON.stringify(writing)}`;
|
|
1401
|
+
});
|
|
1402
|
+
return [
|
|
1403
|
+
`${title} complete for profile ${String(value.profileId ?? "unknown")} (generation ${String(value.generation ?? "unknown")}).`,
|
|
1404
|
+
`Usable samples: ${value.samples.length}.`,
|
|
1405
|
+
...(samples.length > 0 ? samples : ["- No usable writing samples remain."]),
|
|
1406
|
+
...(Number.isInteger(value.heldNotUsable) && value.heldNotUsable > 0
|
|
1407
|
+
? [`Held but not usable for rebuild: ${value.heldNotUsable}.`]
|
|
1408
|
+
: []),
|
|
1409
|
+
].join("\n");
|
|
1410
|
+
}
|
|
1411
|
+
return `${title} complete.`;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async function callWritingProfileApi(name, args) {
|
|
1415
|
+
const request = writingProfileRequest(name, args);
|
|
1416
|
+
const res = await fetch(`${ACCOUNT_BASE}${request.path}`, {
|
|
1417
|
+
method: request.method,
|
|
1418
|
+
headers: {
|
|
1419
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
1420
|
+
...(request.body ? { "content-type": "application/json" } : {}),
|
|
1421
|
+
},
|
|
1422
|
+
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
1423
|
+
});
|
|
1424
|
+
let parsed = null;
|
|
1425
|
+
try {
|
|
1426
|
+
parsed = safeWritingProfileResult(name, await res.json());
|
|
1427
|
+
} catch {
|
|
1428
|
+
// The bounded failure below handles non-JSON and malformed responses.
|
|
1429
|
+
}
|
|
1430
|
+
if (!parsed) throw new AccountRequestError(`${name}: Idiolect AI account API failed (${res.status}).`, res.status);
|
|
1431
|
+
const title = accountToolDefinitions.find((tool) => tool.name === name)?.annotations.title ?? name;
|
|
1432
|
+
const building = name === "create_writing_profile"
|
|
1433
|
+
&& parsed.ok === false
|
|
1434
|
+
&& parsed.error.code === "build_in_progress";
|
|
1435
|
+
const message = parsed.ok
|
|
1436
|
+
? writingProfileSuccessMessage(name, parsed.value, title)
|
|
1437
|
+
: building
|
|
1438
|
+
? "Writing Profile accepted and building. Retry this same call shortly to get the completed profile."
|
|
1439
|
+
: parsed.error.detail?.message ?? `${title} failed: ${parsed.error.code}.`;
|
|
1440
|
+
return {
|
|
1441
|
+
content: [{ type: "text", text: message }],
|
|
1442
|
+
structuredContent: parsed,
|
|
1443
|
+
...(!parsed.ok && !building ? { isError: true } : {}),
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** Import-safe account handler used by the stdio server and its parity tests. */
|
|
1448
|
+
export async function handleAccountTool(name, args = {}) {
|
|
1449
|
+
if (Object.hasOwn(WRITING_PROFILE_ERROR_CODES, name)) {
|
|
1450
|
+
try {
|
|
1451
|
+
return await callWritingProfileApi(name, args);
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
return accountToolError(error);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (name === "get_credit_status") {
|
|
1457
|
+
try {
|
|
1458
|
+
const credits = safeCreditPresentation(await accountRequest("/api/v1/credits", {
|
|
1459
|
+
toolName: "get_credit_status",
|
|
1460
|
+
}));
|
|
1461
|
+
if (!credits) return toolError("get_credit_status: Idiolect AI returned an invalid credit status.");
|
|
1462
|
+
return {
|
|
1463
|
+
content: [{ type: "text", text: credits.message }],
|
|
1464
|
+
structuredContent: credits,
|
|
1465
|
+
};
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
return accountToolError(error);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (name === "get_my_voice") {
|
|
1472
|
+
try {
|
|
1473
|
+
const r = await accountRequest("/api/v1/voice", { toolName: "get_my_voice" });
|
|
1474
|
+
const card = r.card || formatValue(r.voice);
|
|
1475
|
+
return {
|
|
1476
|
+
content: [
|
|
1477
|
+
{
|
|
1478
|
+
type: "text",
|
|
1479
|
+
text: `${card}\n\n---\n(Idiolect AI account voice loaded. Write everything as this person from now on.)`,
|
|
1480
|
+
},
|
|
1481
|
+
],
|
|
1482
|
+
structuredContent: publicMcpState("ready", "Writing Profile ready.", { action: "continue_writing", tool: null }, {
|
|
1483
|
+
status: "ready",
|
|
1484
|
+
...(r.voice || {}),
|
|
1485
|
+
}),
|
|
1486
|
+
};
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
if (error instanceof AccountRequestError && error.status === 404) {
|
|
1489
|
+
const message = "No Writing Profile yet. Offer the user both ways to create one: sharing writing they authored, or answering a few short questions about how they write. Use only natural writing the user actually composed; approval alone does not establish authorship. Nothing is created until the user agrees.";
|
|
1490
|
+
return {
|
|
1491
|
+
content: [{ type: "text", text: message }],
|
|
1492
|
+
structuredContent: publicMcpState("missing", message, { action: "collect_evidence", tool: "create_my_voice" }, {
|
|
1493
|
+
status: "setup_required",
|
|
1494
|
+
}),
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
return accountToolError(error);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (name === "create_my_voice") {
|
|
1502
|
+
const prepared = createVoiceBody(args);
|
|
1503
|
+
if (prepared.error) return {
|
|
1504
|
+
content: [{ type: "text", text: prepared.error }],
|
|
1505
|
+
structuredContent: publicMcpState("failed", prepared.error, { action: "retry", tool: "create_my_voice" }),
|
|
1506
|
+
isError: true,
|
|
1507
|
+
};
|
|
1508
|
+
try {
|
|
1509
|
+
const r = await accountRequest("/api/v1/voice/create", {
|
|
1510
|
+
method: "POST",
|
|
1511
|
+
body: prepared.body,
|
|
1512
|
+
toolName: "create_my_voice",
|
|
1513
|
+
});
|
|
1514
|
+
const structuredContent = safeCreationResult(r);
|
|
1515
|
+
return {
|
|
1516
|
+
content: [{ type: "text", text: structuredContent.message }],
|
|
1517
|
+
structuredContent,
|
|
1518
|
+
};
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
return accountToolError(error);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (name === "record_voice_feedback" || name === "learn_writing_style") {
|
|
1525
|
+
try {
|
|
1526
|
+
return await accountMcpRequest(name, args);
|
|
1527
|
+
} catch {
|
|
1528
|
+
const learning = name === "learn_writing_style";
|
|
1529
|
+
const message = learning
|
|
1530
|
+
? "Writing style could not be saved. Retry once or finish."
|
|
1531
|
+
: "Feedback could not be recorded. Retry once or finish.";
|
|
1532
|
+
return {
|
|
1533
|
+
content: [{ type: "text", text: message }],
|
|
1534
|
+
...(learning
|
|
1535
|
+
? { structuredContent: { status: "failed", message, next: { action: "retry", tool: null } } }
|
|
1536
|
+
: { structuredContent: publicMcpState("failed", message, { action: "retry", tool: "record_voice_feedback" }) }),
|
|
1537
|
+
isError: true,
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return undefined;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
341
1546
|
const server = new Server(
|
|
342
|
-
{ name: "idiolect", version:
|
|
1547
|
+
{ name: "idiolect", version: PACKAGE_VERSION },
|
|
343
1548
|
{
|
|
344
1549
|
capabilities: { tools: {} },
|
|
345
|
-
// Hint folded into the client's context (MCP spec)
|
|
346
|
-
instructions:
|
|
347
|
-
|
|
1550
|
+
// Hint folded into the client's context (MCP spec).
|
|
1551
|
+
instructions: ACCOUNT_MODE
|
|
1552
|
+
? accountInstructions
|
|
1553
|
+
: "Use `rewrite_in_voice` or `draft_in_voice` only when the user asks for writing in the configured voice. Use `score_voice` only when the user explicitly asks for measurement. Never infer another person's writing or invent the user's opinions.",
|
|
348
1554
|
},
|
|
349
1555
|
);
|
|
350
1556
|
|
|
@@ -352,27 +1558,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
352
1558
|
tools: ACCOUNT_MODE ? accountTools : legacyTools,
|
|
353
1559
|
}));
|
|
354
1560
|
|
|
355
|
-
|
|
1561
|
+
export async function handleToolRequest(req) {
|
|
356
1562
|
if (ACCOUNT_MODE) {
|
|
357
1563
|
const args = req.params.arguments || {};
|
|
358
|
-
|
|
359
|
-
|
|
1564
|
+
if (
|
|
1565
|
+
PROGRESSIVE_ACCOUNT_TOOL_NAMES.has(req.params.name) ||
|
|
1566
|
+
REMOTE_COMPATIBILITY_TOOL_NAMES.has(req.params.name)
|
|
1567
|
+
) {
|
|
360
1568
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return
|
|
364
|
-
content: [
|
|
365
|
-
{
|
|
366
|
-
type: "text",
|
|
367
|
-
text: `${card}\n\n---\n(Idiolect AI account voice loaded. Write everything as this person from now on.)`,
|
|
368
|
-
},
|
|
369
|
-
],
|
|
370
|
-
structuredContent: r.voice || {},
|
|
371
|
-
};
|
|
372
|
-
} catch (e) {
|
|
373
|
-
return { content: [{ type: "text", text: String(e) }], isError: true };
|
|
1569
|
+
return await accountMcpRequest(req.params.name, args);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
return accountToolError(error);
|
|
374
1572
|
}
|
|
375
1573
|
}
|
|
1574
|
+
const nativeAccountResult = await handleAccountTool(req.params.name, args);
|
|
1575
|
+
if (nativeAccountResult) return nativeAccountResult;
|
|
376
1576
|
|
|
377
1577
|
if (req.params.name === "score_voice") {
|
|
378
1578
|
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
@@ -387,26 +1587,37 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
387
1587
|
});
|
|
388
1588
|
return {
|
|
389
1589
|
content: [{ type: "text", text: scoreSummary(r) }],
|
|
390
|
-
structuredContent: {
|
|
391
|
-
voiceMatch: r.voiceMatch ?? null,
|
|
1590
|
+
structuredContent: publicMcpState("complete", "Voice Match check complete.", { action: "continue_writing", tool: null }, {
|
|
1591
|
+
voiceMatch: r.absoluteScoreReportable === false ? null : r.voiceMatch ?? null,
|
|
392
1592
|
measured: !!r.measured,
|
|
393
1593
|
validated: !!r.validated,
|
|
394
1594
|
instrument: r.instrument ?? null,
|
|
395
1595
|
reason: r.reason ?? null,
|
|
396
|
-
register: r.register
|
|
397
|
-
approximate:
|
|
1596
|
+
...(typeof r.register === "string" ? { register: r.register } : {}),
|
|
1597
|
+
...(typeof r.approximate === "boolean" ? { approximate: r.approximate } : {}),
|
|
398
1598
|
...(r.voice ? { voice: r.voice } : {}),
|
|
399
1599
|
...(r.evidence ? { evidence: r.evidence } : {}),
|
|
400
|
-
},
|
|
1600
|
+
}),
|
|
401
1601
|
};
|
|
402
1602
|
} catch (e) {
|
|
403
|
-
return
|
|
1603
|
+
return accountToolError(e);
|
|
404
1604
|
}
|
|
405
1605
|
}
|
|
406
1606
|
|
|
407
1607
|
if (req.params.name === "rewrite_in_voice") {
|
|
408
1608
|
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
1609
|
+
const profileId = typeof args.profileId === "string" ? args.profileId : undefined;
|
|
409
1610
|
const targetRegister = typeof args.targetRegister === "string" ? args.targetRegister : undefined;
|
|
1611
|
+
const instructions = typeof args.instructions === "string" && args.instructions.trim()
|
|
1612
|
+
? args.instructions.trim()
|
|
1613
|
+
: undefined;
|
|
1614
|
+
const invalidChangeRequest = changeRequestError(args, "rewrite_in_voice");
|
|
1615
|
+
if (invalidChangeRequest) return invalidChangeRequest;
|
|
1616
|
+
const invalidSourceAuthor = authorProvenanceError(args, "rewrite_in_voice", "sourceAuthor");
|
|
1617
|
+
if (invalidSourceAuthor) return invalidSourceAuthor;
|
|
1618
|
+
const changeScope = args.changeScope === "polish" ? "light_edit" : args.changeScope;
|
|
1619
|
+
const effects = args.effects;
|
|
1620
|
+
const sourceAuthor = args.sourceAuthor;
|
|
410
1621
|
if (!text) {
|
|
411
1622
|
return { content: [{ type: "text", text: "rewrite_in_voice: provide `text` to rewrite." }], isError: true };
|
|
412
1623
|
}
|
|
@@ -415,7 +1626,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
415
1626
|
content: [
|
|
416
1627
|
{
|
|
417
1628
|
type: "text",
|
|
418
|
-
text: `rewrite_in_voice: \`targetRegister\` must be one of ${REWRITE_REGISTERS
|
|
1629
|
+
text: `rewrite_in_voice: \`targetRegister\` must be one of ${backtickedValues(REWRITE_REGISTERS)}.`,
|
|
419
1630
|
},
|
|
420
1631
|
],
|
|
421
1632
|
isError: true,
|
|
@@ -424,45 +1635,49 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
424
1635
|
try {
|
|
425
1636
|
const r = await accountRequest("/api/v1/rewrite", {
|
|
426
1637
|
method: "POST",
|
|
427
|
-
body: {
|
|
1638
|
+
body: {
|
|
1639
|
+
text,
|
|
1640
|
+
...(profileId !== undefined ? { voice: profileId } : {}),
|
|
1641
|
+
...(targetRegister ? { targetRegister } : {}),
|
|
1642
|
+
...(instructions ? { instructions } : {}),
|
|
1643
|
+
...(changeScope !== undefined ? { changeScope } : {}),
|
|
1644
|
+
...(effects !== undefined ? { effects } : {}),
|
|
1645
|
+
...(sourceAuthor !== undefined ? { sourceAuthor } : {}),
|
|
1646
|
+
},
|
|
428
1647
|
toolName: "rewrite_in_voice",
|
|
429
1648
|
});
|
|
1649
|
+
// The account API returns the rewrite, the profile and its evidence state. The
|
|
1650
|
+
// score block this used to print and the constants it used to coerce into the
|
|
1651
|
+
// structured result are gone (IDI-381).
|
|
430
1652
|
return {
|
|
431
|
-
content: [
|
|
432
|
-
|
|
433
|
-
type: "text",
|
|
434
|
-
text: `Rewritten:\n${r.rewritten || ""}\n\n${scoreSummary(r)}${formatAlternatives(r.alternatives)}`,
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
structuredContent: {
|
|
1653
|
+
content: [{ type: "text", text: `Rewritten:\n${r.rewritten || ""}` }],
|
|
1654
|
+
structuredContent: publicMcpState("complete", "Rewrite complete.", { action: "continue_writing", tool: null }, {
|
|
438
1655
|
rewritten: r.rewritten ?? "",
|
|
439
|
-
|
|
440
|
-
delta: r.delta ?? null,
|
|
441
|
-
measured: !!r.measured,
|
|
442
|
-
crossFit: !!r.crossFit,
|
|
443
|
-
candidateText: r.candidateText ?? r.rewritten ?? "",
|
|
444
|
-
candidateVoiceMatch: r.candidateVoiceMatch ?? null,
|
|
445
|
-
candidateDelta: r.candidateDelta ?? null,
|
|
446
|
-
improved: r.improved ?? null,
|
|
447
|
-
regressed: r.regressed ?? null,
|
|
448
|
-
validated: !!r.validated,
|
|
449
|
-
instrument: r.instrument ?? null,
|
|
450
|
-
reason: r.reason ?? null,
|
|
451
|
-
register: r.register ?? "",
|
|
452
|
-
approximate: !!r.approximate,
|
|
1656
|
+
...(r.reason !== undefined ? { reason: r.reason ?? null } : {}),
|
|
453
1657
|
...(r.voice ? { voice: r.voice } : {}),
|
|
454
1658
|
...(r.evidence ? { evidence: r.evidence } : {}),
|
|
455
|
-
},
|
|
1659
|
+
}),
|
|
456
1660
|
};
|
|
457
1661
|
} catch (e) {
|
|
458
|
-
return
|
|
1662
|
+
return accountToolError(e);
|
|
459
1663
|
}
|
|
460
1664
|
}
|
|
461
1665
|
|
|
462
1666
|
if (req.params.name === "draft_in_voice") {
|
|
463
1667
|
const brief = typeof args.brief === "string" ? args.brief.trim() : "";
|
|
1668
|
+
const profileId = typeof args.profileId === "string" ? args.profileId : undefined;
|
|
464
1669
|
const format = typeof args.format === "string" ? args.format : undefined;
|
|
1670
|
+
const instructions = typeof args.instructions === "string" && args.instructions.trim()
|
|
1671
|
+
? args.instructions.trim()
|
|
1672
|
+
: undefined;
|
|
465
1673
|
const formats = new Set(["linkedin", "x_post", "x_thread", "newsletter", "generic"]);
|
|
1674
|
+
const invalidChangeRequest = changeRequestError(args, "draft_in_voice");
|
|
1675
|
+
if (invalidChangeRequest) return invalidChangeRequest;
|
|
1676
|
+
const invalidBriefAuthor = authorProvenanceError(args, "draft_in_voice", "briefAuthor");
|
|
1677
|
+
if (invalidBriefAuthor) return invalidBriefAuthor;
|
|
1678
|
+
const changeScope = args.changeScope === "polish" ? "light_edit" : args.changeScope;
|
|
1679
|
+
const effects = args.effects;
|
|
1680
|
+
const briefAuthor = args.briefAuthor;
|
|
466
1681
|
if (!brief) {
|
|
467
1682
|
return { content: [{ type: "text", text: "draft_in_voice: provide `brief` to draft from." }], isError: true };
|
|
468
1683
|
}
|
|
@@ -471,7 +1686,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
471
1686
|
content: [
|
|
472
1687
|
{
|
|
473
1688
|
type: "text",
|
|
474
|
-
text:
|
|
1689
|
+
text: `draft_in_voice: \`format\` must be one of ${backtickedValues([...formats])}.`,
|
|
475
1690
|
},
|
|
476
1691
|
],
|
|
477
1692
|
isError: true,
|
|
@@ -480,32 +1695,28 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
480
1695
|
try {
|
|
481
1696
|
const r = await accountRequest("/api/v1/draft", {
|
|
482
1697
|
method: "POST",
|
|
483
|
-
body: {
|
|
1698
|
+
body: {
|
|
1699
|
+
brief,
|
|
1700
|
+
...(profileId !== undefined ? { voice: profileId } : {}),
|
|
1701
|
+
...(format ? { format } : {}),
|
|
1702
|
+
...(instructions ? { instructions } : {}),
|
|
1703
|
+
...(changeScope !== undefined ? { changeScope } : {}),
|
|
1704
|
+
...(effects !== undefined ? { effects } : {}),
|
|
1705
|
+
...(briefAuthor !== undefined ? { briefAuthor } : {}),
|
|
1706
|
+
},
|
|
484
1707
|
toolName: "draft_in_voice",
|
|
485
1708
|
});
|
|
486
1709
|
return {
|
|
487
|
-
content: [
|
|
488
|
-
|
|
489
|
-
type: "text",
|
|
490
|
-
text: `Draft:\n${r.draft || ""}\n\n${scoreSummary(r)}${formatAlternatives(r.alternatives)}`,
|
|
491
|
-
},
|
|
492
|
-
],
|
|
493
|
-
structuredContent: {
|
|
1710
|
+
content: [{ type: "text", text: `Draft:\n${r.draft || ""}` }],
|
|
1711
|
+
structuredContent: publicMcpState("complete", "Draft complete.", { action: "continue_writing", tool: null }, {
|
|
494
1712
|
draft: r.draft ?? "",
|
|
495
|
-
|
|
496
|
-
measured: !!r.measured,
|
|
497
|
-
crossFit: !!r.crossFit,
|
|
498
|
-
validated: !!r.validated,
|
|
499
|
-
instrument: r.instrument ?? null,
|
|
500
|
-
reason: r.reason ?? null,
|
|
501
|
-
register: r.register ?? "",
|
|
502
|
-
approximate: !!r.approximate,
|
|
1713
|
+
...(r.reason !== undefined ? { reason: r.reason ?? null } : {}),
|
|
503
1714
|
...(r.voice ? { voice: r.voice } : {}),
|
|
504
1715
|
...(r.evidence ? { evidence: r.evidence } : {}),
|
|
505
|
-
},
|
|
1716
|
+
}),
|
|
506
1717
|
};
|
|
507
1718
|
} catch (e) {
|
|
508
|
-
return
|
|
1719
|
+
return accountToolError(e);
|
|
509
1720
|
}
|
|
510
1721
|
}
|
|
511
1722
|
|
|
@@ -533,14 +1744,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
533
1744
|
}
|
|
534
1745
|
try {
|
|
535
1746
|
const r = await scoreVoice(texts, region);
|
|
536
|
-
const lines = r.scores
|
|
537
|
-
.map((s, i) => `#${i + 1} · Voice Match ${s.voiceMatch}/100 — ${s.text.slice(0, 60)}…`)
|
|
538
|
-
.join("\n");
|
|
539
1747
|
return {
|
|
540
1748
|
content: [
|
|
541
1749
|
{
|
|
542
1750
|
type: "text",
|
|
543
|
-
text:
|
|
1751
|
+
text: formatLegacyScoreVoiceResult(r),
|
|
544
1752
|
},
|
|
545
1753
|
],
|
|
546
1754
|
};
|
|
@@ -577,7 +1785,20 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
577
1785
|
} catch (e) {
|
|
578
1786
|
return { content: [{ type: "text", text: String(e) }], isError: true };
|
|
579
1787
|
}
|
|
580
|
-
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
server.setRequestHandler(CallToolRequestSchema, handleToolRequest);
|
|
581
1791
|
|
|
582
|
-
|
|
583
|
-
|
|
1792
|
+
function isMainModule() {
|
|
1793
|
+
if (!process.argv[1]) return false;
|
|
1794
|
+
try {
|
|
1795
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (isMainModule()) {
|
|
1802
|
+
const transport = new StdioServerTransport();
|
|
1803
|
+
await server.connect(transport);
|
|
1804
|
+
}
|