idiolect-mcp 0.4.0 → 0.5.1
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 +5 -4
- package/progressive-catalog.json +6595 -0
- package/public-contract.js +85 -0
- package/server.mjs +1369 -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 "./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}.`;
|
|
87
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.`;
|
|
88
202
|
}
|
|
89
203
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
93
233
|
}
|
|
94
|
-
if (
|
|
95
|
-
return
|
|
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;
|
|
239
|
+
}
|
|
240
|
+
return state === "zero_balance" && balance === 0 && !generationAvailable && purchaseRequired && manageUrl !== null;
|
|
241
|
+
}
|
|
242
|
+
|
|
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
|
+
}
|
|
341
|
+
}
|
|
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,122 @@ 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
|
+
const CALIBRATION_SCHEMA = {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: {
|
|
495
|
+
direction: {
|
|
496
|
+
type: "string",
|
|
497
|
+
enum: ["direct", "balanced", "gentle"],
|
|
498
|
+
description: "The user's answer to the single task-local calibration question.",
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
required: ["direction"],
|
|
502
|
+
additionalProperties: false,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/** Validates `sourceAuthor`/`briefAuthor` and defaults an absent one to `"unknown"` on the
|
|
506
|
+
* returned copy of `args`. Returns a tool error, never throws. */
|
|
507
|
+
export function authorProvenanceError(args, toolName, key) {
|
|
508
|
+
if (args[key] === undefined) return null;
|
|
509
|
+
if (typeof args[key] !== "string" || !SOURCE_AUTHOR_SET.has(args[key])) {
|
|
510
|
+
return toolError(`${toolName}: \`${key}\` must be one of ${backtickedValues(SOURCE_AUTHORS)}.`);
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function changeRequestError(args, toolName) {
|
|
516
|
+
if (args.changeScope !== undefined
|
|
517
|
+
&& (typeof args.changeScope !== "string" || !CHANGE_SCOPE_SET.has(args.changeScope))) {
|
|
518
|
+
return toolError(`${toolName}: \`changeScope\` must be one of ${backtickedValues(CHANGE_SCOPES)}.`);
|
|
519
|
+
}
|
|
520
|
+
if (args.effects === undefined) return null;
|
|
521
|
+
if (!Array.isArray(args.effects)) return toolError(`${toolName}: \`effects\` must be a list.`);
|
|
522
|
+
if (args.effects.length > 20) return toolError(`${toolName}: \`effects\` accepts at most 20 entries.`);
|
|
523
|
+
for (const effect of args.effects) {
|
|
524
|
+
if (!plainObject(effect) || Object.keys(effect).some((key) => !EFFECT_PROPERTY_SET.has(key))) {
|
|
525
|
+
return toolError(`${toolName}: each \`effects\` entry must contain only \`kind\`, \`ask\`, and \`strength\`.`);
|
|
526
|
+
}
|
|
527
|
+
if (typeof effect.kind !== "string" || !EFFECT_KIND_SET.has(effect.kind)) {
|
|
528
|
+
return toolError(`${toolName}: \`effects[].kind\` must be one of ${backtickedValues(EFFECT_KINDS)}.`);
|
|
529
|
+
}
|
|
530
|
+
if (typeof effect.ask !== "string" || effect.ask.length < 1 || effect.ask.length > 2000) {
|
|
531
|
+
return toolError(`${toolName}: \`effects[].ask\` must be 1 to 2,000 characters.`);
|
|
532
|
+
}
|
|
533
|
+
if (effect.strength !== undefined
|
|
534
|
+
&& (typeof effect.strength !== "string" || !EFFECT_STRENGTH_SET.has(effect.strength))) {
|
|
535
|
+
return toolError(`${toolName}: \`effects[].strength\` must be one of ${backtickedValues(EFFECT_STRENGTHS)}.`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
150
540
|
|
|
151
541
|
const legacyTools = [
|
|
152
542
|
{
|
|
153
543
|
name: "get_my_voice",
|
|
154
544
|
description:
|
|
155
|
-
"Load the user's Idiolect AI Voice Card
|
|
545
|
+
"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
546
|
inputSchema: {
|
|
157
547
|
type: "object",
|
|
158
548
|
properties: {
|
|
@@ -215,14 +605,95 @@ const PROVENANCE_OUTPUT_PROPERTIES = {
|
|
|
215
605
|
validated: { type: "boolean", description: "True only when this result was measured by the validated LUAR instrument." },
|
|
216
606
|
instrument: { type: ["string", "null"] },
|
|
217
607
|
reason: { type: ["string", "null"] },
|
|
218
|
-
register: { type: "string" },
|
|
219
|
-
approximate: { type: "boolean" },
|
|
220
608
|
voice: SELECTED_VOICE_SCHEMA,
|
|
221
609
|
evidence: EVIDENCE_OUTPUT_SCHEMA,
|
|
222
610
|
};
|
|
611
|
+
const CREDIT_OUTPUT_SCHEMA = {
|
|
612
|
+
type: "object",
|
|
613
|
+
additionalProperties: false,
|
|
614
|
+
properties: {
|
|
615
|
+
state: {
|
|
616
|
+
type: "string",
|
|
617
|
+
enum: ["free_beta", "balance_available", "low_balance", "zero_balance", "legacy_unlimited", "accounting_unavailable"],
|
|
618
|
+
},
|
|
619
|
+
mode: { type: "string", enum: ["free", "credits"] },
|
|
620
|
+
balance: { type: ["integer", "null"], minimum: 0 },
|
|
621
|
+
balancePreview: { type: "boolean" },
|
|
622
|
+
unitCost: { type: "number", enum: [0, 1] },
|
|
623
|
+
charged: { type: "number", enum: [0, 1] },
|
|
624
|
+
unlimited: { type: "boolean" },
|
|
625
|
+
accountingAvailable: { type: "boolean" },
|
|
626
|
+
generationAvailable: { type: "boolean" },
|
|
627
|
+
purchaseRequired: { type: "boolean" },
|
|
628
|
+
manageUrl: { type: ["string", "null"] },
|
|
629
|
+
replayed: { type: "boolean" },
|
|
630
|
+
message: { type: "string" },
|
|
631
|
+
},
|
|
632
|
+
required: [
|
|
633
|
+
"state",
|
|
634
|
+
"mode",
|
|
635
|
+
"balance",
|
|
636
|
+
"balancePreview",
|
|
637
|
+
"unitCost",
|
|
638
|
+
"charged",
|
|
639
|
+
"unlimited",
|
|
640
|
+
"accountingAvailable",
|
|
641
|
+
"generationAvailable",
|
|
642
|
+
"purchaseRequired",
|
|
643
|
+
"manageUrl",
|
|
644
|
+
"replayed",
|
|
645
|
+
"message",
|
|
646
|
+
],
|
|
647
|
+
};
|
|
648
|
+
const CREDIT_ERROR_SCHEMA = {
|
|
649
|
+
type: "object",
|
|
650
|
+
additionalProperties: false,
|
|
651
|
+
properties: {
|
|
652
|
+
code: { type: "string", enum: ["insufficient_credits", "credit_accounting_unavailable"] },
|
|
653
|
+
retryable: { type: "boolean", enum: [true] },
|
|
654
|
+
},
|
|
655
|
+
required: ["code", "retryable"],
|
|
656
|
+
};
|
|
657
|
+
const CREDIT_RETRY_SCHEMA = {
|
|
658
|
+
type: "object",
|
|
659
|
+
additionalProperties: false,
|
|
660
|
+
properties: {
|
|
661
|
+
safe: { type: "boolean", enum: [true] },
|
|
662
|
+
idempotent: { type: "boolean", enum: [true] },
|
|
663
|
+
scope: { type: "string", enum: ["pre_generation"] },
|
|
664
|
+
after: { type: "string", enum: ["balance_updated", "accounting_available"] },
|
|
665
|
+
},
|
|
666
|
+
required: ["safe", "idempotent", "scope", "after"],
|
|
667
|
+
};
|
|
668
|
+
// Carried by a credit-gate refusal only; a completed draft or rewrite no longer reports
|
|
669
|
+
// credits (13 constants in free mode) or a status beside `state` (IDI-381).
|
|
670
|
+
const CREDIT_GENERATION_OUTPUT_PROPERTIES = {
|
|
671
|
+
status: { type: "string", enum: ["purchase_required", "accounting_unavailable"] },
|
|
672
|
+
message: { type: "string" },
|
|
673
|
+
credits: CREDIT_OUTPUT_SCHEMA,
|
|
674
|
+
error: CREDIT_ERROR_SCHEMA,
|
|
675
|
+
retry: CREDIT_RETRY_SCHEMA,
|
|
676
|
+
};
|
|
677
|
+
const NEXT_ACTION_SCHEMA = {
|
|
678
|
+
type: "object",
|
|
679
|
+
additionalProperties: false,
|
|
680
|
+
properties: {
|
|
681
|
+
action: { type: "string", enum: ["collect_evidence", "reload_voice", "continue_writing", "ask_feedback", "retry", "done"] },
|
|
682
|
+
tool: { type: ["string", "null"], enum: ["create_my_voice", "get_my_voice", "record_voice_feedback", null] },
|
|
683
|
+
},
|
|
684
|
+
required: ["action", "tool"],
|
|
685
|
+
};
|
|
686
|
+
const PUBLIC_STATE_PROPERTIES = {
|
|
687
|
+
state: { type: "string", enum: ["ready", "missing", "thin", "stale", "complete", "failed"] },
|
|
688
|
+
message: { type: "string" },
|
|
689
|
+
next: NEXT_ACTION_SCHEMA,
|
|
690
|
+
};
|
|
223
691
|
const VOICE_OUTPUT_SCHEMA = {
|
|
224
692
|
type: "object",
|
|
225
693
|
properties: {
|
|
694
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
695
|
+
status: { type: "string", enum: ["ready", "setup_required"] },
|
|
696
|
+
message: { type: "string" },
|
|
226
697
|
id: { type: "string" },
|
|
227
698
|
name: { type: ["string", "null"] },
|
|
228
699
|
isDefault: { type: "boolean" },
|
|
@@ -235,58 +706,330 @@ const VOICE_OUTPUT_SCHEMA = {
|
|
|
235
706
|
taboos: { type: "array", items: { type: "string" } },
|
|
236
707
|
positions: { type: "array", items: { type: "string" }, description: "Real takes the user has expressed; never invent new ones." },
|
|
237
708
|
evidence: EVIDENCE_OUTPUT_SCHEMA,
|
|
238
|
-
register: { type: "string" },
|
|
239
|
-
approximate: { type: "boolean" },
|
|
240
709
|
},
|
|
241
710
|
};
|
|
242
711
|
const SCORE_OUTPUT_SCHEMA = {
|
|
243
712
|
type: "object",
|
|
244
713
|
properties: {
|
|
714
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
245
715
|
voiceMatch: { type: ["number", "null"], description: "0–100; higher = closer to the user's own writing. Relative, not a forensic identity claim." },
|
|
246
716
|
measured: { type: "boolean", description: "False when the candidate or corpus is in the unscored band, or the scorer is unavailable." },
|
|
247
717
|
...PROVENANCE_OUTPUT_PROPERTIES,
|
|
718
|
+
register: { type: "string", description: "Calibration register the anchors come from; the absolute is approximate outside it." },
|
|
719
|
+
approximate: { type: "boolean" },
|
|
248
720
|
},
|
|
249
721
|
};
|
|
722
|
+
// A draft or rewrite is the writing, the profile that wrote it and its evidence state.
|
|
723
|
+
// The score cluster (voiceMatch, measured, deltas, candidate fields, crossFit, the
|
|
724
|
+
// calibration constants) was constant on every call and is gone from the account API
|
|
725
|
+
// and from here (IDI-381). Voice Match is measured on score_voice.
|
|
726
|
+
// IDI-522 step 2: present only for a soft language-policy pass through the profile-less
|
|
727
|
+
// shared engine — mirrors app/api/mcp/schemas.ts's `caveatsOutputShape` so this stdio
|
|
728
|
+
// schema stays in step with every other writing-tool surface.
|
|
729
|
+
const CAVEATS_OUTPUT_PROPERTY = { caveats: { type: "array", items: { type: "string", enum: ["language"] } } };
|
|
250
730
|
const REWRITE_OUTPUT_SCHEMA = {
|
|
251
731
|
type: "object",
|
|
252
732
|
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,
|
|
733
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
734
|
+
rewritten: { type: "string", description: "The rewritten text." },
|
|
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,
|
|
264
740
|
},
|
|
265
741
|
};
|
|
266
742
|
const DRAFT_OUTPUT_SCHEMA = {
|
|
267
743
|
type: "object",
|
|
268
744
|
properties: {
|
|
745
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
269
746
|
draft: { type: "string", description: "New prose drafted in the user's voice." },
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
...
|
|
747
|
+
reason: { type: ["string", "null"], description: "Present only when scoring is enabled and the engine did not measure; says why." },
|
|
748
|
+
voice: SELECTED_VOICE_SCHEMA,
|
|
749
|
+
evidence: EVIDENCE_OUTPUT_SCHEMA,
|
|
750
|
+
...CREDIT_GENERATION_OUTPUT_PROPERTIES,
|
|
751
|
+
...CAVEATS_OUTPUT_PROPERTY,
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
const CREATE_VOICE_OUTPUT_SCHEMA = {
|
|
755
|
+
type: "object",
|
|
756
|
+
properties: {
|
|
757
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
758
|
+
status: { type: "string", enum: ["ready", "more_evidence_required", "build_in_progress", "failed"] },
|
|
759
|
+
message: { type: "string" },
|
|
760
|
+
reason: { type: ["string", "null"] },
|
|
761
|
+
voiceId: { type: ["string", "null"] },
|
|
762
|
+
neededChars: { type: "number", description: "Present only when more writing is needed." },
|
|
763
|
+
neededBlocks: { type: "number", description: "Present only when more writing is needed." },
|
|
764
|
+
replay: { type: "boolean" },
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
const FEEDBACK_OUTPUT_SCHEMA = {
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: {
|
|
770
|
+
...PUBLIC_STATE_PROPERTIES,
|
|
771
|
+
needsFinalText: { type: "boolean" },
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
const PROFILE_REF_OPTIONAL_SCHEMA = {
|
|
775
|
+
type: "string", minLength: 1,
|
|
776
|
+
description: "The Writing Profile's id, or its exact name. Omit to use the current profile.",
|
|
777
|
+
};
|
|
778
|
+
const PROFILE_REF_REQUIRED_SCHEMA = {
|
|
779
|
+
type: "string", minLength: 1,
|
|
780
|
+
description: "The Writing Profile's id, or its exact name.",
|
|
781
|
+
};
|
|
782
|
+
const PROFILE_ID_SCHEMA = { type: "string", format: "uuid" };
|
|
783
|
+
const PROFILE_SAMPLE_SCHEMA = {
|
|
784
|
+
type: "array",
|
|
785
|
+
minItems: 1,
|
|
786
|
+
maxItems: 500,
|
|
787
|
+
items: { type: "string", maxLength: 20000 },
|
|
788
|
+
};
|
|
789
|
+
const OPTIONAL_PROFILE_SAMPLE_SCHEMA = {
|
|
790
|
+
type: "array",
|
|
791
|
+
maxItems: 500,
|
|
792
|
+
items: { type: "string", maxLength: 20000 },
|
|
793
|
+
};
|
|
794
|
+
const PROFILE_SAMPLE_STATE_SCHEMA = {
|
|
795
|
+
type: "object",
|
|
796
|
+
properties: {
|
|
797
|
+
profileId: PROFILE_ID_SCHEMA,
|
|
798
|
+
generation: { type: "integer", minimum: 0 },
|
|
799
|
+
samples: {
|
|
800
|
+
type: "array",
|
|
801
|
+
items: {
|
|
802
|
+
type: "object",
|
|
803
|
+
properties: {
|
|
804
|
+
id: PROFILE_ID_SCHEMA,
|
|
805
|
+
text: { type: "string" },
|
|
806
|
+
addedAt: { type: "string" },
|
|
807
|
+
},
|
|
808
|
+
required: ["id", "text", "addedAt"],
|
|
809
|
+
additionalProperties: false,
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
needsWriting: { type: "boolean" },
|
|
813
|
+
heldNotUsable: { type: "integer", minimum: 0 },
|
|
274
814
|
},
|
|
815
|
+
required: ["profileId", "generation", "samples", "needsWriting", "heldNotUsable"],
|
|
816
|
+
additionalProperties: false,
|
|
817
|
+
};
|
|
818
|
+
/** What the caller must change to succeed; present only on evidence failures. */
|
|
819
|
+
const PROFILE_SHORTFALL_SCHEMA = {
|
|
820
|
+
type: "object",
|
|
821
|
+
properties: {
|
|
822
|
+
reason: { type: "string", enum: ["insufficient_input", "unsupported_language"] },
|
|
823
|
+
message: { type: "string" },
|
|
824
|
+
neededChars: { type: "integer", minimum: 0 },
|
|
825
|
+
neededBlocks: { type: "integer", minimum: 0 },
|
|
826
|
+
droppedBlocks: { type: "integer", minimum: 0 },
|
|
827
|
+
minimumChars: { type: "integer", minimum: 0 },
|
|
828
|
+
minimumBlocks: { type: "integer", minimum: 0 },
|
|
829
|
+
},
|
|
830
|
+
required: ["message"],
|
|
831
|
+
additionalProperties: false,
|
|
275
832
|
};
|
|
276
833
|
|
|
277
|
-
|
|
834
|
+
function profileActionOutput(value, codes) {
|
|
835
|
+
return {
|
|
836
|
+
type: "object",
|
|
837
|
+
properties: {
|
|
838
|
+
ok: { type: "boolean" },
|
|
839
|
+
value,
|
|
840
|
+
error: {
|
|
841
|
+
type: "object",
|
|
842
|
+
properties: {
|
|
843
|
+
code: { type: "string", enum: [...codes, "unexpected_error"] },
|
|
844
|
+
detail: PROFILE_SHORTFALL_SCHEMA,
|
|
845
|
+
},
|
|
846
|
+
required: ["code"],
|
|
847
|
+
additionalProperties: false,
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
required: ["ok"],
|
|
851
|
+
additionalProperties: false,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const accountToolDefinitions = [
|
|
278
856
|
{
|
|
279
857
|
name: "get_my_voice",
|
|
280
858
|
description:
|
|
281
|
-
"Use this when
|
|
859
|
+
"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
860
|
inputSchema: { type: "object", properties: {} },
|
|
283
861
|
outputSchema: VOICE_OUTPUT_SCHEMA,
|
|
284
862
|
annotations: { readOnlyHint: true, openWorldHint: false, destructiveHint: false, title: "Load Idiolect AI Voice" },
|
|
285
863
|
},
|
|
864
|
+
{
|
|
865
|
+
name: "create_my_voice",
|
|
866
|
+
description:
|
|
867
|
+
"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.",
|
|
868
|
+
inputSchema: {
|
|
869
|
+
type: "object",
|
|
870
|
+
properties: {
|
|
871
|
+
profileId: {
|
|
872
|
+
...PROFILE_ID_SCHEMA,
|
|
873
|
+
description:
|
|
874
|
+
"Exact owned Writing Profile to receive this evidence. Omit only when creating the account's first profile or adding to the current profile.",
|
|
875
|
+
},
|
|
876
|
+
samples: {
|
|
877
|
+
type: "array",
|
|
878
|
+
maxItems: 500,
|
|
879
|
+
items: { type: "string", maxLength: 20000 },
|
|
880
|
+
description: "Distinct blocks of writing supplied or explicitly approved by the user.",
|
|
881
|
+
},
|
|
882
|
+
answers: {
|
|
883
|
+
type: "array",
|
|
884
|
+
maxItems: 20,
|
|
885
|
+
items: {
|
|
886
|
+
type: "object",
|
|
887
|
+
properties: {
|
|
888
|
+
question: { type: "string", maxLength: 500 },
|
|
889
|
+
answer: { type: "string", maxLength: 5000 },
|
|
890
|
+
},
|
|
891
|
+
required: ["question", "answer"],
|
|
892
|
+
additionalProperties: false,
|
|
893
|
+
},
|
|
894
|
+
description: "Optional guided questions and the user's own answers.",
|
|
895
|
+
},
|
|
896
|
+
name: { type: "string", maxLength: 60, description: "Optional private name for the Writing Profile." },
|
|
897
|
+
},
|
|
898
|
+
additionalProperties: false,
|
|
899
|
+
},
|
|
900
|
+
outputSchema: CREATE_VOICE_OUTPUT_SCHEMA,
|
|
901
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Create My Writing Profile" },
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
name: "list_writing_profiles",
|
|
905
|
+
description: "List the authenticated user's Writing Profiles and identify the current profile.",
|
|
906
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
907
|
+
outputSchema: profileActionOutput({ type: "array" }, []),
|
|
908
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "List Writing Profiles" },
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
name: "list_writing_profile_samples",
|
|
912
|
+
description: "List the writing samples and stable sample IDs held by the exact Writing Profile identified by profileId. This does not change the profile.",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
type: "object",
|
|
915
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA },
|
|
916
|
+
required: ["profileId"],
|
|
917
|
+
additionalProperties: false,
|
|
918
|
+
},
|
|
919
|
+
outputSchema: profileActionOutput(PROFILE_SAMPLE_STATE_SCHEMA, ["profile_not_found"]),
|
|
920
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "List Writing Profile Samples" },
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
name: "create_writing_profile",
|
|
924
|
+
description: "Create a new Writing Profile from approved writing. This never changes an existing profile.",
|
|
925
|
+
inputSchema: {
|
|
926
|
+
type: "object",
|
|
927
|
+
properties: {
|
|
928
|
+
name: { type: "string", maxLength: 60 },
|
|
929
|
+
samples: OPTIONAL_PROFILE_SAMPLE_SCHEMA,
|
|
930
|
+
answers: {
|
|
931
|
+
type: "array",
|
|
932
|
+
maxItems: 20,
|
|
933
|
+
items: {
|
|
934
|
+
type: "object",
|
|
935
|
+
properties: {
|
|
936
|
+
question: { type: "string", maxLength: 500 },
|
|
937
|
+
answer: { type: "string", maxLength: 5000 },
|
|
938
|
+
},
|
|
939
|
+
required: ["question", "answer"],
|
|
940
|
+
additionalProperties: false,
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
additionalProperties: false,
|
|
945
|
+
},
|
|
946
|
+
outputSchema: profileActionOutput({ type: "object" }, [
|
|
947
|
+
"insufficient_evidence", "rate_limited", "request_conflict", "build_in_progress",
|
|
948
|
+
]),
|
|
949
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "Create Writing Profile" },
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: "add_samples_to_writing_profile",
|
|
953
|
+
description: "Add approved writing samples to the exact Writing Profile identified by profileId.",
|
|
954
|
+
inputSchema: {
|
|
955
|
+
type: "object",
|
|
956
|
+
properties: { profileId: PROFILE_REF_OPTIONAL_SCHEMA, samples: PROFILE_SAMPLE_SCHEMA },
|
|
957
|
+
required: ["samples"],
|
|
958
|
+
additionalProperties: false,
|
|
959
|
+
},
|
|
960
|
+
outputSchema: profileActionOutput({ type: "object" }, [
|
|
961
|
+
"insufficient_evidence", "request_conflict", "build_in_progress", "profile_not_found",
|
|
962
|
+
]),
|
|
963
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "Add Samples to Writing Profile" },
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: "replace_samples_in_writing_profile",
|
|
967
|
+
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.",
|
|
968
|
+
inputSchema: {
|
|
969
|
+
type: "object",
|
|
970
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA, samples: OPTIONAL_PROFILE_SAMPLE_SCHEMA },
|
|
971
|
+
required: ["profileId", "samples"],
|
|
972
|
+
additionalProperties: false,
|
|
973
|
+
},
|
|
974
|
+
outputSchema: profileActionOutput(PROFILE_SAMPLE_STATE_SCHEMA, ["profile_not_found"]),
|
|
975
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true, title: "Replace Samples in Writing Profile" },
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
name: "remove_sample_from_writing_profile",
|
|
979
|
+
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.",
|
|
980
|
+
inputSchema: {
|
|
981
|
+
type: "object",
|
|
982
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA, sampleId: PROFILE_ID_SCHEMA },
|
|
983
|
+
required: ["profileId", "sampleId"],
|
|
984
|
+
additionalProperties: false,
|
|
985
|
+
},
|
|
986
|
+
outputSchema: profileActionOutput(PROFILE_SAMPLE_STATE_SCHEMA, ["profile_not_found", "sample_not_found"]),
|
|
987
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true, title: "Remove Sample from Writing Profile" },
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "set_current_writing_profile",
|
|
991
|
+
description: "Make the exact Writing Profile identified by profileId current for future writing.",
|
|
992
|
+
inputSchema: {
|
|
993
|
+
type: "object",
|
|
994
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA },
|
|
995
|
+
required: ["profileId"],
|
|
996
|
+
additionalProperties: false,
|
|
997
|
+
},
|
|
998
|
+
outputSchema: profileActionOutput({ type: "object" }, ["profile_not_found"]),
|
|
999
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false, title: "Set Current Writing Profile" },
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
name: "delete_writing_profile",
|
|
1003
|
+
description: "Delete the exact Writing Profile identified by profileId. A current profile may require a replacementProfileId.",
|
|
1004
|
+
inputSchema: {
|
|
1005
|
+
type: "object",
|
|
1006
|
+
properties: { profileId: PROFILE_REF_REQUIRED_SCHEMA, replacementProfileId: PROFILE_REF_REQUIRED_SCHEMA },
|
|
1007
|
+
required: ["profileId"],
|
|
1008
|
+
additionalProperties: false,
|
|
1009
|
+
},
|
|
1010
|
+
outputSchema: profileActionOutput({ type: "object" }, [
|
|
1011
|
+
"profile_not_found", "replacement_required", "invalid_replacement",
|
|
1012
|
+
]),
|
|
1013
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true, title: "Delete Writing Profile" },
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: "get_credit_status",
|
|
1017
|
+
description:
|
|
1018
|
+
"Check the account's current generation-credit state. During the free beta, this confirms that no credits are being used.",
|
|
1019
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1020
|
+
outputSchema: CREDIT_OUTPUT_SCHEMA,
|
|
1021
|
+
annotations: {
|
|
1022
|
+
readOnlyHint: true,
|
|
1023
|
+
idempotentHint: true,
|
|
1024
|
+
openWorldHint: false,
|
|
1025
|
+
destructiveHint: false,
|
|
1026
|
+
title: "Check Credit Status",
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
286
1029
|
{
|
|
287
1030
|
name: "score_voice",
|
|
288
1031
|
description:
|
|
289
|
-
"Use this when
|
|
1032
|
+
"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
1033
|
inputSchema: {
|
|
291
1034
|
type: "object",
|
|
292
1035
|
properties: { text: { type: "string", description: "Text to score against the user's voice." } },
|
|
@@ -298,7 +1041,7 @@ const accountTools = [
|
|
|
298
1041
|
{
|
|
299
1042
|
name: "rewrite_in_voice",
|
|
300
1043
|
description:
|
|
301
|
-
"Rewrite supplied prose in the user's voice. Returns an in-voice alternative with
|
|
1044
|
+
"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
1045
|
inputSchema: {
|
|
303
1046
|
type: "object",
|
|
304
1047
|
properties: {
|
|
@@ -311,6 +1054,18 @@ const accountTools = [
|
|
|
311
1054
|
description:
|
|
312
1055
|
"Optional target register. Adapts the output format while keeping the user's voice; omitted, the input's own format is kept.",
|
|
313
1056
|
},
|
|
1057
|
+
// The ask, kept out of `text`. /api/v1/rewrite has accepted this all along;
|
|
1058
|
+
// only the tool schema was missing it.
|
|
1059
|
+
instructions: {
|
|
1060
|
+
type: "string",
|
|
1061
|
+
description:
|
|
1062
|
+
"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.",
|
|
1063
|
+
},
|
|
1064
|
+
changeScope: CHANGE_SCOPE_SCHEMA,
|
|
1065
|
+
effects: EFFECTS_SCHEMA,
|
|
1066
|
+
sourceAuthor: SOURCE_AUTHOR_SCHEMA,
|
|
1067
|
+
calibration: CALIBRATION_SCHEMA,
|
|
1068
|
+
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
1069
|
},
|
|
315
1070
|
required: ["text"],
|
|
316
1071
|
},
|
|
@@ -320,31 +1075,496 @@ const accountTools = [
|
|
|
320
1075
|
{
|
|
321
1076
|
name: "draft_in_voice",
|
|
322
1077
|
description:
|
|
323
|
-
"Draft new prose in the user's voice from a supplied brief
|
|
1078
|
+
"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
1079
|
inputSchema: {
|
|
325
1080
|
type: "object",
|
|
326
1081
|
properties: {
|
|
327
|
-
brief: {
|
|
1082
|
+
brief: {
|
|
1083
|
+
type: "string",
|
|
1084
|
+
description:
|
|
1085
|
+
"`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`.",
|
|
1086
|
+
},
|
|
328
1087
|
format: {
|
|
329
1088
|
type: "string",
|
|
330
1089
|
enum: ["linkedin", "x_post", "x_thread", "newsletter", "generic"],
|
|
331
|
-
description: "Optional output format (default: generic).",
|
|
1090
|
+
description: "Optional output format (default: `generic`).",
|
|
1091
|
+
},
|
|
1092
|
+
// The direction, kept out of `brief`. /api/v1/draft accepts it; this schema is the
|
|
1093
|
+
// stdio package's own copy because it cannot import lib/connectors.ts. The bounds
|
|
1094
|
+
// mirror GENERATION_INPUT_LIMITS.draftInstructions, including the nonempty minimum
|
|
1095
|
+
// the remote catalog advertises — an optional field that accepts "" is a third
|
|
1096
|
+
// state neither side means.
|
|
1097
|
+
instructions: {
|
|
1098
|
+
type: "string",
|
|
1099
|
+
minLength: 1,
|
|
1100
|
+
maxLength: 8000,
|
|
1101
|
+
description:
|
|
1102
|
+
"`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`.",
|
|
332
1103
|
},
|
|
1104
|
+
effects: EFFECTS_SCHEMA,
|
|
1105
|
+
briefAuthor: BRIEF_AUTHOR_SCHEMA,
|
|
1106
|
+
calibration: CALIBRATION_SCHEMA,
|
|
1107
|
+
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
1108
|
},
|
|
334
1109
|
required: ["brief"],
|
|
335
1110
|
},
|
|
336
1111
|
outputSchema: DRAFT_OUTPUT_SCHEMA,
|
|
337
1112
|
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Draft In Voice" },
|
|
338
1113
|
},
|
|
1114
|
+
{
|
|
1115
|
+
name: "learn_writing_style",
|
|
1116
|
+
description:
|
|
1117
|
+
"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.",
|
|
1118
|
+
inputSchema: {
|
|
1119
|
+
type: "object",
|
|
1120
|
+
additionalProperties: false,
|
|
1121
|
+
properties: {
|
|
1122
|
+
kind: {
|
|
1123
|
+
type: "string",
|
|
1124
|
+
enum: ["authored_example", "preference", "observation"],
|
|
1125
|
+
description: "Whether this is the user's authored writing, a preference they stated, or an observation grounded in their writing.",
|
|
1126
|
+
},
|
|
1127
|
+
text: { type: "string", minLength: 1, maxLength: 12000 },
|
|
1128
|
+
consent: {
|
|
1129
|
+
type: "boolean",
|
|
1130
|
+
const: true,
|
|
1131
|
+
description: "Required only for authored_example: the user wrote it and agreed it may be stored.",
|
|
1132
|
+
},
|
|
1133
|
+
basis: { type: "string", minLength: 1, maxLength: 2000 },
|
|
1134
|
+
scope: { type: "string", enum: ["general", "occasion", "piece"] },
|
|
1135
|
+
occasion: { type: "string", minLength: 1, maxLength: 500 },
|
|
1136
|
+
profileId: { type: "string", format: "uuid" },
|
|
1137
|
+
writingJobVersionId: { type: "string", format: "uuid" },
|
|
1138
|
+
},
|
|
1139
|
+
required: ["kind", "text"],
|
|
1140
|
+
},
|
|
1141
|
+
outputSchema: {
|
|
1142
|
+
type: "object",
|
|
1143
|
+
additionalProperties: false,
|
|
1144
|
+
properties: {
|
|
1145
|
+
status: { type: "string", enum: ["saved", "pending", "ready", "failed"] },
|
|
1146
|
+
message: { type: "string" },
|
|
1147
|
+
next: {
|
|
1148
|
+
type: "object",
|
|
1149
|
+
additionalProperties: false,
|
|
1150
|
+
properties: {
|
|
1151
|
+
action: { type: "string", enum: ["done", "retry", "continue_writing"] },
|
|
1152
|
+
tool: { type: ["string", "null"], enum: ["refine", null] },
|
|
1153
|
+
},
|
|
1154
|
+
required: ["action", "tool"],
|
|
1155
|
+
},
|
|
1156
|
+
contribution: {
|
|
1157
|
+
type: "object",
|
|
1158
|
+
additionalProperties: false,
|
|
1159
|
+
properties: {
|
|
1160
|
+
id: { type: "string", format: "uuid" },
|
|
1161
|
+
kind: { type: "string", enum: ["authored_example", "preference", "observation"] },
|
|
1162
|
+
status: { type: "string", enum: ["pending", "waiting", "applied", "not_used", "piece_only"] },
|
|
1163
|
+
},
|
|
1164
|
+
required: ["id", "kind", "status"],
|
|
1165
|
+
},
|
|
1166
|
+
profileId: { type: "string", format: "uuid" },
|
|
1167
|
+
writingJobVersionId: { type: "string", format: "uuid" },
|
|
1168
|
+
neededWriting: {
|
|
1169
|
+
type: "object",
|
|
1170
|
+
additionalProperties: false,
|
|
1171
|
+
properties: {
|
|
1172
|
+
pieces: { type: "integer", minimum: 0 },
|
|
1173
|
+
characters: { type: "integer", minimum: 0 },
|
|
1174
|
+
},
|
|
1175
|
+
required: ["pieces", "characters"],
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
required: ["status", "message", "next"],
|
|
1179
|
+
},
|
|
1180
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Learn Writing Style" },
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
name: "record_voice_feedback",
|
|
1184
|
+
description:
|
|
1185
|
+
"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.",
|
|
1186
|
+
inputSchema: {
|
|
1187
|
+
type: "object",
|
|
1188
|
+
additionalProperties: false,
|
|
1189
|
+
properties: {
|
|
1190
|
+
outputId: { type: "string", format: "uuid" },
|
|
1191
|
+
// Hand-written mirror of the HTTP route's vocabulary — this file cannot import
|
|
1192
|
+
// `lib/feedback/outcomes.ts`. `superseded` means the person changed the brief or
|
|
1193
|
+
// direction rather than judging the text (IDI-446); keep it in step by hand.
|
|
1194
|
+
status: { type: "string", enum: ["accepted", "corrected_and_accepted", "rejected", "superseded"] },
|
|
1195
|
+
correctionText: {
|
|
1196
|
+
type: "string",
|
|
1197
|
+
minLength: 1,
|
|
1198
|
+
maxLength: 20000,
|
|
1199
|
+
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.",
|
|
1200
|
+
},
|
|
1201
|
+
critique: {
|
|
1202
|
+
type: "string",
|
|
1203
|
+
minLength: 1,
|
|
1204
|
+
maxLength: 2000,
|
|
1205
|
+
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.",
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
required: ["outputId", "status"],
|
|
1209
|
+
},
|
|
1210
|
+
outputSchema: FEEDBACK_OUTPUT_SCHEMA,
|
|
1211
|
+
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false, title: "Record Voice Feedback" },
|
|
1212
|
+
},
|
|
339
1213
|
];
|
|
340
1214
|
|
|
1215
|
+
export const accountTools = progressiveCatalog.tools;
|
|
1216
|
+
export const accountInstructions = progressiveCatalog.instructions;
|
|
1217
|
+
/** Hidden schemas retained for cached stdio callers and compatibility tests. */
|
|
1218
|
+
export const accountCompatibilityTools = accountToolDefinitions;
|
|
1219
|
+
|
|
1220
|
+
function toolError(message) {
|
|
1221
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function accountToolError(error) {
|
|
1225
|
+
if (error instanceof AccountRequestError && error.structuredContent) {
|
|
1226
|
+
return {
|
|
1227
|
+
content: [{ type: "text", text: error.structuredContent.message }],
|
|
1228
|
+
structuredContent: error.structuredContent,
|
|
1229
|
+
isError: true,
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
if (error instanceof AccountRequestError) return toolError(error.message);
|
|
1233
|
+
return toolError("Idiolect AI account API failed before a safe response was available.");
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function createVoiceBody(args) {
|
|
1237
|
+
const samples = args.samples ?? [];
|
|
1238
|
+
const answers = args.answers ?? [];
|
|
1239
|
+
if (!Array.isArray(samples) || samples.length > 500 || samples.some((sample) => typeof sample !== "string" || sample.length > 20000)) {
|
|
1240
|
+
return { error: "create_my_voice: `samples` must contain at most 500 text blocks of at most 20,000 characters each." };
|
|
1241
|
+
}
|
|
1242
|
+
if (
|
|
1243
|
+
!Array.isArray(answers)
|
|
1244
|
+
|| answers.length > 20
|
|
1245
|
+
|| answers.some((answer) => (
|
|
1246
|
+
!answer
|
|
1247
|
+
|| typeof answer !== "object"
|
|
1248
|
+
|| Array.isArray(answer)
|
|
1249
|
+
|| typeof answer.question !== "string"
|
|
1250
|
+
|| answer.question.length > 500
|
|
1251
|
+
|| typeof answer.answer !== "string"
|
|
1252
|
+
|| answer.answer.length > 5000
|
|
1253
|
+
))
|
|
1254
|
+
) {
|
|
1255
|
+
return { error: "create_my_voice: `answers` must contain at most 20 question/answer pairs within the documented limits." };
|
|
1256
|
+
}
|
|
1257
|
+
if (args.name !== undefined && (typeof args.name !== "string" || args.name.length > 60)) {
|
|
1258
|
+
return { error: "create_my_voice: `name` must be text of at most 60 characters." };
|
|
1259
|
+
}
|
|
1260
|
+
if (args.profileId !== undefined && typeof args.profileId !== "string") {
|
|
1261
|
+
return { error: "create_my_voice: `profileId` must be a Writing Profile UUID." };
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
body: {
|
|
1265
|
+
...(args.profileId !== undefined ? { profileId: args.profileId } : {}),
|
|
1266
|
+
samples,
|
|
1267
|
+
answers: answers.map(({ question, answer }) => ({ question, answer })),
|
|
1268
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function safeCreationResult(r) {
|
|
1274
|
+
const status = r.status ?? "more_evidence_required";
|
|
1275
|
+
const state = status === "ready" ? "complete" : status === "build_in_progress" ? "stale" : status === "failed" ? "failed" : "thin";
|
|
1276
|
+
const next = status === "ready"
|
|
1277
|
+
? { action: "reload_voice", tool: "get_my_voice" }
|
|
1278
|
+
: status === "build_in_progress"
|
|
1279
|
+
? { action: "retry", tool: "create_my_voice" }
|
|
1280
|
+
: status === "failed" ? { action: "done", tool: null } : { action: "collect_evidence", tool: "create_my_voice" };
|
|
1281
|
+
const neededChars = Number.isFinite(r.neededChars) ? r.neededChars : 0;
|
|
1282
|
+
const neededBlocks = Number.isFinite(r.neededBlocks) ? r.neededBlocks : 0;
|
|
1283
|
+
const message = state === "thin"
|
|
1284
|
+
? `More of the user's own writing is needed: add ${neededBlocks} distinct block${neededBlocks === 1 ? "" : "s"} and ${neededChars} characters.`
|
|
1285
|
+
: r.message ?? (state === "complete" ? "Writing Profile ready." : "Writing Profile creation is still in progress.");
|
|
1286
|
+
return publicMcpState(state, message, next, {
|
|
1287
|
+
status,
|
|
1288
|
+
reason: r.reason ?? null,
|
|
1289
|
+
voiceId: r.voiceId ?? null,
|
|
1290
|
+
// The shortfall appears only when there is one; the fixed minimums are gone (IDI-381).
|
|
1291
|
+
...(neededChars > 0 || neededBlocks > 0 ? { neededChars, neededBlocks } : {}),
|
|
1292
|
+
replay: r.replay === true,
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** Preserve a server-proven refinement conflict without copying unknown fields. */
|
|
1297
|
+
function safeCreationFailurePayload(value) {
|
|
1298
|
+
if (
|
|
1299
|
+
!plainObject(value)
|
|
1300
|
+
|| value.status !== "failed"
|
|
1301
|
+
|| typeof value.message !== "string"
|
|
1302
|
+
|| !(value.reason === null || typeof value.reason === "string")
|
|
1303
|
+
|| !(value.voiceId === null || typeof value.voiceId === "string")
|
|
1304
|
+
|| typeof value.replay !== "boolean"
|
|
1305
|
+
) return null;
|
|
1306
|
+
return safeCreationResult(value);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const WRITING_PROFILE_ERROR_CODES = {
|
|
1310
|
+
list_writing_profiles: new Set(["unexpected_error"]),
|
|
1311
|
+
list_writing_profile_samples: new Set(["profile_not_found", "unexpected_error"]),
|
|
1312
|
+
create_writing_profile: new Set([
|
|
1313
|
+
"insufficient_evidence", "rate_limited", "request_conflict", "build_in_progress", "unexpected_error",
|
|
1314
|
+
]),
|
|
1315
|
+
add_samples_to_writing_profile: new Set([
|
|
1316
|
+
"insufficient_evidence", "request_conflict", "build_in_progress", "profile_not_found", "unexpected_error",
|
|
1317
|
+
]),
|
|
1318
|
+
replace_samples_in_writing_profile: new Set([
|
|
1319
|
+
"insufficient_evidence", "profile_not_found", "samples_not_fully_visible", "unexpected_error",
|
|
1320
|
+
]),
|
|
1321
|
+
remove_sample_from_writing_profile: new Set([
|
|
1322
|
+
"profile_not_found", "sample_not_found", "unexpected_error",
|
|
1323
|
+
]),
|
|
1324
|
+
set_current_writing_profile: new Set(["profile_not_found", "unexpected_error"]),
|
|
1325
|
+
delete_writing_profile: new Set([
|
|
1326
|
+
"profile_not_found", "replacement_required", "invalid_replacement", "unexpected_error",
|
|
1327
|
+
]),
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
function writingProfileRequest(name, args) {
|
|
1331
|
+
if (name === "list_writing_profiles") return { path: "/api/writing-profiles", method: "GET" };
|
|
1332
|
+
if (name === "create_writing_profile") {
|
|
1333
|
+
return {
|
|
1334
|
+
path: "/api/writing-profiles",
|
|
1335
|
+
method: "POST",
|
|
1336
|
+
body: {
|
|
1337
|
+
samples: args.samples ?? [],
|
|
1338
|
+
answers: args.answers ?? [],
|
|
1339
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const id = encodeURIComponent(args.profileId ?? '');
|
|
1344
|
+
if (name === "list_writing_profile_samples") {
|
|
1345
|
+
return { path: `/api/writing-profiles/${id}/samples`, method: "GET" };
|
|
1346
|
+
}
|
|
1347
|
+
if (name === "add_samples_to_writing_profile") {
|
|
1348
|
+
// Absent means the current profile; the account API resolves it, this only forwards.
|
|
1349
|
+
const path = args.profileId === undefined
|
|
1350
|
+
? "/api/writing-profiles/current/samples"
|
|
1351
|
+
: `/api/writing-profiles/${id}/samples`;
|
|
1352
|
+
return { path, method: "POST", body: { samples: args.samples } };
|
|
1353
|
+
}
|
|
1354
|
+
if (name === "replace_samples_in_writing_profile") {
|
|
1355
|
+
return { path: `/api/writing-profiles/${id}/samples`, method: "PUT", body: { samples: args.samples } };
|
|
1356
|
+
}
|
|
1357
|
+
if (name === "remove_sample_from_writing_profile") {
|
|
1358
|
+
const sampleId = encodeURIComponent(args.sampleId);
|
|
1359
|
+
return { path: `/api/writing-profiles/${id}/samples/${sampleId}`, method: "DELETE" };
|
|
1360
|
+
}
|
|
1361
|
+
if (name === "set_current_writing_profile") {
|
|
1362
|
+
return { path: `/api/writing-profiles/${id}/current`, method: "PUT" };
|
|
1363
|
+
}
|
|
1364
|
+
const replacement = args.replacementProfileId === undefined
|
|
1365
|
+
? ""
|
|
1366
|
+
: `?replacementProfileId=${encodeURIComponent(args.replacementProfileId)}`;
|
|
1367
|
+
return { path: `/api/writing-profiles/${id}${replacement}`, method: "DELETE" };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/** The shortfall is re-validated field by field: an unchecked passthrough would let the
|
|
1371
|
+
* account API widen this tool's declared output schema without the stdio server knowing. */
|
|
1372
|
+
function safeProfileShortfall(value) {
|
|
1373
|
+
if (!plainObject(value) || typeof value.message !== "string") return null;
|
|
1374
|
+
const detail = { message: value.message.slice(0, 2000) };
|
|
1375
|
+
if (value.reason === "insufficient_input" || value.reason === "unsupported_language") {
|
|
1376
|
+
detail.reason = value.reason;
|
|
1377
|
+
}
|
|
1378
|
+
// Counts travel only when the evidence gate produced them; a profile that could not be
|
|
1379
|
+
// found has no shortfall to count.
|
|
1380
|
+
for (const key of ["neededChars", "neededBlocks", "droppedBlocks", "minimumChars", "minimumBlocks"]) {
|
|
1381
|
+
if (Number.isInteger(value[key]) && value[key] >= 0) detail[key] = value[key];
|
|
1382
|
+
}
|
|
1383
|
+
return detail;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function safeWritingProfileResult(name, value) {
|
|
1387
|
+
if (!plainObject(value) || typeof value.ok !== "boolean") return null;
|
|
1388
|
+
if (value.ok === true && Object.hasOwn(value, "value")) return { ok: true, value: value.value };
|
|
1389
|
+
const code = plainObject(value.error) ? value.error.code : null;
|
|
1390
|
+
if (typeof code !== "string" || !WRITING_PROFILE_ERROR_CODES[name]?.has(code)) return null;
|
|
1391
|
+
const detail = safeProfileShortfall(plainObject(value.error) ? value.error.detail : null);
|
|
1392
|
+
return { ok: false, error: detail ? { code, detail } : { code } };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function writingProfileSuccessMessage(name, value, title) {
|
|
1396
|
+
if (name === "list_writing_profiles" && Array.isArray(value)) {
|
|
1397
|
+
if (value.length === 0) return "No Writing Profiles found.";
|
|
1398
|
+
return ["Writing Profiles:", ...value.map((profile) => {
|
|
1399
|
+
const label = typeof profile?.name === "string" && profile.name.length > 0
|
|
1400
|
+
? profile.name
|
|
1401
|
+
: "Unnamed profile";
|
|
1402
|
+
const current = profile?.isCurrent === true ? " (current)" : "";
|
|
1403
|
+
return `- ${label}: ${String(profile?.id ?? "unknown")}${current}`;
|
|
1404
|
+
})].join("\n");
|
|
1405
|
+
}
|
|
1406
|
+
if (
|
|
1407
|
+
["list_writing_profile_samples", "replace_samples_in_writing_profile", "remove_sample_from_writing_profile"].includes(name)
|
|
1408
|
+
&& plainObject(value)
|
|
1409
|
+
&& Array.isArray(value.samples)
|
|
1410
|
+
) {
|
|
1411
|
+
const samples = value.samples.map((sample) => {
|
|
1412
|
+
const id = plainObject(sample) && typeof sample.id === "string" ? sample.id : "unknown";
|
|
1413
|
+
const writing = plainObject(sample) && typeof sample.text === "string" ? sample.text : "";
|
|
1414
|
+
return `- sampleId ${id}: ${JSON.stringify(writing)}`;
|
|
1415
|
+
});
|
|
1416
|
+
return [
|
|
1417
|
+
`${title} complete for profile ${String(value.profileId ?? "unknown")} (generation ${String(value.generation ?? "unknown")}).`,
|
|
1418
|
+
`Usable samples: ${value.samples.length}.`,
|
|
1419
|
+
...(samples.length > 0 ? samples : ["- No usable writing samples remain."]),
|
|
1420
|
+
...(Number.isInteger(value.heldNotUsable) && value.heldNotUsable > 0
|
|
1421
|
+
? [`Held but not usable for rebuild: ${value.heldNotUsable}.`]
|
|
1422
|
+
: []),
|
|
1423
|
+
].join("\n");
|
|
1424
|
+
}
|
|
1425
|
+
return `${title} complete.`;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async function callWritingProfileApi(name, args) {
|
|
1429
|
+
const request = writingProfileRequest(name, args);
|
|
1430
|
+
const res = await fetch(`${ACCOUNT_BASE}${request.path}`, {
|
|
1431
|
+
method: request.method,
|
|
1432
|
+
headers: {
|
|
1433
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
1434
|
+
...(request.body ? { "content-type": "application/json" } : {}),
|
|
1435
|
+
},
|
|
1436
|
+
body: request.body ? JSON.stringify(request.body) : undefined,
|
|
1437
|
+
});
|
|
1438
|
+
let parsed = null;
|
|
1439
|
+
try {
|
|
1440
|
+
parsed = safeWritingProfileResult(name, await res.json());
|
|
1441
|
+
} catch {
|
|
1442
|
+
// The bounded failure below handles non-JSON and malformed responses.
|
|
1443
|
+
}
|
|
1444
|
+
if (!parsed) throw new AccountRequestError(`${name}: Idiolect AI account API failed (${res.status}).`, res.status);
|
|
1445
|
+
const title = accountToolDefinitions.find((tool) => tool.name === name)?.annotations.title ?? name;
|
|
1446
|
+
const building = name === "create_writing_profile"
|
|
1447
|
+
&& parsed.ok === false
|
|
1448
|
+
&& parsed.error.code === "build_in_progress";
|
|
1449
|
+
const message = parsed.ok
|
|
1450
|
+
? writingProfileSuccessMessage(name, parsed.value, title)
|
|
1451
|
+
: building
|
|
1452
|
+
? "Writing Profile accepted and building. Retry this same call shortly to get the completed profile."
|
|
1453
|
+
: parsed.error.detail?.message ?? `${title} failed: ${parsed.error.code}.`;
|
|
1454
|
+
return {
|
|
1455
|
+
content: [{ type: "text", text: message }],
|
|
1456
|
+
structuredContent: parsed,
|
|
1457
|
+
...(!parsed.ok && !building ? { isError: true } : {}),
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/** Import-safe account handler used by the stdio server and its parity tests. */
|
|
1462
|
+
export async function handleAccountTool(name, args = {}) {
|
|
1463
|
+
if (Object.hasOwn(WRITING_PROFILE_ERROR_CODES, name)) {
|
|
1464
|
+
try {
|
|
1465
|
+
return await callWritingProfileApi(name, args);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
return accountToolError(error);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (name === "get_credit_status") {
|
|
1471
|
+
try {
|
|
1472
|
+
const credits = safeCreditPresentation(await accountRequest("/api/v1/credits", {
|
|
1473
|
+
toolName: "get_credit_status",
|
|
1474
|
+
}));
|
|
1475
|
+
if (!credits) return toolError("get_credit_status: Idiolect AI returned an invalid credit status.");
|
|
1476
|
+
return {
|
|
1477
|
+
content: [{ type: "text", text: credits.message }],
|
|
1478
|
+
structuredContent: credits,
|
|
1479
|
+
};
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
return accountToolError(error);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
if (name === "get_my_voice") {
|
|
1486
|
+
try {
|
|
1487
|
+
const r = await accountRequest("/api/v1/voice", { toolName: "get_my_voice" });
|
|
1488
|
+
const card = r.card || formatValue(r.voice);
|
|
1489
|
+
return {
|
|
1490
|
+
content: [
|
|
1491
|
+
{
|
|
1492
|
+
type: "text",
|
|
1493
|
+
text: `${card}\n\n---\n(Idiolect AI account voice loaded. Write everything as this person from now on.)`,
|
|
1494
|
+
},
|
|
1495
|
+
],
|
|
1496
|
+
structuredContent: publicMcpState("ready", "Writing Profile ready.", { action: "continue_writing", tool: null }, {
|
|
1497
|
+
status: "ready",
|
|
1498
|
+
...(r.voice || {}),
|
|
1499
|
+
}),
|
|
1500
|
+
};
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
if (error instanceof AccountRequestError && error.status === 404) {
|
|
1503
|
+
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.";
|
|
1504
|
+
return {
|
|
1505
|
+
content: [{ type: "text", text: message }],
|
|
1506
|
+
structuredContent: publicMcpState("missing", message, { action: "collect_evidence", tool: "create_my_voice" }, {
|
|
1507
|
+
status: "setup_required",
|
|
1508
|
+
}),
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
return accountToolError(error);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (name === "create_my_voice") {
|
|
1516
|
+
const prepared = createVoiceBody(args);
|
|
1517
|
+
if (prepared.error) return {
|
|
1518
|
+
content: [{ type: "text", text: prepared.error }],
|
|
1519
|
+
structuredContent: publicMcpState("failed", prepared.error, { action: "retry", tool: "create_my_voice" }),
|
|
1520
|
+
isError: true,
|
|
1521
|
+
};
|
|
1522
|
+
try {
|
|
1523
|
+
const r = await accountRequest("/api/v1/voice/create", {
|
|
1524
|
+
method: "POST",
|
|
1525
|
+
body: prepared.body,
|
|
1526
|
+
toolName: "create_my_voice",
|
|
1527
|
+
});
|
|
1528
|
+
const structuredContent = safeCreationResult(r);
|
|
1529
|
+
return {
|
|
1530
|
+
content: [{ type: "text", text: structuredContent.message }],
|
|
1531
|
+
structuredContent,
|
|
1532
|
+
};
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
return accountToolError(error);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (name === "record_voice_feedback" || name === "learn_writing_style") {
|
|
1539
|
+
try {
|
|
1540
|
+
return await accountMcpRequest(name, args);
|
|
1541
|
+
} catch {
|
|
1542
|
+
const learning = name === "learn_writing_style";
|
|
1543
|
+
const message = learning
|
|
1544
|
+
? "Writing style could not be saved. Retry once or finish."
|
|
1545
|
+
: "Feedback could not be recorded. Retry once or finish.";
|
|
1546
|
+
return {
|
|
1547
|
+
content: [{ type: "text", text: message }],
|
|
1548
|
+
...(learning
|
|
1549
|
+
? { structuredContent: { status: "failed", message, next: { action: "retry", tool: null } } }
|
|
1550
|
+
: { structuredContent: publicMcpState("failed", message, { action: "retry", tool: "record_voice_feedback" }) }),
|
|
1551
|
+
isError: true,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return undefined;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
341
1560
|
const server = new Server(
|
|
342
|
-
{ name: "idiolect", version:
|
|
1561
|
+
{ name: "idiolect", version: PACKAGE_VERSION },
|
|
343
1562
|
{
|
|
344
1563
|
capabilities: { tools: {} },
|
|
345
|
-
// Hint folded into the client's context (MCP spec)
|
|
346
|
-
instructions:
|
|
347
|
-
|
|
1564
|
+
// Hint folded into the client's context (MCP spec).
|
|
1565
|
+
instructions: ACCOUNT_MODE
|
|
1566
|
+
? accountInstructions
|
|
1567
|
+
: "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
1568
|
},
|
|
349
1569
|
);
|
|
350
1570
|
|
|
@@ -352,27 +1572,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
352
1572
|
tools: ACCOUNT_MODE ? accountTools : legacyTools,
|
|
353
1573
|
}));
|
|
354
1574
|
|
|
355
|
-
|
|
1575
|
+
export async function handleToolRequest(req) {
|
|
356
1576
|
if (ACCOUNT_MODE) {
|
|
357
1577
|
const args = req.params.arguments || {};
|
|
358
|
-
|
|
359
|
-
|
|
1578
|
+
if (
|
|
1579
|
+
PROGRESSIVE_ACCOUNT_TOOL_NAMES.has(req.params.name) ||
|
|
1580
|
+
REMOTE_COMPATIBILITY_TOOL_NAMES.has(req.params.name)
|
|
1581
|
+
) {
|
|
360
1582
|
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 };
|
|
1583
|
+
return await accountMcpRequest(req.params.name, args);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
return accountToolError(error);
|
|
374
1586
|
}
|
|
375
1587
|
}
|
|
1588
|
+
const nativeAccountResult = await handleAccountTool(req.params.name, args);
|
|
1589
|
+
if (nativeAccountResult) return nativeAccountResult;
|
|
376
1590
|
|
|
377
1591
|
if (req.params.name === "score_voice") {
|
|
378
1592
|
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
@@ -387,26 +1601,38 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
387
1601
|
});
|
|
388
1602
|
return {
|
|
389
1603
|
content: [{ type: "text", text: scoreSummary(r) }],
|
|
390
|
-
structuredContent: {
|
|
391
|
-
voiceMatch: r.voiceMatch ?? null,
|
|
1604
|
+
structuredContent: publicMcpState("complete", "Voice Match check complete.", { action: "continue_writing", tool: null }, {
|
|
1605
|
+
voiceMatch: r.absoluteScoreReportable === false ? null : r.voiceMatch ?? null,
|
|
392
1606
|
measured: !!r.measured,
|
|
393
1607
|
validated: !!r.validated,
|
|
394
1608
|
instrument: r.instrument ?? null,
|
|
395
1609
|
reason: r.reason ?? null,
|
|
396
|
-
register: r.register
|
|
397
|
-
approximate:
|
|
1610
|
+
...(typeof r.register === "string" ? { register: r.register } : {}),
|
|
1611
|
+
...(typeof r.approximate === "boolean" ? { approximate: r.approximate } : {}),
|
|
398
1612
|
...(r.voice ? { voice: r.voice } : {}),
|
|
399
1613
|
...(r.evidence ? { evidence: r.evidence } : {}),
|
|
400
|
-
},
|
|
1614
|
+
}),
|
|
401
1615
|
};
|
|
402
1616
|
} catch (e) {
|
|
403
|
-
return
|
|
1617
|
+
return accountToolError(e);
|
|
404
1618
|
}
|
|
405
1619
|
}
|
|
406
1620
|
|
|
407
1621
|
if (req.params.name === "rewrite_in_voice") {
|
|
408
1622
|
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
1623
|
+
const profileId = typeof args.profileId === "string" ? args.profileId : undefined;
|
|
409
1624
|
const targetRegister = typeof args.targetRegister === "string" ? args.targetRegister : undefined;
|
|
1625
|
+
const instructions = typeof args.instructions === "string" && args.instructions.trim()
|
|
1626
|
+
? args.instructions.trim()
|
|
1627
|
+
: undefined;
|
|
1628
|
+
const invalidChangeRequest = changeRequestError(args, "rewrite_in_voice");
|
|
1629
|
+
if (invalidChangeRequest) return invalidChangeRequest;
|
|
1630
|
+
const invalidSourceAuthor = authorProvenanceError(args, "rewrite_in_voice", "sourceAuthor");
|
|
1631
|
+
if (invalidSourceAuthor) return invalidSourceAuthor;
|
|
1632
|
+
const changeScope = args.changeScope === "polish" ? "light_edit" : args.changeScope;
|
|
1633
|
+
const effects = args.effects;
|
|
1634
|
+
const sourceAuthor = args.sourceAuthor;
|
|
1635
|
+
const calibration = args.calibration;
|
|
410
1636
|
if (!text) {
|
|
411
1637
|
return { content: [{ type: "text", text: "rewrite_in_voice: provide `text` to rewrite." }], isError: true };
|
|
412
1638
|
}
|
|
@@ -415,7 +1641,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
415
1641
|
content: [
|
|
416
1642
|
{
|
|
417
1643
|
type: "text",
|
|
418
|
-
text: `rewrite_in_voice: \`targetRegister\` must be one of ${REWRITE_REGISTERS
|
|
1644
|
+
text: `rewrite_in_voice: \`targetRegister\` must be one of ${backtickedValues(REWRITE_REGISTERS)}.`,
|
|
419
1645
|
},
|
|
420
1646
|
],
|
|
421
1647
|
isError: true,
|
|
@@ -424,45 +1650,51 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
424
1650
|
try {
|
|
425
1651
|
const r = await accountRequest("/api/v1/rewrite", {
|
|
426
1652
|
method: "POST",
|
|
427
|
-
body: {
|
|
1653
|
+
body: {
|
|
1654
|
+
text,
|
|
1655
|
+
...(profileId !== undefined ? { voice: profileId } : {}),
|
|
1656
|
+
...(targetRegister ? { targetRegister } : {}),
|
|
1657
|
+
...(instructions ? { instructions } : {}),
|
|
1658
|
+
...(changeScope !== undefined ? { changeScope } : {}),
|
|
1659
|
+
...(effects !== undefined ? { effects } : {}),
|
|
1660
|
+
...(sourceAuthor !== undefined ? { sourceAuthor } : {}),
|
|
1661
|
+
...(calibration !== undefined ? { calibration } : {}),
|
|
1662
|
+
},
|
|
428
1663
|
toolName: "rewrite_in_voice",
|
|
429
1664
|
});
|
|
1665
|
+
// The account API returns the rewrite, the profile and its evidence state. The
|
|
1666
|
+
// score block this used to print and the constants it used to coerce into the
|
|
1667
|
+
// structured result are gone (IDI-381).
|
|
430
1668
|
return {
|
|
431
|
-
content: [
|
|
432
|
-
|
|
433
|
-
type: "text",
|
|
434
|
-
text: `Rewritten:\n${r.rewritten || ""}\n\n${scoreSummary(r)}${formatAlternatives(r.alternatives)}`,
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
structuredContent: {
|
|
1669
|
+
content: [{ type: "text", text: `Rewritten:\n${r.rewritten || ""}` }],
|
|
1670
|
+
structuredContent: publicMcpState("complete", "Rewrite complete.", { action: "continue_writing", tool: null }, {
|
|
438
1671
|
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,
|
|
1672
|
+
...(r.reason !== undefined ? { reason: r.reason ?? null } : {}),
|
|
453
1673
|
...(r.voice ? { voice: r.voice } : {}),
|
|
454
1674
|
...(r.evidence ? { evidence: r.evidence } : {}),
|
|
455
|
-
},
|
|
1675
|
+
}),
|
|
456
1676
|
};
|
|
457
1677
|
} catch (e) {
|
|
458
|
-
return
|
|
1678
|
+
return accountToolError(e);
|
|
459
1679
|
}
|
|
460
1680
|
}
|
|
461
1681
|
|
|
462
1682
|
if (req.params.name === "draft_in_voice") {
|
|
463
1683
|
const brief = typeof args.brief === "string" ? args.brief.trim() : "";
|
|
1684
|
+
const profileId = typeof args.profileId === "string" ? args.profileId : undefined;
|
|
464
1685
|
const format = typeof args.format === "string" ? args.format : undefined;
|
|
1686
|
+
const instructions = typeof args.instructions === "string" && args.instructions.trim()
|
|
1687
|
+
? args.instructions.trim()
|
|
1688
|
+
: undefined;
|
|
465
1689
|
const formats = new Set(["linkedin", "x_post", "x_thread", "newsletter", "generic"]);
|
|
1690
|
+
const invalidChangeRequest = changeRequestError(args, "draft_in_voice");
|
|
1691
|
+
if (invalidChangeRequest) return invalidChangeRequest;
|
|
1692
|
+
const invalidBriefAuthor = authorProvenanceError(args, "draft_in_voice", "briefAuthor");
|
|
1693
|
+
if (invalidBriefAuthor) return invalidBriefAuthor;
|
|
1694
|
+
const changeScope = args.changeScope === "polish" ? "light_edit" : args.changeScope;
|
|
1695
|
+
const effects = args.effects;
|
|
1696
|
+
const briefAuthor = args.briefAuthor;
|
|
1697
|
+
const calibration = args.calibration;
|
|
466
1698
|
if (!brief) {
|
|
467
1699
|
return { content: [{ type: "text", text: "draft_in_voice: provide `brief` to draft from." }], isError: true };
|
|
468
1700
|
}
|
|
@@ -471,7 +1703,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
471
1703
|
content: [
|
|
472
1704
|
{
|
|
473
1705
|
type: "text",
|
|
474
|
-
text:
|
|
1706
|
+
text: `draft_in_voice: \`format\` must be one of ${backtickedValues([...formats])}.`,
|
|
475
1707
|
},
|
|
476
1708
|
],
|
|
477
1709
|
isError: true,
|
|
@@ -480,32 +1712,29 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
480
1712
|
try {
|
|
481
1713
|
const r = await accountRequest("/api/v1/draft", {
|
|
482
1714
|
method: "POST",
|
|
483
|
-
body: {
|
|
1715
|
+
body: {
|
|
1716
|
+
brief,
|
|
1717
|
+
...(profileId !== undefined ? { voice: profileId } : {}),
|
|
1718
|
+
...(format ? { format } : {}),
|
|
1719
|
+
...(instructions ? { instructions } : {}),
|
|
1720
|
+
...(changeScope !== undefined ? { changeScope } : {}),
|
|
1721
|
+
...(effects !== undefined ? { effects } : {}),
|
|
1722
|
+
...(briefAuthor !== undefined ? { briefAuthor } : {}),
|
|
1723
|
+
...(calibration !== undefined ? { calibration } : {}),
|
|
1724
|
+
},
|
|
484
1725
|
toolName: "draft_in_voice",
|
|
485
1726
|
});
|
|
486
1727
|
return {
|
|
487
|
-
content: [
|
|
488
|
-
|
|
489
|
-
type: "text",
|
|
490
|
-
text: `Draft:\n${r.draft || ""}\n\n${scoreSummary(r)}${formatAlternatives(r.alternatives)}`,
|
|
491
|
-
},
|
|
492
|
-
],
|
|
493
|
-
structuredContent: {
|
|
1728
|
+
content: [{ type: "text", text: `Draft:\n${r.draft || ""}` }],
|
|
1729
|
+
structuredContent: publicMcpState("complete", "Draft complete.", { action: "continue_writing", tool: null }, {
|
|
494
1730
|
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,
|
|
1731
|
+
...(r.reason !== undefined ? { reason: r.reason ?? null } : {}),
|
|
503
1732
|
...(r.voice ? { voice: r.voice } : {}),
|
|
504
1733
|
...(r.evidence ? { evidence: r.evidence } : {}),
|
|
505
|
-
},
|
|
1734
|
+
}),
|
|
506
1735
|
};
|
|
507
1736
|
} catch (e) {
|
|
508
|
-
return
|
|
1737
|
+
return accountToolError(e);
|
|
509
1738
|
}
|
|
510
1739
|
}
|
|
511
1740
|
|
|
@@ -533,14 +1762,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
533
1762
|
}
|
|
534
1763
|
try {
|
|
535
1764
|
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
1765
|
return {
|
|
540
1766
|
content: [
|
|
541
1767
|
{
|
|
542
1768
|
type: "text",
|
|
543
|
-
text:
|
|
1769
|
+
text: formatLegacyScoreVoiceResult(r),
|
|
544
1770
|
},
|
|
545
1771
|
],
|
|
546
1772
|
};
|
|
@@ -577,7 +1803,20 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
577
1803
|
} catch (e) {
|
|
578
1804
|
return { content: [{ type: "text", text: String(e) }], isError: true };
|
|
579
1805
|
}
|
|
580
|
-
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
server.setRequestHandler(CallToolRequestSchema, handleToolRequest);
|
|
581
1809
|
|
|
582
|
-
|
|
583
|
-
|
|
1810
|
+
function isMainModule() {
|
|
1811
|
+
if (!process.argv[1]) return false;
|
|
1812
|
+
try {
|
|
1813
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
1814
|
+
} catch {
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
if (isMainModule()) {
|
|
1820
|
+
const transport = new StdioServerTransport();
|
|
1821
|
+
await server.connect(transport);
|
|
1822
|
+
}
|