@rubytech/create-maxy-code 0.1.174 → 0.1.177
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +13 -4
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +49 -10
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +18 -16
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts +11 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js +95 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +98 -15
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +3 -2
- package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +1 -0
- package/payload/platform/scripts/setup-account.sh +8 -0
- package/payload/platform/scripts/verify-skill-tool-surface.sh +27 -8
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +1 -1
- package/payload/premium-plugins/writer-craft/PLUGIN.md +11 -7
- package/payload/premium-plugins/writer-craft/hooks/__tests__/voice-session-end-text.test.sh +284 -0
- package/payload/premium-plugins/writer-craft/hooks/voice-session-end-text.sh +75 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.d.ts +3 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.js +58 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js +95 -18
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts +47 -6
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js +69 -6
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts +8 -2
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +82 -53
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.d.ts +23 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js +191 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts +6 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js +14 -6
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +12 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +23 -12
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts +3 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js +33 -5
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/scripts/smoke.mjs +289 -28
- package/payload/premium-plugins/writer-craft/mcp/src/cli/ingest-session-text.ts +65 -0
- package/payload/premium-plugins/writer-craft/mcp/src/index.ts +108 -18
- package/payload/premium-plugins/writer-craft/mcp/src/lib/voice-corpus.ts +78 -6
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +115 -56
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-ingest-session-text.ts +229 -0
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-record-feedback.ts +20 -6
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +38 -13
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-tag-content.ts +42 -5
- package/payload/premium-plugins/writer-craft/skills/voice-mirror/SKILL.md +65 -31
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI wrapper for voice-ingest-session-text.
|
|
4
|
+
*
|
|
5
|
+
* Invoked by the SessionEnd hook after each admin PTY session:
|
|
6
|
+
* node dist/cli/ingest-session-text.js <transcript_path>
|
|
7
|
+
*
|
|
8
|
+
* All other parameters are read from environment variables inherited from
|
|
9
|
+
* the PTY session (which inherits from the session manager's systemd unit):
|
|
10
|
+
*
|
|
11
|
+
* ACCOUNT_ID — operator's account identifier
|
|
12
|
+
* USER_ID — operator's userId (= adminUserId)
|
|
13
|
+
* SESSION_ID — session identifier (parsed by hook from stdin)
|
|
14
|
+
* NEO4J_URI — neo4j bolt URI
|
|
15
|
+
* NEO4J_USER — neo4j username
|
|
16
|
+
* NEO4J_PASSWORD — neo4j password
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 — success (includes no-new-turns case)
|
|
20
|
+
* 1 — error (missing required env, JSONL not found, DB error)
|
|
21
|
+
*
|
|
22
|
+
* stdout: one-line result summary ("turns=N skipped=M") for the hook to
|
|
23
|
+
* capture and forward to the log-ingest endpoint.
|
|
24
|
+
* stderr: detailed diagnostics (tool log lines).
|
|
25
|
+
*/
|
|
26
|
+
import { voiceIngestSessionText } from "../tools/voice-ingest-session-text.js";
|
|
27
|
+
import { closeDriver } from "../lib/neo4j.js";
|
|
28
|
+
|
|
29
|
+
const transcriptPath = process.argv[2];
|
|
30
|
+
if (!transcriptPath) {
|
|
31
|
+
process.stderr.write("[ingest-session-text] error: transcript path required as argv[2]\n");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const accountId = process.env.ACCOUNT_ID ?? "";
|
|
36
|
+
const userId = process.env.USER_ID ?? "";
|
|
37
|
+
const sessionId = process.env.SESSION_ID ?? undefined;
|
|
38
|
+
|
|
39
|
+
if (!accountId || !userId) {
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
`[ingest-session-text] error: ACCOUNT_ID and USER_ID must be set in env (accountId=${accountId || "empty"} userId=${userId || "empty"})\n`,
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result = await voiceIngestSessionText({
|
|
48
|
+
sessionJsonlPath: transcriptPath,
|
|
49
|
+
accountId,
|
|
50
|
+
userId,
|
|
51
|
+
sessionId,
|
|
52
|
+
});
|
|
53
|
+
// Emit one-line summary to stdout for the hook to capture.
|
|
54
|
+
process.stdout.write(
|
|
55
|
+
`turns=${result.ingestedCount} skipped=${result.skippedCount} sessionId=${result.sessionId ?? "unknown"}\n`,
|
|
56
|
+
);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`[ingest-session-text] error: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
} finally {
|
|
64
|
+
await closeDriver();
|
|
65
|
+
}
|
|
@@ -8,6 +8,8 @@ import { voiceTagContent, AUTHORSHIP_MODES, type AuthorshipMode } from "./tools/
|
|
|
8
8
|
import { voiceDistilProfile } from "./tools/voice-distil-profile.js";
|
|
9
9
|
import { voiceRetrieveConditioning } from "./tools/voice-retrieve-conditioning.js";
|
|
10
10
|
import { voiceRecordFeedback } from "./tools/voice-record-feedback.js";
|
|
11
|
+
import { voiceIngestSessionText } from "./tools/voice-ingest-session-text.js";
|
|
12
|
+
import { FORMAT_VALUES } from "./lib/voice-corpus.js";
|
|
11
13
|
import { closeDriver } from "./lib/neo4j.js";
|
|
12
14
|
|
|
13
15
|
const server = new McpServer({
|
|
@@ -46,9 +48,14 @@ function resolveUserId(): string | null {
|
|
|
46
48
|
return userIdEnv;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// Format enum schema — shared across tools that accept VoiceFormat.
|
|
52
|
+
const zFormat = z.enum([...FORMAT_VALUES] as [string, ...string[]]).describe(
|
|
53
|
+
"Writing format: text | email | social-post | article | marketing-copy | note. One :VoiceProfile per (accountId, userId, format).",
|
|
54
|
+
);
|
|
55
|
+
|
|
49
56
|
server.tool(
|
|
50
57
|
"voice-tag-content",
|
|
51
|
-
"Stamp authorshipMode on one or many content nodes (:KnowledgeDocument | :Message | :SocialPost) the operator owns. Email threads live as :KnowledgeDocument {source:'email'} since Task 321. Valid modes: human-only, human-led-agent-assisted, agent-led-human-reviewed, agent-only, unknown.",
|
|
58
|
+
"Stamp authorshipMode (and optionally format) on one or many content nodes (:KnowledgeDocument | :Message | :SocialPost) the operator owns. Email threads live as :KnowledgeDocument {source:'email'} since Task 321. Valid modes: human-only, human-led-agent-assisted, agent-led-human-reviewed, agent-only, unknown. Valid formats: text, email, social-post, article, marketing-copy, note. When format is omitted, it is derived per-node from labels and source property.",
|
|
52
59
|
{
|
|
53
60
|
nodeIds: z.array(z.string()).min(1).describe("Element IDs of nodes to tag"),
|
|
54
61
|
mode: z
|
|
@@ -60,20 +67,24 @@ server.tool(
|
|
|
60
67
|
"unknown",
|
|
61
68
|
])
|
|
62
69
|
.describe("Authorship mode to apply"),
|
|
70
|
+
format: zFormat.optional().describe(
|
|
71
|
+
"Writing format. When omitted, derived from each node's labels and source property.",
|
|
72
|
+
),
|
|
63
73
|
},
|
|
64
|
-
async ({ nodeIds, mode }) => {
|
|
74
|
+
async ({ nodeIds, mode, format }) => {
|
|
65
75
|
if (!accountId) return refuseNoAccount("voice-tag-content");
|
|
66
76
|
try {
|
|
67
77
|
const result = await voiceTagContent({
|
|
68
78
|
nodeIds,
|
|
69
79
|
mode: mode as AuthorshipMode,
|
|
70
80
|
accountId,
|
|
81
|
+
format: format as import("./lib/voice-corpus.js").VoiceFormat | undefined,
|
|
71
82
|
});
|
|
72
83
|
return {
|
|
73
84
|
content: [
|
|
74
85
|
{
|
|
75
86
|
type: "text" as const,
|
|
76
|
-
text: `voice-tag: mode=${mode} updated=${result.updated} skipped=${result.skipped}`,
|
|
87
|
+
text: `voice-tag: mode=${mode} format=${format ?? "derived"} updated=${result.updated} skipped=${result.skipped}`,
|
|
77
88
|
},
|
|
78
89
|
],
|
|
79
90
|
};
|
|
@@ -93,7 +104,7 @@ server.tool(
|
|
|
93
104
|
|
|
94
105
|
server.tool(
|
|
95
106
|
"voice-distil-profile",
|
|
96
|
-
"Two-mode tool (Task 424). mode='sample' (default) walks the operator's human-only corpus, returns
|
|
107
|
+
"Two-mode tool (Task 424). mode='sample' (default) walks the operator's per-format human-only corpus, returns exemplars + recent edit intents for the agent to read. mode='write' persists the YAML style card the agent composed. Cadence-guarded: skips in 'write' mode without writing if corpus didn't grow ≥20% AND <30 days passed (pass force=true to override). When format is omitted, enumerates all formats in the corpus and distils each.",
|
|
97
108
|
{
|
|
98
109
|
userId: z
|
|
99
110
|
.string()
|
|
@@ -101,6 +112,9 @@ server.tool(
|
|
|
101
112
|
.describe(
|
|
102
113
|
"Operator AdminUser identifier. Defaults to ADMIN_USER_ID env from spawn context.",
|
|
103
114
|
),
|
|
115
|
+
format: zFormat.optional().describe(
|
|
116
|
+
"Writing format to distil. When omitted, enumerates corpus formats and distils each that fires the cadence guard.",
|
|
117
|
+
),
|
|
104
118
|
force: z
|
|
105
119
|
.boolean()
|
|
106
120
|
.optional()
|
|
@@ -114,7 +128,7 @@ server.tool(
|
|
|
114
128
|
.optional()
|
|
115
129
|
.describe("Required for mode='write'. The YAML style card the agent composed from the sample-mode exemplars + feedback intents."),
|
|
116
130
|
},
|
|
117
|
-
async ({ userId, force, mode, styleCardYaml }) => {
|
|
131
|
+
async ({ userId, format, force, mode, styleCardYaml }) => {
|
|
118
132
|
if (!accountId) return refuseNoAccount("voice-distil-profile");
|
|
119
133
|
const resolvedUserId = userId ?? resolveUserId();
|
|
120
134
|
if (!resolvedUserId) {
|
|
@@ -133,15 +147,31 @@ server.tool(
|
|
|
133
147
|
const result = await voiceDistilProfile({
|
|
134
148
|
accountId,
|
|
135
149
|
userId: resolvedUserId,
|
|
150
|
+
format: format as import("./lib/voice-corpus.js").VoiceFormat | undefined,
|
|
136
151
|
force: force === true,
|
|
137
152
|
mode,
|
|
138
153
|
styleCardYaml,
|
|
139
154
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
|
|
156
|
+
// Summarise single-format or multi-format result.
|
|
157
|
+
const summariseOne = (r: import("./tools/voice-distil-profile.js").VoiceDistilProfileResult, fmt?: string): string => {
|
|
158
|
+
const tag = fmt ? ` format=${fmt}` : "";
|
|
159
|
+
if (r.skipped && r.exemplars) {
|
|
160
|
+
return `voice-distil sample${tag}: corpusSize=${r.corpusSize} exemplars=${r.exemplars.length} feedbackEntries=${r.feedbackEntries}\n${JSON.stringify({ exemplars: r.exemplars, feedbackIntents: r.feedbackIntents }, null, 2)}`;
|
|
161
|
+
}
|
|
162
|
+
if (r.skipped) {
|
|
163
|
+
return `voice-distil${tag}: skipped reason=${r.skipReason} corpusSize=${r.corpusSize}`;
|
|
164
|
+
}
|
|
165
|
+
return `voice-distil${tag}: ok profileId=${r.profileId} corpusSize=${r.corpusSize} feedbackEntries=${r.feedbackEntries} generatedAt=${r.generatedAt}`;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
let summary: string;
|
|
169
|
+
if (Array.isArray(result)) {
|
|
170
|
+
summary = result.map((r, i) => summariseOne(r)).join("\n---\n");
|
|
171
|
+
} else {
|
|
172
|
+
summary = summariseOne(result, format);
|
|
173
|
+
}
|
|
174
|
+
|
|
145
175
|
return { content: [{ type: "text" as const, text: summary }] };
|
|
146
176
|
} catch (err) {
|
|
147
177
|
return {
|
|
@@ -159,23 +189,25 @@ server.tool(
|
|
|
159
189
|
|
|
160
190
|
server.tool(
|
|
161
191
|
"voice-retrieve-conditioning",
|
|
162
|
-
"Return the operator's style card and K voice-matched exemplars for a drafting brief. K=5 short-form, K=15 long-form. Token-budget bounded. Returns empty {styleCard:null,exemplars:[]} when no profile exists.",
|
|
192
|
+
"Return the operator's per-format style card and K voice-matched exemplars for a drafting brief. K=5 short-form (length='short'), K=15 long-form (length='long'). Token-budget bounded. format is required (the writing type: text|email|social-post|article|marketing-copy|note). Returns empty {styleCard:null,exemplars:[]} when no profile exists.",
|
|
163
193
|
{
|
|
164
194
|
userId: z
|
|
165
195
|
.string()
|
|
166
196
|
.optional()
|
|
167
197
|
.describe("Operator AdminUser identifier. Defaults to ADMIN_USER_ID env."),
|
|
198
|
+
format: zFormat.describe("Writing format for corpus and profile scoping."),
|
|
168
199
|
topic: z
|
|
169
200
|
.string()
|
|
170
201
|
.optional()
|
|
171
202
|
.describe("Drafting topic keyword (filters exemplars to relevant ones)"),
|
|
172
|
-
|
|
203
|
+
length: z
|
|
173
204
|
.enum(["short", "long"])
|
|
174
|
-
.
|
|
205
|
+
.optional()
|
|
206
|
+
.describe("K-selector: short = K=5 (16k tokens, email/post); long = K=15 (96k tokens, brochure/prospectus). Defaults to short."),
|
|
175
207
|
register: z.string().optional().describe("Optional register hint"),
|
|
176
208
|
tokenBudget: z.number().int().positive().optional(),
|
|
177
209
|
},
|
|
178
|
-
async ({ userId, topic,
|
|
210
|
+
async ({ userId, format, topic, length, register, tokenBudget }) => {
|
|
179
211
|
if (!accountId) return refuseNoAccount("voice-retrieve-conditioning");
|
|
180
212
|
const resolvedUserId = userId ?? resolveUserId();
|
|
181
213
|
if (!resolvedUserId) {
|
|
@@ -192,7 +224,12 @@ server.tool(
|
|
|
192
224
|
const result = await voiceRetrieveConditioning({
|
|
193
225
|
accountId,
|
|
194
226
|
userId: resolvedUserId,
|
|
195
|
-
brief: {
|
|
227
|
+
brief: {
|
|
228
|
+
format: format as import("./lib/voice-corpus.js").VoiceFormat,
|
|
229
|
+
topic,
|
|
230
|
+
length: length as "short" | "long" | undefined,
|
|
231
|
+
register,
|
|
232
|
+
},
|
|
196
233
|
tokenBudget,
|
|
197
234
|
});
|
|
198
235
|
return {
|
|
@@ -220,9 +257,10 @@ server.tool(
|
|
|
220
257
|
|
|
221
258
|
server.tool(
|
|
222
259
|
"voice-record-feedback",
|
|
223
|
-
"Capture an operator's edit on an agent draft. Writes :VoiceEdit linked to :VoiceProfile via :FEEDBACK_FOR. The calling agent supplies `intent` (one sentence describing what
|
|
260
|
+
"Capture an operator's edit on an agent draft. Writes :VoiceEdit linked to the per-format :VoiceProfile via :FEEDBACK_FOR. The calling agent supplies `intent` (one sentence describing what changed and what voice preference it reveals — Task 424) and `format` (the writing format of the draft).",
|
|
224
261
|
{
|
|
225
262
|
userId: z.string().optional(),
|
|
263
|
+
format: zFormat.describe("Writing format of the draft being edited."),
|
|
226
264
|
originalText: z.string().min(1),
|
|
227
265
|
editedText: z.string().min(1),
|
|
228
266
|
intent: z
|
|
@@ -232,7 +270,7 @@ server.tool(
|
|
|
232
270
|
skill: z.string().optional().describe("Calling skill name, for provenance"),
|
|
233
271
|
brief: z.string().optional().describe("Drafting brief that produced the original"),
|
|
234
272
|
},
|
|
235
|
-
async ({ userId, originalText, editedText, intent, skill, brief }) => {
|
|
273
|
+
async ({ userId, format, originalText, editedText, intent, skill, brief }) => {
|
|
236
274
|
if (!accountId) return refuseNoAccount("voice-record-feedback");
|
|
237
275
|
const resolvedUserId = userId ?? resolveUserId();
|
|
238
276
|
if (!resolvedUserId) {
|
|
@@ -250,6 +288,7 @@ server.tool(
|
|
|
250
288
|
const result = await voiceRecordFeedback({
|
|
251
289
|
accountId,
|
|
252
290
|
userId: resolvedUserId,
|
|
291
|
+
format: format as import("./lib/voice-corpus.js").VoiceFormat,
|
|
253
292
|
originalText,
|
|
254
293
|
editedText,
|
|
255
294
|
intent,
|
|
@@ -259,7 +298,7 @@ server.tool(
|
|
|
259
298
|
content: [
|
|
260
299
|
{
|
|
261
300
|
type: "text" as const,
|
|
262
|
-
text: `voice-record-feedback: editId=${result.editId} intent="${result.intent}" hadProfile=${result.hadProfile}`,
|
|
301
|
+
text: `voice-record-feedback: editId=${result.editId} format=${format} intent="${result.intent}" hadProfile=${result.hadProfile}`,
|
|
263
302
|
},
|
|
264
303
|
],
|
|
265
304
|
};
|
|
@@ -277,6 +316,57 @@ server.tool(
|
|
|
277
316
|
},
|
|
278
317
|
);
|
|
279
318
|
|
|
319
|
+
server.tool(
|
|
320
|
+
"voice-ingest-session-text",
|
|
321
|
+
"Ingest operator PTY turns from a Claude Code session JSONL as :Message {format:'text', source:'admin-pty', authorshipMode:'human-only'} corpus nodes. Deduplication via SHA256 contentHash — re-running against the same JSONL is a no-op. Triggers voice-distil-profile {format:'text'} after ingest (cadence guard decides whether distillation runs). Called automatically by the SessionEnd hook; can also be invoked manually.",
|
|
322
|
+
{
|
|
323
|
+
sessionJsonlPath: z.string().min(1).describe("Absolute path to the Claude Code session JSONL file"),
|
|
324
|
+
sessionId: z.string().optional().describe("Session ID for provenance; read from the SessionEnd envelope"),
|
|
325
|
+
userId: z.string().optional().describe("Operator AdminUser identifier. Defaults to ADMIN_USER_ID env."),
|
|
326
|
+
},
|
|
327
|
+
async ({ sessionJsonlPath, sessionId, userId }) => {
|
|
328
|
+
if (!accountId) return refuseNoAccount("voice-ingest-session-text");
|
|
329
|
+
const resolvedUserId = userId ?? resolveUserId();
|
|
330
|
+
if (!resolvedUserId) {
|
|
331
|
+
return {
|
|
332
|
+
content: [
|
|
333
|
+
{
|
|
334
|
+
type: "text" as const,
|
|
335
|
+
text: "voice-ingest-session-text: userId required (no ADMIN_USER_ID in spawn env).",
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
isError: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const result = await voiceIngestSessionText({
|
|
343
|
+
sessionJsonlPath,
|
|
344
|
+
accountId,
|
|
345
|
+
userId: resolvedUserId,
|
|
346
|
+
sessionId,
|
|
347
|
+
});
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "text" as const,
|
|
352
|
+
text: `voice-ingest-session-text: ingestedCount=${result.ingestedCount} skippedCount=${result.skippedCount} sessionId=${result.sessionId ?? "unknown"}`,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
} catch (err) {
|
|
357
|
+
return {
|
|
358
|
+
content: [
|
|
359
|
+
{
|
|
360
|
+
type: "text" as const,
|
|
361
|
+
text: `voice-ingest-session-text failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
isError: true,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
|
|
280
370
|
const transport = new StdioServerTransport();
|
|
281
371
|
await server.connect(transport);
|
|
282
372
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared corpus-filter
|
|
2
|
+
* Shared corpus-filter clauses and format helpers for the voice-mirror tools.
|
|
3
3
|
*
|
|
4
|
-
* Imported by `voice-distil-profile` (count + sample walks)
|
|
5
|
-
* `voice-retrieve-conditioning` (exemplar walk)
|
|
6
|
-
*
|
|
4
|
+
* Imported by `voice-distil-profile` (count + sample walks),
|
|
5
|
+
* `voice-retrieve-conditioning` (exemplar walk), `voice-tag-content` (tag
|
|
6
|
+
* writes), and `voice-ingest-session-text` (SessionEnd ingest). The single
|
|
7
|
+
* load-bearing filter — when the corpus rules change, this is the only place
|
|
8
|
+
* to edit.
|
|
7
9
|
*
|
|
8
|
-
* Two predicates:
|
|
10
|
+
* Two predicates in VOICE_CORPUS_WHERE:
|
|
9
11
|
*
|
|
10
12
|
* 1. `authorshipMode IN ['human-only', 'human-led-agent-assisted']`.
|
|
11
13
|
* `human-only` is the operator's own writing verbatim;
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
* Chat captures voice in a way email rarely does — this is the
|
|
26
28
|
* single highest-leverage corpus addition for voice fidelity.
|
|
27
29
|
*
|
|
28
|
-
* The clause expects `$accountId` to be bound in the calling
|
|
30
|
+
* The clause expects `$accountId` to be bound in the calling Cypher
|
|
29
31
|
* parameters. It does not bind any other parameters.
|
|
30
32
|
*/
|
|
31
33
|
export const VOICE_CORPUS_WHERE = `n.accountId = $accountId
|
|
@@ -52,3 +54,73 @@ export const TAGGABLE_PRIMARY_LABELS = [
|
|
|
52
54
|
"Message",
|
|
53
55
|
"SocialPost",
|
|
54
56
|
] as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Closed enum of writing-format values. One `:VoiceProfile` exists per
|
|
60
|
+
* `(accountId, userId, format)`. Adding a new value is a separate task.
|
|
61
|
+
*
|
|
62
|
+
* `marketing-copy` has no auto-derivation path — operator-tagged only.
|
|
63
|
+
*/
|
|
64
|
+
export const FORMAT_VALUES = [
|
|
65
|
+
"text",
|
|
66
|
+
"email",
|
|
67
|
+
"social-post",
|
|
68
|
+
"article",
|
|
69
|
+
"marketing-copy",
|
|
70
|
+
"note",
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
export type VoiceFormat = (typeof FORMAT_VALUES)[number];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns VOICE_CORPUS_WHERE with an additional `n.format = $format`
|
|
77
|
+
* predicate appended. Requires `$format` to be bound alongside `$accountId`
|
|
78
|
+
* in the calling Cypher parameters.
|
|
79
|
+
*
|
|
80
|
+
* Format-discovery queries (which enumerate distinct formats from the
|
|
81
|
+
* corpus) use the base `VOICE_CORPUS_WHERE` directly; per-format corpus
|
|
82
|
+
* walks use this function.
|
|
83
|
+
*/
|
|
84
|
+
export function voiceCorpusWhereWithFormat(format: VoiceFormat): string {
|
|
85
|
+
return `${VOICE_CORPUS_WHERE}\n AND n.format = $format`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Derives the default writing format for a corpus node from its labels
|
|
90
|
+
* and properties. Used by ingest writers and `voice-tag-content` when the
|
|
91
|
+
* operator does not specify a format explicitly.
|
|
92
|
+
*
|
|
93
|
+
* Derivation rules (first match wins):
|
|
94
|
+
*
|
|
95
|
+
* | Node shape | format |
|
|
96
|
+
* |-------------------------------------------------------------|---------------|
|
|
97
|
+
* | :Message | 'text' |
|
|
98
|
+
* | :SocialPost | 'social-post' |
|
|
99
|
+
* | :KnowledgeDocument {source:'email'} | 'email' |
|
|
100
|
+
* | :ConversationArchive {source:'email'} | 'email' |
|
|
101
|
+
* | :KnowledgeDocument (non-email), body.length > 1500 | 'article' |
|
|
102
|
+
* | :KnowledgeDocument (non-email), body.length ≤ 1500 | 'note' |
|
|
103
|
+
* | :Section (conversation-document chunk) | 'text' |
|
|
104
|
+
* | fallback | 'text' |
|
|
105
|
+
*
|
|
106
|
+
* `marketing-copy` has no auto-derivation path.
|
|
107
|
+
*/
|
|
108
|
+
export function deriveFormat(
|
|
109
|
+
labels: string[],
|
|
110
|
+
source?: string | null,
|
|
111
|
+
body?: string | null,
|
|
112
|
+
): VoiceFormat {
|
|
113
|
+
if (labels.includes("Message")) return "text";
|
|
114
|
+
if (labels.includes("SocialPost")) return "social-post";
|
|
115
|
+
if (
|
|
116
|
+
(labels.includes("KnowledgeDocument") || labels.includes("ConversationArchive")) &&
|
|
117
|
+
source === "email"
|
|
118
|
+
) {
|
|
119
|
+
return "email";
|
|
120
|
+
}
|
|
121
|
+
if (labels.includes("KnowledgeDocument")) {
|
|
122
|
+
return (body?.length ?? 0) > 1500 ? "article" : "note";
|
|
123
|
+
}
|
|
124
|
+
// :Section (conversation-document chunk) and any other label → text.
|
|
125
|
+
return "text";
|
|
126
|
+
}
|