@rubytech/create-maxy-code 0.1.170 → 0.1.171

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.
Files changed (37) hide show
  1. package/dist/__tests__/premium-mcp-discover.test.js +42 -0
  2. package/dist/lib/premium-mcp-discover.js +9 -2
  3. package/package.json +1 -1
  4. package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.d.ts +51 -0
  5. package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -0
  6. package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.js +196 -0
  7. package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/dist/index.js.map +1 -0
  8. package/payload/premium-plugins/writer-craft/lib/mcp-stderr-tee/package.json +7 -0
  9. package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts +3 -0
  10. package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -0
  11. package/payload/premium-plugins/writer-craft/mcp/dist/index.js +257 -0
  12. package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -0
  13. package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.d.ts +14 -0
  14. package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.d.ts.map +1 -0
  15. package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.js +47 -0
  16. package/payload/premium-plugins/writer-craft/mcp/dist/lib/neo4j.js.map +1 -0
  17. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts +42 -0
  18. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts.map +1 -0
  19. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js +54 -0
  20. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js.map +1 -0
  21. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts +36 -0
  22. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -0
  23. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +225 -0
  24. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -0
  25. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts +26 -0
  26. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -0
  27. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js +74 -0
  28. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js.map +1 -0
  29. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +22 -0
  30. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -0
  31. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +102 -0
  32. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -0
  33. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts +14 -0
  34. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts.map +1 -0
  35. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js +84 -0
  36. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js.map +1 -0
  37. package/payload/premium-plugins/writer-craft/mcp/package-lock.json +1327 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Minimal neo4j driver wrapper for the writer-craft MCP server.
