fuli-context 0.7.3 → 0.7.4-beta.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.
Files changed (107) hide show
  1. package/README.md +170 -18
  2. package/README.zh-CN.md +146 -12
  3. package/compose.graphiti.yml +2 -0
  4. package/dist/web/assets/{AboutPage-qeO_vm0T.js → AboutPage-CftRexSY.js} +1 -1
  5. package/dist/web/assets/{ConnectionsPage-DO0GR9Xg.js → ConnectionsPage-D5J91x6d.js} +1 -1
  6. package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
  7. package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
  8. package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
  9. package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
  10. package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
  11. package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
  12. package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
  13. package/dist/web/assets/{OverviewPage-CmCcDCAs.js → OverviewPage-CHE6XhdS.js} +1 -1
  14. package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
  15. package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
  16. package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
  17. package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
  18. package/dist/web/assets/{ReviewPage-B3vnZ6gn.js → ReviewPage-DUeyu7E8.js} +1 -1
  19. package/dist/web/assets/{SearchableSelect-ih69qbcS.js → SearchableSelect-C5RiB5F8.js} +1 -1
  20. package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
  21. package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
  22. package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
  23. package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
  24. package/dist/web/assets/{console-DE40hXGC.js → console-DMvHxKf9.js} +1 -1
  25. package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
  26. package/dist/web/assets/{i18n-CDxBs1sz.js → i18n-CWfd8Dtt.js} +1 -1
  27. package/dist/web/assets/{identity-BCxb1mcy.js → identity-C9mLdR8L.js} +1 -1
  28. package/dist/web/assets/index-F5e2sQ7p.js +2 -0
  29. package/dist/web/assets/{index-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
  30. package/dist/web/assets/{model-CCMdt10Q.js → model-CsYAR8O3.js} +1 -1
  31. package/dist/web/assets/paths-CDREouhi.js +1 -0
  32. package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
  33. package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
  34. package/dist/web/index.html +7 -6
  35. package/graph-provider/fuli_graph/app.py +238 -2
  36. package/graph-provider/fuli_graph/auth.py +18 -0
  37. package/graph-provider/fuli_graph/collaboration_context.py +253 -17
  38. package/graph-provider/fuli_graph/common_knowledge.py +6 -2
  39. package/graph-provider/fuli_graph/config.py +5 -0
  40. package/graph-provider/fuli_graph/graph_models.py +6 -0
  41. package/graph-provider/fuli_graph/graph_projection.py +14 -0
  42. package/graph-provider/fuli_graph/graph_query.py +27 -3
  43. package/graph-provider/fuli_graph/knowledge_management.py +19 -1
  44. package/graph-provider/fuli_graph/knowledge_search.py +11 -0
  45. package/graph-provider/fuli_graph/models.py +15 -2
  46. package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
  47. package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
  48. package/graph-provider/fuli_graph/project_action_models.py +22 -0
  49. package/graph-provider/fuli_graph/project_knowledge.py +121 -2
  50. package/graph-provider/fuli_graph/provider_values.py +24 -0
  51. package/graph-provider/fuli_graph/runtime.py +78 -0
  52. package/graph-provider/fuli_graph/search_projection.py +14 -1
  53. package/graph-provider/fuli_graph/store_knowledge.py +116 -2
  54. package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
  55. package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
  56. package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
  57. package/graph-provider/fuli_graph/workflow_observations.py +84 -0
  58. package/graph-provider/pyproject.toml +1 -1
  59. package/npm-shrinkwrap.json +18 -18
  60. package/package.json +4 -4
  61. package/skills/capturing-session-knowledge/SKILL.md +32 -5
  62. package/skills/flreview/SKILL.md +4 -3
  63. package/src/agent-tools/graph-definitions.js +136 -20
  64. package/src/agent-tools/graph-handlers.js +10 -4
  65. package/src/cli/command-registry.js +1 -1
  66. package/src/cli/local-runtime-command.js +69 -2
  67. package/src/graphiti/agent-knowledge-workflows.js +497 -16
  68. package/src/graphiti/collaboration-preference-projection.js +162 -0
  69. package/src/graphiti/federated-application.js +155 -161
  70. package/src/graphiti/federated-search-support.js +102 -0
  71. package/src/graphiti/knowledge-provider-mapping.js +15 -0
  72. package/src/graphiti/provider-client.js +162 -10
  73. package/src/graphiti/related-project-suggestions.js +109 -0
  74. package/src/graphiti/runtime-config.js +14 -0
  75. package/src/graphiti/source-marker.js +2 -2
  76. package/src/graphiti/user-taste-skill.js +241 -0
  77. package/src/graphiti/workflow-candidate-mapping.js +87 -0
  78. package/src/graphiti/workflow-observation.js +82 -0
  79. package/src/graphiti/writing-taste-profile-workflow.js +25 -0
  80. package/src/graphiti/writing-taste-profile.js +490 -0
  81. package/src/http/graph-api-router.js +15 -2
  82. package/src/mcp/context-resources.js +158 -0
  83. package/src/mcp/create-mcp-server.js +39 -5
  84. package/src/mcp/instructions.js +5 -7
  85. package/src/mcp/session-id.js +60 -0
  86. package/src/mcp/task-context-registry.js +15 -3
  87. package/src/mcp/tool-annotations.js +8 -1
  88. package/src/mcp/tool-result.js +28 -4
  89. package/src/server.js +0 -1
  90. package/src/setup/claude-code-config.js +17 -6
  91. package/src/setup/graph-runtime.js +20 -2
  92. package/src/system/runtime-settings.js +121 -2
  93. package/src/system/system-service.js +5 -1
  94. package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +0 -1
  95. package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +0 -1
  96. package/dist/web/assets/KnowledgePage-BAT4aZIm.js +0 -1
  97. package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +0 -4
  98. package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +0 -1
  99. package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
  100. package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +0 -1
  101. package/dist/web/assets/PublicProjectsPage-egKBulOq.js +0 -1
  102. package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
  103. package/dist/web/assets/SettingsPage-oLE8mNs5.js +0 -1
  104. package/dist/web/assets/index-BzhNfzU_.js +0 -2
  105. package/dist/web/assets/paths-DwHeiGMT.js +0 -1
  106. package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +0 -1
  107. package/dist/web/assets/vue-router-Drfl1F8i.js +0 -1
@@ -0,0 +1,241 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const DEFAULT_ITEM_LIMIT = 64;
4
+ const DEFAULT_RECOMMENDATION_LIMIT = 12;
5
+
6
+ const ASPECT_LABELS = Object.freeze({
7
+ taste: 'Taste',
8
+ personality: 'Personality',
9
+ judgment_preference: 'Judgment preferences'
10
+ });
11
+
12
+ const STATUS_LABELS = Object.freeze({
13
+ confirmed: 'Confirmed',
14
+ agent_confirmed: 'Observed',
15
+ pending: 'Working hypothesis'
16
+ });
17
+
18
+ export function buildUserTasteSkill({
19
+ preferences = [],
20
+ taskPrompt = null,
21
+ personalSpaceId = null,
22
+ personalProjectId = null,
23
+ generatedAt = new Date().toISOString(),
24
+ maxItems = DEFAULT_ITEM_LIMIT,
25
+ recommendationLimit = DEFAULT_RECOMMENDATION_LIMIT
26
+ } = {}) {
27
+ if (!Array.isArray(preferences)) {
28
+ throw new TypeError('User taste preferences must be an array');
29
+ }
30
+ const normalized = preferences
31
+ .map((item, index) => normalizePreference(item, index))
32
+ .filter((item) => item.instruction && item.preferenceKey);
33
+ const active = normalized.filter((item) => item.confirmationStatus !== 'pending');
34
+ const bounded = active.slice(0, boundedLimit(maxItems, DEFAULT_ITEM_LIMIT));
35
+ const recommendations = recommendUserTaste(
36
+ bounded,
37
+ taskPrompt,
38
+ recommendationLimit
39
+ );
40
+ const sourceFingerprint = createHash('sha256')
41
+ .update(canonicalJson(active.map(preferenceFingerprint)))
42
+ .digest('hex');
43
+
44
+ return {
45
+ skill_name: 'user-taste',
46
+ skill_version: `v1:${sourceFingerprint.slice(0, 24)}`,
47
+ generated_at: generatedAt,
48
+ generated_from: 'effective_preferences',
49
+ source_count: bounded.length,
50
+ total_source_count: active.length,
51
+ pending_source_count: normalized.length - active.length,
52
+ truncated: active.length > bounded.length,
53
+ scope: {
54
+ personal_space_id: personalSpaceId,
55
+ personal_project_id: personalProjectId
56
+ },
57
+ recommendations,
58
+ markdown: renderUserTasteSkill({
59
+ preferences: bounded,
60
+ recommendations,
61
+ skillVersion: `v1:${sourceFingerprint.slice(0, 24)}`,
62
+ generatedAt
63
+ })
64
+ };
65
+ }
66
+
67
+ export function recommendUserTaste(
68
+ preferences,
69
+ taskPrompt = null,
70
+ limit = DEFAULT_RECOMMENDATION_LIMIT
71
+ ) {
72
+ const promptTokens = tokenize(taskPrompt);
73
+ const ranked = preferences.map((preference) => {
74
+ const preferenceTokens = tokenize([
75
+ preference.title,
76
+ preference.instruction,
77
+ preference.reason,
78
+ ...preference.searchTerms
79
+ ].join(' '));
80
+ const matchedTerms = [...promptTokens]
81
+ .filter((token) => preferenceTokens.has(token))
82
+ .sort();
83
+ const matchScore = promptTokens.size === 0
84
+ ? 0
85
+ : Number(Math.min(1, matchedTerms.length / Math.min(promptTokens.size, 6)).toFixed(4));
86
+ return {
87
+ preference_key: preference.preferenceKey,
88
+ title: preference.title,
89
+ instruction: preference.instruction,
90
+ profile_aspect: preference.profileAspect,
91
+ preference_scope: preference.preferenceScope,
92
+ preference_project_id: preference.preferenceProjectId,
93
+ evidence_status: STATUS_LABELS[preference.confirmationStatus]
94
+ ?? preference.confirmationStatus,
95
+ match_score: matchScore,
96
+ matched_terms: matchedTerms,
97
+ reason: matchedTerms.length
98
+ ? `Matches prior preference terms: ${matchedTerms.join(', ')}`
99
+ : 'Available as a default profile rule; no task-specific term matched.'
100
+ };
101
+ });
102
+ return ranked
103
+ .sort((left, right) => right.match_score - left.match_score)
104
+ .slice(0, boundedLimit(limit, DEFAULT_RECOMMENDATION_LIMIT));
105
+ }
106
+
107
+ export function renderUserTasteSkill({
108
+ preferences,
109
+ recommendations = [],
110
+ skillVersion,
111
+ generatedAt
112
+ }) {
113
+ const sections = [];
114
+ for (const [aspect, label] of Object.entries(ASPECT_LABELS)) {
115
+ const items = preferences.filter((preference) => preference.profileAspect === aspect);
116
+ if (!items.length) continue;
117
+ sections.push(`### ${label}`);
118
+ sections.push(items.map(renderPreferenceLine).join('\n'));
119
+ }
120
+ if (!sections.length) {
121
+ sections.push('No effective personal taste preferences are available yet.');
122
+ }
123
+
124
+ const recommendationLines = recommendations.length
125
+ ? recommendations.slice(0, 8).map((item) => (
126
+ `- **${item.title}** — ${item.instruction} ` +
127
+ `(${item.evidence_status}; ${item.reason})`
128
+ )).join('\n')
129
+ : '- No task-specific recommendation is available.';
130
+
131
+ return [
132
+ '---',
133
+ 'name: user-taste',
134
+ 'description: Apply the user\'s current evidence-backed taste and working preferences when making recommendations or judgment calls.',
135
+ `version: ${skillVersion}`,
136
+ '---',
137
+ '',
138
+ '# User Taste · Generated',
139
+ '',
140
+ `Generated from FULI effective preferences at ${generatedAt}. This is a derived, read-time projection; it does not overwrite a user-authored taste Skill.`,
141
+ '',
142
+ '## Precedence',
143
+ '',
144
+ '1. The current user request and authoritative project constraints.',
145
+ '2. Confirmed preferences in the narrowest matching scope.',
146
+ '3. Observed preferences as weaker signals; never present them as user-confirmed.',
147
+ '4. Explain the matching rule and material tradeoff when making a recommendation.',
148
+ '',
149
+ '## Active profile rules',
150
+ '',
151
+ sections.join('\n\n'),
152
+ '',
153
+ '## Task recommendations',
154
+ '',
155
+ recommendationLines,
156
+ ''
157
+ ].join('\n');
158
+ }
159
+
160
+ function normalizePreference(item, index) {
161
+ const value = item && typeof item === 'object' ? item : {};
162
+ const attributes = value.attributes && typeof value.attributes === 'object'
163
+ ? value.attributes
164
+ : {};
165
+ const id = String(value.id ?? value.preference_key ?? value.key ?? `preference-${index}`);
166
+ const searchTerms = Array.isArray(attributes.searchTerms)
167
+ ? attributes.searchTerms.filter((term) => typeof term === 'string' && term.trim())
168
+ : [];
169
+ return {
170
+ id,
171
+ preferenceKey: String(
172
+ value.preference_key ?? value.preferenceKey ?? value.key ?? id
173
+ ),
174
+ title: singleLine(value.title ?? value.preference_key ?? value.key ?? id),
175
+ instruction: singleLine(value.instruction ?? value.summary ?? ''),
176
+ reason: singleLine(value.reason ?? value.reasoning_summary ?? ''),
177
+ profileAspect: value.profile_aspect ?? value.profileAspect ?? 'taste',
178
+ preferenceScope: value.preference_scope ?? value.preferenceScope ?? 'global',
179
+ preferenceProjectId: value.preference_project_id
180
+ ?? value.preferenceProjectId
181
+ ?? null,
182
+ confirmationStatus: value.confirmation_status
183
+ ?? value.confirmationStatus
184
+ ?? 'confirmed',
185
+ searchTerms
186
+ };
187
+ }
188
+
189
+ function preferenceFingerprint(preference) {
190
+ return {
191
+ preferenceKey: preference.preferenceKey,
192
+ title: preference.title,
193
+ instruction: preference.instruction,
194
+ reason: preference.reason,
195
+ profileAspect: preference.profileAspect,
196
+ preferenceScope: preference.preferenceScope,
197
+ preferenceProjectId: preference.preferenceProjectId,
198
+ confirmationStatus: preference.confirmationStatus,
199
+ searchTerms: [...preference.searchTerms].sort()
200
+ };
201
+ }
202
+
203
+ function renderPreferenceLine(preference) {
204
+ const status = STATUS_LABELS[preference.confirmationStatus]
205
+ ?? preference.confirmationStatus;
206
+ const scope = preference.preferenceScope === 'project'
207
+ ? `project:${preference.preferenceProjectId ?? 'active'}`
208
+ : 'global';
209
+ return `- **${status} · ${scope} · ${preference.preferenceKey}** ${preference.instruction}`;
210
+ }
211
+
212
+ function tokenize(value) {
213
+ const normalized = String(value ?? '').toLocaleLowerCase();
214
+ const tokens = new Set(normalized.match(/[a-z0-9][a-z0-9_-]*/g) ?? []);
215
+ const cjkRuns = normalized.match(/[\u3400-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g) ?? [];
216
+ for (const run of cjkRuns) {
217
+ if (run.length >= 2) tokens.add(run);
218
+ for (let index = 0; index < run.length - 1; index += 1) {
219
+ tokens.add(run.slice(index, index + 2));
220
+ }
221
+ }
222
+ return tokens;
223
+ }
224
+
225
+ function canonicalJson(value) {
226
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
227
+ if (value && typeof value === 'object') {
228
+ return `{${Object.keys(value).sort().map((key) =>
229
+ `${JSON.stringify(key)}:${canonicalJson(value[key])}`
230
+ ).join(',')}}`;
231
+ }
232
+ return JSON.stringify(value);
233
+ }
234
+
235
+ function singleLine(value) {
236
+ return String(value ?? '').replace(/[\r\n\t]+/g, ' ').trim();
237
+ }
238
+
239
+ function boundedLimit(value, fallback) {
240
+ return Number.isInteger(value) && value > 0 ? Math.min(value, 100) : fallback;
241
+ }
@@ -0,0 +1,87 @@
1
+ export function providerWorkflowCandidateSearch(input) {
2
+ return {
3
+ personal_space_id: input.personalSpaceId,
4
+ personal_project_id: input.personalProjectId ?? null,
5
+ after_step_key: input.afterStepKey ?? null,
6
+ limit: input.limit ?? 20
7
+ };
8
+ }
9
+
10
+ export function workflowCandidatePage(value) {
11
+ return {
12
+ policy: workflowRecommendationPolicy(value.policy),
13
+ candidates: value.candidates.map(workflowCandidate)
14
+ };
15
+ }
16
+
17
+ export function workflowCandidate(value) {
18
+ return {
19
+ candidateId: value.candidate_id,
20
+ candidateVersion: value.candidate_version,
21
+ evidenceRevision: value.evidence_revision,
22
+ decisionRevision: value.decision_revision,
23
+ ruleFingerprint: value.rule_fingerprint,
24
+ workflowKey: value.workflow_key,
25
+ condition: value.condition,
26
+ personalSpaceId: value.personal_space_id,
27
+ personalProjectId: value.personal_project_id ?? null,
28
+ sourceStepId: value.source_step_id,
29
+ sourceStepKey: value.source_step_key,
30
+ sourceStepName: value.source_step_name,
31
+ targetStepId: value.target_step_id,
32
+ targetStepKey: value.target_step_key,
33
+ targetStepName: value.target_step_name,
34
+ status: value.status,
35
+ occurrenceCount: value.occurrence_count,
36
+ distinctSessionCount: value.distinct_session_count,
37
+ recency: {
38
+ firstObservedAt: value.recency.first_observed_at,
39
+ lastObservedAt: value.recency.last_observed_at,
40
+ ageDays: value.recency.age_days,
41
+ score: value.recency.score
42
+ },
43
+ confirmationAuthority: value.confirmation_authority,
44
+ negativeEvidenceCount: value.negative_evidence_count,
45
+ declineCount: value.decline_count,
46
+ reviewedAt: value.reviewed_at ?? null,
47
+ reviewReason: value.review_reason ?? null,
48
+ recommendation: value.recommendation,
49
+ executionAuthorized: value.execution_authorized,
50
+ authorization: value.authorization
51
+ ? workflowAuthorization(value.authorization)
52
+ : null
53
+ };
54
+ }
55
+
56
+ function workflowRecommendationPolicy(value) {
57
+ return {
58
+ minimumOccurrences: value.minimum_occurrences,
59
+ minimumDistinctSessions: value.minimum_distinct_sessions,
60
+ recommendationThreshold: value.recommendation_threshold,
61
+ weights: {
62
+ occurrences: value.weights.occurrences,
63
+ distinctSessions: value.weights.distinct_sessions,
64
+ recency: value.weights.recency,
65
+ confirmationAuthority: value.weights.confirmation_authority
66
+ },
67
+ declinePenalty: value.decline_penalty,
68
+ negativeEvidencePenalty: value.negative_evidence_penalty
69
+ };
70
+ }
71
+
72
+ function workflowAuthorization(value) {
73
+ return {
74
+ authorizationId: value.authorization_id,
75
+ candidateId: value.candidate_id,
76
+ candidateVersion: value.candidate_version,
77
+ ruleId: value.rule_id,
78
+ ruleFingerprint: value.rule_fingerprint,
79
+ scope: value.scope,
80
+ active: value.active,
81
+ authority: value.authority,
82
+ createdAt: value.created_at,
83
+ highRiskPerCallApprovalRequired:
84
+ value.high_risk_per_call_approval_required,
85
+ highRiskActionCategories: value.high_risk_action_categories
86
+ };
87
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const FORBIDDEN_AGENT_FIELDS = [
4
+ 'sessionId',
5
+ 'idempotencyKey',
6
+ 'observedAt',
7
+ 'occurrenceCount',
8
+ 'distinctSessionCount',
9
+ 'confirmationAuthority',
10
+ 'authority',
11
+ 'durableAuthorizationConfirmed',
12
+ 'approvalToken'
13
+ ];
14
+
15
+ export function providerWorkflowTransitionObservation(input) {
16
+ for (const field of FORBIDDEN_AGENT_FIELDS) {
17
+ if (Object.hasOwn(input, field)) {
18
+ throw new TypeError(
19
+ `Workflow observations cannot submit host, aggregate, or authority field: ${field}`
20
+ );
21
+ }
22
+ }
23
+ assertHostValue(input.hostSessionId, 'hostSessionId');
24
+ assertHostValue(input.hostObservedAt, 'hostObservedAt');
25
+ const condition = input.condition ?? {};
26
+ const observationId = workflowObservationId({
27
+ hostSessionId: input.hostSessionId,
28
+ personalProjectId: input.personalProjectId ?? null,
29
+ workflowKey: input.workflowKey,
30
+ fromActionId: input.fromStep.actionId,
31
+ toActionId: input.toStep.actionId,
32
+ condition
33
+ });
34
+ return {
35
+ personal_space_id: input.personalSpaceId,
36
+ personal_project_id: input.personalProjectId ?? null,
37
+ host_session_id: input.hostSessionId,
38
+ observation_id: observationId,
39
+ from_step: providerStep(input.fromStep),
40
+ to_step: providerStep(input.toStep),
41
+ workflow_key: input.workflowKey,
42
+ condition,
43
+ observed_at: input.hostObservedAt,
44
+ evidence_summary: input.evidenceSummary,
45
+ source_application: input.sourceApplication ?? null,
46
+ source_turn_id: input.sourceTurnId ?? null,
47
+ sensitivity: input.sensitivity ?? 'normal'
48
+ };
49
+ }
50
+
51
+ function providerStep(step) {
52
+ return {
53
+ action_id: step.actionId,
54
+ name: step.name,
55
+ summary: step.summary ?? null
56
+ };
57
+ }
58
+
59
+ function workflowObservationId(value) {
60
+ const digest = createHash('sha256')
61
+ .update(canonicalJson(value))
62
+ .digest('hex');
63
+ return `workflow-observation:${digest}`;
64
+ }
65
+
66
+ function canonicalJson(value) {
67
+ if (Array.isArray(value)) {
68
+ return `[${value.map(canonicalJson).join(',')}]`;
69
+ }
70
+ if (value && typeof value === 'object') {
71
+ return `{${Object.keys(value).sort().map((key) =>
72
+ `${JSON.stringify(key)}:${canonicalJson(value[key])}`
73
+ ).join(',')}}`;
74
+ }
75
+ return JSON.stringify(value);
76
+ }
77
+
78
+ function assertHostValue(value, label) {
79
+ if (typeof value !== 'string' || !value.trim()) {
80
+ throw new TypeError(`${label} must be injected by the MCP host`);
81
+ }
82
+ }
@@ -0,0 +1,25 @@
1
+ import { buildWritingTasteProfile } from './writing-taste-profile.js';
2
+
3
+ export async function getWritingTasteProfile(application, {
4
+ personalSpaceId,
5
+ personalProjectId = null,
6
+ limit = 500
7
+ } = {}) {
8
+ const [graph, conflictRecords] = await Promise.all([
9
+ application.getKnowledgeGraph({
10
+ spaceId: personalSpaceId,
11
+ limit
12
+ }),
13
+ application.personal.listPreferenceConflicts(
14
+ personalSpaceId,
15
+ null,
16
+ limit
17
+ )
18
+ ]);
19
+ return buildWritingTasteProfile({
20
+ graph,
21
+ conflictRecords,
22
+ personalSpaceId,
23
+ personalProjectId
24
+ });
25
+ }