3
+ *
4
+ * Mirrors the public surface of `platform/plugins/memory/mcp/src/lib/neo4j.ts`:
5
+ * `getSession()` returns a session, `closeDriver()` releases the singleton.
6
+ * Password resolution follows the same `PLATFORM_ROOT/config/.neo4j-password`
7
+ * convention, with an explicit `NEO4J_URI` requirement to avoid silent
8
+ * cross-account orphaning.
9
+ */
10
+ import neo4j from "neo4j-driver";
11
+ import { readFileSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ let driver = null;
14
+ function readPassword() {
15
+ if (process.env.NEO4J_PASSWORD)
16
+ return process.env.NEO4J_PASSWORD;
17
+ const passwordFile = resolve(process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname, "../../../../../platform"), "config/.neo4j-password");
18
+ try {
19
+ return readFileSync(passwordFile, "utf-8").trim();
20
+ }
21
+ catch {
22
+ throw new Error(`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`);
23
+ }
24
+ }
25
+ export function getDriver() {
26
+ if (!driver) {
27
+ const uri = process.env.NEO4J_URI;
28
+ if (!uri) {
29
+ throw new Error("[writer-craft] NEO4J_URI unset — refusing to default to bolt://localhost:7687");
30
+ }
31
+ const user = process.env.NEO4J_USER ?? "neo4j";
32
+ const password = readPassword();
33
+ process.stderr.write(`[writer-craft] resolved neo4j_uri=${uri}\n`);
34
+ driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
35
+ }
36
+ return driver;
37
+ }
38
+ export function getSession() {
39
+ return getDriver().session();
40
+ }
41
+ export async function closeDriver() {
42
+ if (driver) {
43
+ await driver.close();
44
+ driver = null;
45
+ }
46
+ }
47
+ //# sourceMappingURL=neo4j.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"neo4j.js","sourceRoot":"","sources":["../../src/lib/neo4j.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAA0B,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,SAAS,YAAY;IACnB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAClE,MAAM,YAAY,GAAG,OAAO,CAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,yBAAyB,CAAC,EACpF,wBAAwB,CACzB,CAAC;IACF,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,yCAAyC,YAAY,gCAAgC,CACtF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC;QAC/C,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;QAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,GAAG,IAAI,CAAC,CAAC;QACnE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared corpus-filter clause for the voice-mirror tools.
3
+ *
4
+ * Imported by `voice-distil-profile` (count + sample walks) and
5
+ * `voice-retrieve-conditioning` (exemplar walk). The single load-bearing
6
+ * filter — when the corpus rules change, this is the only place to edit.
7
+ *
8
+ * Two predicates:
9
+ *
10
+ * 1. `authorshipMode IN ['human-only', 'human-led-agent-assisted']`.
11
+ * `human-only` is the operator's own writing verbatim;
12
+ * `human-led-agent-assisted` is operator-authored with light agent
13
+ * polish, which still carries voice signal (the operator chose the
14
+ * claims and the structure). Including this expands the available
15
+ * corpus to match how most modern writing actually happens.
16
+ *
17
+ * 2. Label is one of the four primary content labels OR is a
18
+ * conversation-document `:Section` chunk (unguarded chat voice from
19
+ * WhatsApp / Telegram / Signal / Granola / Otter / Circleback etc.
20
+ * archives). After the Task 324 collapse, conversation transcripts
21
+ * land as `:KnowledgeDocument` parents carrying `conversationIdentity`
22
+ * with plain `:Section` children — no `:Conversation` secondary label.
23
+ * Conversation-document sections are identified by their parent's
24
+ * `conversationIdentity` property via an EXISTS sub-pattern.
25
+ * Chat captures voice in a way email rarely does — this is the
26
+ * single highest-leverage corpus addition for voice fidelity.
27
+ *
28
+ * The clause expects `$accountId` to be bound in the calling cypher
29
+ * parameters. It does not bind any other parameters.
30
+ */
31
+ export declare const VOICE_CORPUS_WHERE = "n.accountId = $accountId\n AND n.authorshipMode IN ['human-only', 'human-led-agent-assisted']\n AND (\n any(lbl IN labels(n) WHERE lbl IN ['KnowledgeDocument', 'Message', 'SocialPost'])\n OR ('Section' IN labels(n) AND EXISTS {\n MATCH (parent:KnowledgeDocument)-[:HAS_SECTION]->(n)\n WHERE parent.conversationIdentity IS NOT NULL\n })\n )";
32
+ /**
33
+ * The label set the tag tool accepts when stamping `authorshipMode`.
34
+ * Mirrors the label predicate inside VOICE_CORPUS_WHERE so the tag tool
35
+ * and the corpus walk stay in lockstep. The tag tool does not filter
36
+ * by authorshipMode (that's the property it's setting).
37
+ *
38
+ * `:Email` retired with Task 321 — email threads now land as
39
+ * `:KnowledgeDocument {source:'email'}` and are already covered.
40
+ */
41
+ export declare const TAGGABLE_PRIMARY_LABELS: readonly ["KnowledgeDocument", "Message", "SocialPost"];
42
+ //# sourceMappingURL=voice-corpus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-corpus.d.ts","sourceRoot":"","sources":["../../src/lib/voice-corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,kBAAkB,8WAQ3B,CAAC;AAEL;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,yDAI1B,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared corpus-filter clause for the voice-mirror tools.
3
+ *
4
+ * Imported by `voice-distil-profile` (count + sample walks) and
5
+ * `voice-retrieve-conditioning` (exemplar walk). The single load-bearing
6
+ * filter — when the corpus rules change, this is the only place to edit.
7
+ *
8
+ * Two predicates:
9
+ *
10
+ * 1. `authorshipMode IN ['human-only', 'human-led-agent-assisted']`.
11
+ * `human-only` is the operator's own writing verbatim;
12
+ * `human-led-agent-assisted` is operator-authored with light agent
13
+ * polish, which still carries voice signal (the operator chose the
14
+ * claims and the structure). Including this expands the available
15
+ * corpus to match how most modern writing actually happens.
16
+ *
17
+ * 2. Label is one of the four primary content labels OR is a
18
+ * conversation-document `:Section` chunk (unguarded chat voice from
19
+ * WhatsApp / Telegram / Signal / Granola / Otter / Circleback etc.
20
+ * archives). After the Task 324 collapse, conversation transcripts
21
+ * land as `:KnowledgeDocument` parents carrying `conversationIdentity`
22
+ * with plain `:Section` children — no `:Conversation` secondary label.
23
+ * Conversation-document sections are identified by their parent's
24
+ * `conversationIdentity` property via an EXISTS sub-pattern.
25
+ * Chat captures voice in a way email rarely does — this is the
26
+ * single highest-leverage corpus addition for voice fidelity.
27
+ *
28
+ * The clause expects `$accountId` to be bound in the calling cypher
29
+ * parameters. It does not bind any other parameters.
30
+ */
31
+ export const VOICE_CORPUS_WHERE = `n.accountId = $accountId
32
+ AND n.authorshipMode IN ['human-only', 'human-led-agent-assisted']
33
+ AND (
34
+ any(lbl IN labels(n) WHERE lbl IN ['KnowledgeDocument', 'Message', 'SocialPost'])
35
+ OR ('Section' IN labels(n) AND EXISTS {
36
+ MATCH (parent:KnowledgeDocument)-[:HAS_SECTION]->(n)
37
+ WHERE parent.conversationIdentity IS NOT NULL
38
+ })
39
+ )`;
40
+ /**
41
+ * The label set the tag tool accepts when stamping `authorshipMode`.
42
+ * Mirrors the label predicate inside VOICE_CORPUS_WHERE so the tag tool
43
+ * and the corpus walk stay in lockstep. The tag tool does not filter
44
+ * by authorshipMode (that's the property it's setting).
45
+ *
46
+ * `:Email` retired with Task 321 — email threads now land as
47
+ * `:KnowledgeDocument {source:'email'}` and are already covered.
48
+ */
49
+ export const TAGGABLE_PRIMARY_LABELS = [
50
+ "KnowledgeDocument",
51
+ "Message",
52
+ "SocialPost",
53
+ ];
54
+ //# sourceMappingURL=voice-corpus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-corpus.js","sourceRoot":"","sources":["../../src/lib/voice-corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;IAQ9B,CAAC;AAEL;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,mBAAmB;IACnB,SAAS;IACT,YAAY;CACJ,CAAC"}
@@ -0,0 +1,36 @@
1
+ export interface VoiceDistilProfileParams {
2
+ accountId: string;
3
+ adminUserId: string;
4
+ force?: boolean;
5
+ /**
6
+ * Operation mode (Task 424):
7
+ * - `'sample'` (default) — read the corpus + feedback intents and return
8
+ * them. No write. The calling agent composes the style-card YAML
9
+ * in-turn from the returned exemplars.
10
+ * - `'write'` — persist the style card the agent produced. Requires
11
+ * `styleCardYaml`.
12
+ */
13
+ mode?: "sample" | "write";
14
+ /** Required when `mode='write'`. The style-card YAML produced by the agent. */
15
+ styleCardYaml?: string;
16
+ }
17
+ export interface VoiceCorpusExemplar {
18
+ id: string;
19
+ label: string;
20
+ body: string;
21
+ }
22
+ export interface VoiceDistilProfileResult {
23
+ profileId: string | null;
24
+ corpusSize: number;
25
+ generatedAt: string | null;
26
+ feedbackEntries: number;
27
+ skipped: boolean;
28
+ skipReason?: "below-threshold" | "recent" | "empty-corpus" | "missing-style-card";
29
+ errorReason?: string;
30
+ /** Present when `mode='sample'`: corpus exemplars for the agent to read. */
31
+ exemplars?: VoiceCorpusExemplar[];
32
+ /** Present when `mode='sample'`: operator-edit intents to fold into the style card. */
33
+ feedbackIntents?: string[];
34
+ }
35
+ export declare function voiceDistilProfile(params: VoiceDistilProfileParams): Promise<VoiceDistilProfileResult>;
36
+ //# sourceMappingURL=voice-distil-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-distil-profile.d.ts","sourceRoot":"","sources":["../../src/tools/voice-distil-profile.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC1B,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,GAAG,QAAQ,GAAG,cAAc,GAAG,oBAAoB,CAAC;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAOD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CAsNnC"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * voice-distil-profile — walk the operator's `human-only` corpus and
3
+ * persist a `:VoiceProfile` style card.
4
+ *
5
+ * Cadence guard: re-runs on ≥20% corpus growth since last profile, OR
6
+ * ≥30 days since `generatedAt`. `force: true` overrides both.
7
+ *
8
+ * Body-text sampling caps at ~120k chars (rough proxy for tokens at
9
+ * 4 chars/token = 30k tokens) — well inside Haiku's 200k context window
10
+ * with headroom for the system prompt + tool schema.
11
+ */
12
+ import neo4j from "neo4j-driver";
13
+ import { getSession } from "../lib/neo4j.js";
14
+ import { VOICE_CORPUS_WHERE } from "../lib/voice-corpus.js";
15
+ // Task 424 — the Haiku style-card composition moved into the calling agent's
16
+ // turn. The agent reads the corpus exemplars and feedback intents (this tool
17
+ // returns them when called with `mode='sample'`), composes the style-card YAML,
18
+ // then calls this tool again with `mode='write'` + `styleCardYaml` set.
19
+ /**
20
+ * Coerce a neo4j cypher RETURN value to a plain JS number.
21
+ *
22
+ * Neo4j-driver represents INTs as `Integer` objects (high/low 32-bit
23
+ * pair to safely carry int64). `Number()` *usually* works via valueOf()
24
+ * but the safe path is `Integer#toNumber()`. Returns null when the
25
+ * value is null/undefined so cadence-guard arithmetic stays explicit
26
+ * about the "no prior profile" case.
27
+ */
28
+ function toJsNumber(value) {
29
+ if (value === null || value === undefined)
30
+ return null;
31
+ if (typeof value === "number")
32
+ return value;
33
+ if (neo4j.isInt(value))
34
+ return value.toNumber();
35
+ if (typeof value === "string") {
36
+ const parsed = Number(value);
37
+ return Number.isFinite(parsed) ? parsed : null;
38
+ }
39
+ return null;
40
+ }
41
+ const SAMPLE_CHAR_CAP = 120_000;
42
+ const RE_RUN_GROWTH_FRACTION = 0.2;
43
+ const RE_RUN_AGE_DAYS = 30;
44
+ // The agent produces a YAML style card with at minimum these keys:
45
+ // sentenceLengthDistribution, register, favoured, avoided, taboos,
46
+ // openingPattern, closingPattern, exemplarSentences
47
+ // Exemplar sentences should be quoted verbatim from the returned corpus.
48
+ export async function voiceDistilProfile(params) {
49
+ const { accountId, adminUserId, force = false } = params;
50
+ if (!accountId || !adminUserId) {
51
+ throw new Error("voice-distil-profile: accountId and adminUserId required");
52
+ }
53
+ const session = getSession();
54
+ const now = new Date();
55
+ const nowIso = now.toISOString();
56
+ try {
57
+ // 1. Read existing profile (if any) for cadence-guard comparison.
58
+ const existing = await session.run(`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
59
+ OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
60
+ RETURN elementId(p) AS profileId, p.corpusSize AS corpusSize,
61
+ p.generatedAt AS generatedAt`, { accountId, adminUserId });
62
+ const existingRow = existing.records[0];
63
+ const prevProfileId = existingRow?.get("profileId") ?? null;
64
+ const prevCorpusSize = toJsNumber(existingRow?.get("corpusSize"));
65
+ const prevGeneratedAt = existingRow?.get("generatedAt") ?? null;
66
+ // 2. Count corpus. Filter matches human-only + human-led-agent-assisted
67
+ // across the four primary content labels plus conversation-document
68
+ // :Section chunks (chat archives — children of a :KnowledgeDocument
69
+ // with `conversationIdentity` set). See VOICE_CORPUS_WHERE above.
70
+ const countResult = await session.run(`MATCH (n)
71
+ WHERE ${VOICE_CORPUS_WHERE}
72
+ RETURN count(n) AS c`, { accountId });
73
+ const corpusSize = toJsNumber(countResult.records[0]?.get("c")) ?? 0;
74
+ if (corpusSize === 0) {
75
+ process.stderr.write(`[voice-distil] adminUser=${adminUserId} skip reason=empty-corpus\n`);
76
+ return {
77
+ profileId: prevProfileId,
78
+ corpusSize: 0,
79
+ generatedAt: prevGeneratedAt,
80
+ feedbackEntries: 0,
81
+ skipped: true,
82
+ skipReason: "empty-corpus",
83
+ };
84
+ }
85
+ // 3. Cadence guard.
86
+ if (!force && prevProfileId && prevCorpusSize !== null && prevGeneratedAt) {
87
+ const ageMs = now.getTime() - new Date(prevGeneratedAt).getTime();
88
+ const ageDays = ageMs / (24 * 60 * 60 * 1000);
89
+ // prevCorpusSize is non-null here (the guard above checks). When it's
90
+ // zero we treat growth as infinite — first real corpus always triggers
91
+ // a re-run.
92
+ const growth = prevCorpusSize > 0
93
+ ? (corpusSize - prevCorpusSize) / prevCorpusSize
94
+ : 1;
95
+ const ageTrigger = ageDays >= RE_RUN_AGE_DAYS;
96
+ const growthTrigger = growth >= RE_RUN_GROWTH_FRACTION;
97
+ if (!ageTrigger && !growthTrigger) {
98
+ const reason = ageDays < 1 ? "recent" : "below-threshold";
99
+ process.stderr.write(`[voice-distil] adminUser=${adminUserId} skip reason=${reason} corpusSize=${corpusSize} prevCorpus=${prevCorpusSize} ageDays=${ageDays.toFixed(1)}\n`);
100
+ return {
101
+ profileId: prevProfileId,
102
+ corpusSize,
103
+ generatedAt: prevGeneratedAt,
104
+ feedbackEntries: 0,
105
+ skipped: true,
106
+ skipReason: reason,
107
+ };
108
+ }
109
+ }
110
+ // 4. Sample corpus bodies up to the char cap. Pick recent first.
111
+ // Timestamp coalesces include `firstMessageAt` for conversation-document
112
+ // :Section chunks so chat archives sort by when the conversation
113
+ // actually happened.
114
+ const sampled = await session.run(`MATCH (n)
115
+ WHERE ${VOICE_CORPUS_WHERE}
116
+ WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
117
+ OPTIONAL MATCH (parent:KnowledgeDocument)-[:HAS_SECTION]->(n)
118
+ WHERE 'Section' IN labels(n) AND parent.conversationIdentity IS NOT NULL
119
+ RETURN elementId(n) AS id,
120
+ labels(n) AS labels,
121
+ coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '') AS body,
122
+ parent.conversationIdentity IS NOT NULL AS isConversationChunk,
123
+ ts
124
+ ORDER BY ts DESC NULLS LAST
125
+ LIMIT 500`, { accountId });
126
+ const samples = [];
127
+ let charBudget = SAMPLE_CHAR_CAP;
128
+ for (const r of sampled.records) {
129
+ const body = r.get("body") ?? "";
130
+ const labels = r.get("labels") ?? [];
131
+ // Conversation-document sections carry `:Section` only after the Task
132
+ // 324 collapse; surface "Conversation" via the parent's
133
+ // `conversationIdentity` so the LLM exemplar header still says "this
134
+ // is chat" rather than the bare base label.
135
+ const isConversationChunk = Boolean(r.get("isConversationChunk"));
136
+ const label = isConversationChunk
137
+ ? "Conversation"
138
+ : (labels.find((l) => ["KnowledgeDocument", "Message", "SocialPost"].includes(l)) ?? labels[0] ?? "Unknown");
139
+ if (body.length === 0)
140
+ continue;
141
+ const trimmed = body.length > charBudget ? body.slice(0, charBudget) : body;
142
+ samples.push({ id: r.get("id"), label, body: trimmed });
143
+ charBudget -= trimmed.length;
144
+ if (charBudget <= 0)
145
+ break;
146
+ }
147
+ // 5. Pull feedback intents from existing :VoiceEdit nodes for this operator.
148
+ const feedback = await session.run(`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
149
+ MATCH (e:VoiceEdit {adminUserId: $adminUserId, accountId: $accountId})
150
+ RETURN e.intent AS intent ORDER BY e.occurredAt DESC LIMIT 50`, { accountId, adminUserId });
151
+ const feedbackIntents = feedback.records
152
+ .map((r) => r.get("intent"))
153
+ .filter((x) => typeof x === "string" && x.length > 0);
154
+ // 6. Mode branch (Task 424).
155
+ // - `sample` (default): return the exemplars + feedback intents to the
156
+ // calling agent. The agent composes the YAML style card in its own
157
+ // turn and re-invokes with mode='write'.
158
+ // - `write`: persist the agent-produced style card.
159
+ const mode = params.mode ?? "sample";
160
+ if (mode === "sample") {
161
+ process.stderr.write(`[voice-distil] adminUser=${adminUserId} mode=sample corpusSize=${corpusSize} ` +
162
+ `exemplars=${samples.length} feedbackEntries=${feedbackIntents.length}\n`);
163
+ return {
164
+ profileId: prevProfileId,
165
+ corpusSize,
166
+ generatedAt: prevGeneratedAt,
167
+ feedbackEntries: feedbackIntents.length,
168
+ skipped: true,
169
+ skipReason: "missing-style-card",
170
+ exemplars: samples,
171
+ feedbackIntents,
172
+ };
173
+ }
174
+ const styleCardYaml = (params.styleCardYaml ?? "").trim();
175
+ if (!styleCardYaml) {
176
+ throw new Error("voice-distil-profile: mode='write' requires styleCardYaml (the YAML you composed from the sample-mode response).");
177
+ }
178
+ // 7. Write the :VoiceProfile node + LEARNED_FROM edges to every sampled node.
179
+ //
180
+ // The :AdminUser must already exist (onboarding/auth is the only
181
+ // legitimate origin for :AdminUser). MATCH not MERGE: a typo in
182
+ // adminUserId, an env mismatch, or a dropped credential surfaces
183
+ // here as a typed error rather than silently minting an orphan
184
+ // AdminUser that bypasses the platform's label-origin protection.
185
+ const writeResult = await session.run(`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
186
+ MERGE (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile {accountId: $accountId, adminUserId: $adminUserId})
187
+ ON CREATE SET p.createdAt = $now
188
+ SET p.styleCard = $styleCard,
189
+ p.generatedAt = $now,
190
+ p.corpusSize = $corpusSize,
191
+ p.feedbackEntries = $feedbackEntries,
192
+ p.updatedAt = $now
193
+ WITH p
194
+ UNWIND $sampleIds AS sid
195
+ MATCH (s) WHERE elementId(s) = sid
196
+ MERGE (p)-[:LEARNED_FROM]->(s)
197
+ RETURN elementId(p) AS profileId`, {
198
+ accountId,
199
+ adminUserId,
200
+ styleCard: styleCardYaml,
201
+ now: nowIso,
202
+ corpusSize,
203
+ feedbackEntries: feedbackIntents.length,
204
+ sampleIds: samples.map((s) => s.id),
205
+ });
206
+ // MATCH-on-:AdminUser failure yields zero records. Throw the typed
207
+ // error here rather than returning a half-baked result.
208
+ if (writeResult.records.length === 0) {
209
+ throw new Error(`voice-mirror: :AdminUser {accountId='${accountId}', adminUserId='${adminUserId}'} not found. Onboarding must promote the operator to :AdminUser before distillation.`);
210
+ }
211
+ const profileId = writeResult.records[0].get("profileId");
212
+ process.stderr.write(`[voice-distil] adminUser=${adminUserId} mode=write corpusSize=${corpusSize} generatedAt=${nowIso} feedbackEntries=${feedbackIntents.length}\n`);
213
+ return {
214
+ profileId,
215
+ corpusSize,
216
+ generatedAt: nowIso,
217
+ feedbackEntries: feedbackIntents.length,
218
+ skipped: false,
219
+ };
220
+ }
221
+ finally {
222
+ await session.close();
223
+ }
224
+ }
225
+ //# sourceMappingURL=voice-distil-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-distil-profile.js","sourceRoot":"","sources":["../../src/tools/voice-distil-profile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export interface VoiceRecordFeedbackParams {
2
+ accountId: string;
3
+ adminUserId: string;
4
+ originalText: string;
5
+ editedText: string;
6
+ /**
7
+ * One sentence describing what changed and what voice preference it reveals.
8
+ * The calling agent summarises this from `originalText` vs `editedText` in
9
+ * its own turn before invoking the tool (Task 424). Examples: "shorter and
10
+ * dropped the apology", "switched to first person", "removed corporate
11
+ * sign-off", "added a specific date".
12
+ */
13
+ intent: string;
14
+ context?: {
15
+ skill?: string;
16
+ brief?: string;
17
+ };
18
+ }
19
+ export interface VoiceRecordFeedbackResult {
20
+ editId: string;
21
+ intent: string;
22
+ occurredAt: string;
23
+ hadProfile: boolean;
24
+ }
25
+ export declare function voiceRecordFeedback(params: VoiceRecordFeedbackParams): Promise<VoiceRecordFeedbackResult>;
26
+ //# sourceMappingURL=voice-record-feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-record-feedback.d.ts","sourceRoot":"","sources":["../../src/tools/voice-record-feedback.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,yBAAyB,CAAC,CAoEpC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * voice-record-feedback — capture an operator's edit on an agent draft
3
+ * as feedback for the next distillation.
4
+ *
5
+ * Task 424: the Haiku diff-intent summarisation moved into the calling
6
+ * agent's turn. The agent compares `originalText` to `editedText` itself
7
+ * and passes the resulting `intent` string as a required parameter. The
8
+ * tool now does the graph writes only.
9
+ *
10
+ * Side effects:
11
+ * 1. Write `(:VoiceEdit)-[:FEEDBACK_FOR]->(:VoiceProfile)` if the profile
12
+ * exists; otherwise write the :VoiceEdit alone so the next distillation
13
+ * back-fills the edge.
14
+ */
15
+ import { randomUUID } from "node:crypto";
16
+ import { getSession } from "../lib/neo4j.js";
17
+ export async function voiceRecordFeedback(params) {
18
+ const { accountId, adminUserId, originalText, editedText, intent: rawIntent, context } = params;
19
+ if (!accountId || !adminUserId) {
20
+ throw new Error("voice-record-feedback: accountId and adminUserId required");
21
+ }
22
+ if (!originalText || !editedText) {
23
+ throw new Error("voice-record-feedback: originalText and editedText required");
24
+ }
25
+ const intent = (rawIntent ?? "").trim();
26
+ if (!intent) {
27
+ throw new Error("voice-record-feedback: intent is required — summarise what the operator changed in-turn before calling this tool (Task 424).");
28
+ }
29
+ const editId = randomUUID();
30
+ const occurredAt = new Date().toISOString();
31
+ const cappedIntent = intent.slice(0, 500);
32
+ const session = getSession();
33
+ try {
34
+ const result = await session.run(`MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
35
+ CREATE (e:VoiceEdit {
36
+ editId: $editId,
37
+ accountId: $accountId,
38
+ adminUserId: $adminUserId,
39
+ originalText: $originalText,
40
+ editedText: $editedText,
41
+ intent: $intent,
42
+ occurredAt: $occurredAt,
43
+ skill: $skill,
44
+ brief: $brief
45
+ })
46
+ CREATE (a)-[:AUTHORED]->(e)
47
+ WITH e
48
+ OPTIONAL MATCH (:AdminUser {accountId: $accountId, adminUserId: $adminUserId})-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
49
+ FOREACH (_ IN CASE WHEN p IS NULL THEN [] ELSE [1] END |
50
+ MERGE (e)-[:FEEDBACK_FOR]->(p)
51
+ )
52
+ RETURN elementId(e) AS editId, (p IS NOT NULL) AS hadProfile`, {
53
+ accountId,
54
+ adminUserId,
55
+ editId,
56
+ originalText,
57
+ editedText,
58
+ intent: cappedIntent,
59
+ occurredAt,
60
+ skill: context?.skill ?? null,
61
+ brief: context?.brief ?? null,
62
+ });
63
+ if (result.records.length === 0) {
64
+ throw new Error(`voice-mirror: :AdminUser {accountId='${accountId}', adminUserId='${adminUserId}'} not found. Onboarding must promote the operator to :AdminUser before feedback can be recorded.`);
65
+ }
66
+ const hadProfile = result.records[0].get("hadProfile") === true;
67
+ process.stderr.write(`[voice-record-feedback] adminUser=${adminUserId} intent="${cappedIntent.replace(/"/g, "'")}" diffBytes=${editedText.length - originalText.length}\n`);
68
+ return { editId, intent: cappedIntent, occurredAt, hadProfile };
69
+ }
70
+ finally {
71
+ await session.close();
72
+ }
73
+ }
74
+ //# sourceMappingURL=voice-record-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-record-feedback.js","sourceRoot":"","sources":["../../src/tools/voice-record-feedback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA4B7C,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAiC;IAEjC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAChG,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,8HAA8H,CAC/H,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B;;;;;;;;;;;;;;;;;;oEAkB8D,EAC9D;YACE,SAAS;YACT,WAAW;YACX,MAAM;YACN,YAAY;YACZ,UAAU;YACV,MAAM,EAAE,YAAY;YACpB,UAAU;YACV,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,IAAI;YAC7B,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,IAAI;SAC9B,CACF,CAAC;QACF,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CACb,wCAAwC,SAAS,mBAAmB,WAAW,mGAAmG,CACnL,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;QAEhE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qCAAqC,WAAW,YAAY,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,eAAe,UAAU,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM,IAAI,CACtJ,CAAC;QAEF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;IAClE,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ export interface VoiceRetrieveConditioningParams {
2
+ accountId: string;
3
+ adminUserId: string;
4
+ brief: {
5
+ topic?: string;
6
+ format: "short" | "long";
7
+ register?: string;
8
+ };
9
+ tokenBudget?: number;
10
+ }
11
+ export interface VoiceExemplar {
12
+ nodeId: string;
13
+ label: string;
14
+ body: string;
15
+ source: string;
16
+ }
17
+ export interface VoiceRetrieveConditioningResult {
18
+ styleCard: string | null;
19
+ exemplars: VoiceExemplar[];
20
+ }
21
+ export declare function voiceRetrieveConditioning(params: VoiceRetrieveConditioningParams): Promise<VoiceRetrieveConditioningResult>;
22
+ //# sourceMappingURL=voice-retrieve-conditioning.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-retrieve-conditioning.d.ts","sourceRoot":"","sources":["../../src/tools/voice-retrieve-conditioning.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,aAAa,EAAE,CAAC;CAC5B;AAID,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,+BAA+B,GACtC,OAAO,CAAC,+BAA+B,CAAC,CA2F1C"}