smart-context-mcp 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ import {
14
14
  readMetricsEntries,
15
15
  resolveMetricsInput,
16
16
  } from '../metrics.js';
17
+ import { analyzeAdoption } from '../analytics/adoption.js';
17
18
 
18
19
  const WINDOW_MS = {
19
20
  '24h': 24 * 60 * 60 * 1000,
@@ -197,6 +198,8 @@ export const smartMetrics = async ({
197
198
  .filter((entry) => (tool ? entry.tool === tool : true))
198
199
  .filter((entry) => (resolvedSessionId ? entry.sessionId === resolvedSessionId : true));
199
200
 
201
+ const adoption = analyzeAdoption(filteredEntries);
202
+
200
203
  return {
201
204
  filePath: resolved.storagePath,
202
205
  storagePath: resolved.storagePath,
@@ -212,6 +215,7 @@ export const smartMetrics = async ({
212
215
  },
213
216
  invalidLines,
214
217
  summary: aggregateMetrics(filteredEntries),
218
+ adoption,
215
219
  latestEntries: buildLatestEntries(filteredEntries, latest),
216
220
  };
217
221
  }
@@ -229,6 +233,8 @@ export const smartMetrics = async ({
229
233
  window,
230
234
  });
231
235
 
236
+ const adoption = analyzeAdoption(entries);
237
+
232
238
  return {
233
239
  filePath: resolved.storagePath,
234
240
  storagePath: resolved.storagePath,
@@ -244,6 +250,7 @@ export const smartMetrics = async ({
244
250
  },
245
251
  invalidLines,
246
252
  summary: aggregateMetrics(entries),
253
+ adoption,
247
254
  latestEntries: buildLatestEntries(entries, latest),
248
255
  };
249
256
  };
@@ -18,6 +18,15 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
18
18
  maxTokens: item.maxTokens,
19
19
  });
20
20
 
21
+ if (readResult.error) {
22
+ results.push({
23
+ filePath: item.path,
24
+ mode: item.mode ?? 'outline',
25
+ error: readResult.error,
26
+ });
27
+ continue;
28
+ }
29
+
21
30
  const itemTokens = countTokens(readResult.content);
22
31
 
23
32
  if (maxTokens && totalTokens + itemTokens > maxTokens && results.length > 0) {
@@ -363,7 +363,27 @@ const formatContextSections = (sections) => {
363
363
  };
364
364
 
365
365
  export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext }) => {
366
- const { fullPath, content } = readTextFile(filePath);
366
+ let fullPath, content;
367
+
368
+ try {
369
+ const result = readTextFile(filePath);
370
+ fullPath = result.fullPath;
371
+ content = result.content;
372
+ } catch (error) {
373
+ const errorMessage = error.message || String(error);
374
+ return {
375
+ error: errorMessage,
376
+ filePath,
377
+ mode,
378
+ metrics: buildMetrics({
379
+ tool: 'smart_read',
380
+ target: filePath,
381
+ rawText: '',
382
+ compressedText: errorMessage,
383
+ }),
384
+ };
385
+ }
386
+
367
387
  const extension = path.extname(fullPath).toLowerCase();
368
388
  const mtime = getFileMtime(fullPath);
369
389
 
@@ -6,11 +6,21 @@ import { projectRoot } from '../utils/paths.js';
6
6
  import { pickRelevantLines, truncate, uniqueLines } from '../utils/text.js';
7
7
 
8
8
  const execFile = promisify(execFileCallback);
9
- const blockedPattern = /[|&;<>`\n\r]/;
9
+ const isShellDisabled = () => process.env.DEVCTX_SHELL_DISABLED === 'true';
10
+ const blockedPattern = /[|&;<>`\n\r$()]/;
10
11
  const allowedCommands = new Set(['pwd', 'ls', 'find', 'rg', 'git', 'npm', 'pnpm', 'yarn', 'bun']);
11
- const allowedGitSubcommands = new Set(['status', 'diff', 'show', 'log', 'branch', 'rev-parse']);
12
+ const allowedGitSubcommands = new Set(['status', 'diff', 'show', 'log', 'branch', 'rev-parse', 'blame']);
12
13
  const allowedPackageManagerSubcommands = new Set(['test', 'run', 'lint', 'build', 'typecheck', 'check']);
13
- const safeRunScriptPattern = /^(test|lint|build|typecheck|check|smoke|verify)(:|$)/;
14
+ const safeRunScriptPattern = /^(test|lint|build|typecheck|check|smoke|verify|eval)(:|$)/;
15
+ const dangerousPatterns = [
16
+ /rm\s+-rf/i,
17
+ /sudo/i,
18
+ /curl.*\|/i,
19
+ /wget.*\|/i,
20
+ /eval/i,
21
+ /exec/i,
22
+ ];
23
+ const MAX_COMMAND_LENGTH = 500;
14
24
 
15
25
  const tokenize = (command) => {
16
26
  const tokens = [];
@@ -67,12 +77,26 @@ const tokenize = (command) => {
67
77
  };
68
78
 
69
79
  const validateCommand = (command, tokens) => {
80
+ if (isShellDisabled()) {
81
+ return 'Shell execution is disabled (DEVCTX_SHELL_DISABLED=true)';
82
+ }
83
+
70
84
  if (!command.trim()) {
71
85
  return 'Command is empty';
72
86
  }
73
87
 
74
- if (blockedPattern.test(command) || command.includes('$(')) {
75
- return 'Shell operators are not allowed';
88
+ if (command.length > MAX_COMMAND_LENGTH) {
89
+ return `Command too long (max ${MAX_COMMAND_LENGTH} chars)`;
90
+ }
91
+
92
+ if (blockedPattern.test(command)) {
93
+ return 'Shell operators are not allowed (|, &, ;, <, >, `, $, (, ))';
94
+ }
95
+
96
+ for (const pattern of dangerousPatterns) {
97
+ if (pattern.test(command)) {
98
+ return `Dangerous pattern detected: ${pattern.source}`;
99
+ }
76
100
  }
77
101
 
78
102
  if (tokens.length === 0) {
@@ -82,11 +106,11 @@ const validateCommand = (command, tokens) => {
82
106
  const [baseCommand, subcommand, thirdToken] = tokens;
83
107
 
84
108
  if (!allowedCommands.has(baseCommand)) {
85
- return `Command not allowed: ${baseCommand}`;
109
+ return `Command not allowed: ${baseCommand}. Allowed: ${[...allowedCommands].join(', ')}`;
86
110
  }
87
111
 
88
112
  if (baseCommand === 'git' && !allowedGitSubcommands.has(subcommand)) {
89
- return `Git subcommand not allowed: ${subcommand ?? '(missing)'}`;
113
+ return `Git subcommand not allowed: ${subcommand ?? '(missing)'}. Allowed: ${[...allowedGitSubcommands].join(', ')}`;
90
114
  }
91
115
 
92
116
  if (baseCommand === 'find') {
@@ -99,11 +123,11 @@ const validateCommand = (command, tokens) => {
99
123
 
100
124
  if (['npm', 'pnpm', 'yarn', 'bun'].includes(baseCommand)) {
101
125
  if (!subcommand || !allowedPackageManagerSubcommands.has(subcommand)) {
102
- return `Package manager subcommand not allowed: ${subcommand ?? '(missing)'}`;
126
+ return `Package manager subcommand not allowed: ${subcommand ?? '(missing)'}. Allowed: ${[...allowedPackageManagerSubcommands].join(', ')}`;
103
127
  }
104
128
 
105
129
  if (subcommand === 'run' && (!thirdToken || !safeRunScriptPattern.test(thirdToken))) {
106
- return `Package manager script not allowed: ${thirdToken ?? '(missing)'}`;
130
+ return `Package manager script not allowed: ${thirdToken ?? '(missing)'}. Allowed pattern: ${safeRunScriptPattern.source}`;
107
131
  }
108
132
  }
109
133
 
@@ -205,6 +205,7 @@ const startTurn = async ({
205
205
  })
206
206
  : null;
207
207
 
208
+
208
209
  return {
209
210
  phase: 'start',
210
211
  promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW),
@@ -0,0 +1,53 @@
1
+ // Stub for workflow tracking (to be implemented in future version)
2
+ // This avoids SQLite issues in tests while keeping the API available
3
+
4
+ export const detectWorkflowType = () => null;
5
+ export const getWorkflowBaseline = () => 0;
6
+ export const startWorkflow = () => null;
7
+ export const endWorkflow = () => null;
8
+ export const getWorkflowMetrics = () => [];
9
+ export const getWorkflowSummaryByType = () => [];
10
+ export const autoTrackWorkflow = () => null;
11
+
12
+ export const WORKFLOW_DEFINITIONS = {
13
+ debugging: {
14
+ name: 'Debugging',
15
+ description: 'Error-first, symbol-focused debugging workflow',
16
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_shell'],
17
+ minTools: 3,
18
+ baselineTokens: 150000,
19
+ pattern: /debug|error|bug|fix|fail/i,
20
+ },
21
+ 'code-review': {
22
+ name: 'Code Review',
23
+ description: 'Diff-aware, API-focused code review workflow',
24
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
25
+ minTools: 3,
26
+ baselineTokens: 200000,
27
+ pattern: /review|pr|pull.?request|approve/i,
28
+ },
29
+ refactoring: {
30
+ name: 'Refactoring',
31
+ description: 'Graph-aware, test-verified refactoring workflow',
32
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
33
+ minTools: 3,
34
+ baselineTokens: 180000,
35
+ pattern: /refactor|extract|rename|move|restructure/i,
36
+ },
37
+ testing: {
38
+ name: 'Testing',
39
+ description: 'Coverage-aware, TDD-friendly testing workflow',
40
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_context', 'smart_shell'],
41
+ minTools: 3,
42
+ baselineTokens: 120000,
43
+ pattern: /test|spec|coverage|tdd/i,
44
+ },
45
+ architecture: {
46
+ name: 'Architecture Exploration',
47
+ description: 'Index-first, minimal-detail architecture exploration',
48
+ typicalTools: ['smart_turn', 'smart_context', 'smart_search', 'smart_read', 'cross_project'],
49
+ minTools: 3,
50
+ baselineTokens: 300000,
51
+ pattern: /architect|explore|understand|structure|design/i,
52
+ },
53
+ };
@@ -0,0 +1,410 @@
1
+ import { withStateDb } from './storage/sqlite.js';
2
+
3
+ const isWorkflowTrackingAvailable = () => {
4
+ try {
5
+ return withStateDb((db) => workflowTableExists(db));
6
+ } catch {
7
+ return false;
8
+ }
9
+ };
10
+
11
+ // Workflow definitions with typical tool sequences and baselines
12
+ const WORKFLOW_DEFINITIONS = {
13
+ debugging: {
14
+ name: 'Debugging',
15
+ description: 'Error-first, symbol-focused debugging workflow',
16
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_shell'],
17
+ minTools: 3,
18
+ baselineTokens: 150000, // Typical: read 10 full files, grep output, test logs
19
+ pattern: /debug|error|bug|fix|fail/i,
20
+ },
21
+ 'code-review': {
22
+ name: 'Code Review',
23
+ description: 'Diff-aware, API-focused code review workflow',
24
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
25
+ minTools: 3,
26
+ baselineTokens: 200000, // Typical: read 15 full files, diff output, test logs
27
+ pattern: /review|pr|pull.?request|approve/i,
28
+ },
29
+ refactoring: {
30
+ name: 'Refactoring',
31
+ description: 'Graph-aware, test-verified refactoring workflow',
32
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
33
+ minTools: 3,
34
+ baselineTokens: 180000, // Typical: read 12 full files, dependency graph, test logs
35
+ pattern: /refactor|extract|rename|move|restructure/i,
36
+ },
37
+ testing: {
38
+ name: 'Testing',
39
+ description: 'Coverage-aware, TDD-friendly testing workflow',
40
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_context', 'smart_shell'],
41
+ minTools: 3,
42
+ baselineTokens: 120000, // Typical: read 8 full files, test patterns, test logs
43
+ pattern: /test|spec|coverage|tdd/i,
44
+ },
45
+ architecture: {
46
+ name: 'Architecture Exploration',
47
+ description: 'Index-first, minimal-detail architecture exploration',
48
+ typicalTools: ['smart_turn', 'smart_context', 'smart_search', 'smart_read', 'cross_project'],
49
+ minTools: 3,
50
+ baselineTokens: 300000, // Typical: read 20 full files, explore structure
51
+ pattern: /architect|explore|understand|structure|design/i,
52
+ },
53
+ };
54
+
55
+ /**
56
+ * Detect workflow type based on session goal, tools used, and patterns
57
+ */
58
+ export const detectWorkflowType = (sessionGoal, toolsUsed) => {
59
+ if (!sessionGoal && toolsUsed.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ // Try to match based on session goal
64
+ if (sessionGoal) {
65
+ for (const [type, def] of Object.entries(WORKFLOW_DEFINITIONS)) {
66
+ if (def.pattern.test(sessionGoal)) {
67
+ return type;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Try to match based on tools used
73
+ for (const [type, def] of Object.entries(WORKFLOW_DEFINITIONS)) {
74
+ const matchingTools = toolsUsed.filter((tool) => def.typicalTools.includes(tool));
75
+ if (matchingTools.length >= def.minTools) {
76
+ return type;
77
+ }
78
+ }
79
+
80
+ return null;
81
+ };
82
+
83
+ /**
84
+ * Calculate baseline tokens for a workflow type
85
+ */
86
+ export const getWorkflowBaseline = (workflowType) => {
87
+ const def = WORKFLOW_DEFINITIONS[workflowType];
88
+ return def ? def.baselineTokens : 0;
89
+ };
90
+
91
+ /**
92
+ * Start tracking a workflow
93
+ */
94
+ export const startWorkflow = (workflowType, sessionId, metadata = {}) => {
95
+ try {
96
+ return withStateDb((db) => {
97
+ if (!workflowTableExists(db)) {
98
+ return null;
99
+ }
100
+
101
+ const now = new Date().toISOString();
102
+ const stmt = db.prepare(`
103
+ INSERT INTO workflow_metrics (
104
+ workflow_type,
105
+ session_id,
106
+ start_time,
107
+ baseline_tokens,
108
+ metadata_json,
109
+ created_at
110
+ ) VALUES (?, ?, ?, ?, ?, ?)
111
+ `);
112
+
113
+ const result = stmt.run(
114
+ workflowType,
115
+ sessionId,
116
+ now,
117
+ getWorkflowBaseline(workflowType),
118
+ JSON.stringify(metadata),
119
+ now,
120
+ );
121
+
122
+ return result.lastInsertRowid;
123
+ });
124
+ } catch {
125
+ return null;
126
+ }
127
+ };
128
+
129
+ /**
130
+ * End tracking a workflow and calculate metrics
131
+ */
132
+ export const endWorkflow = (workflowId) => {
133
+ try {
134
+ return withStateDb((db) => {
135
+ if (!workflowTableExists(db)) {
136
+ return null;
137
+ }
138
+
139
+ // Get workflow start time and session
140
+ const workflow = db
141
+ .prepare(
142
+ `
143
+ SELECT workflow_type, session_id, start_time, baseline_tokens
144
+ FROM workflow_metrics
145
+ WHERE workflow_id = ?
146
+ `,
147
+ )
148
+ .get(workflowId);
149
+
150
+ if (!workflow) {
151
+ throw new Error(`Workflow ${workflowId} not found`);
152
+ }
153
+
154
+ const now = new Date().toISOString();
155
+ const startTime = new Date(workflow.start_time);
156
+ const endTime = new Date(now);
157
+ const durationMs = endTime - startTime;
158
+
159
+ // Get all metrics for this session since workflow start
160
+ const metrics = db
161
+ .prepare(
162
+ `
163
+ SELECT tool, raw_tokens, compressed_tokens, saved_tokens
164
+ FROM metrics_events
165
+ WHERE session_id = ? AND created_at >= ?
166
+ ORDER BY created_at ASC
167
+ `,
168
+ )
169
+ .all(workflow.session_id, workflow.start_time);
170
+
171
+ // Calculate totals
172
+ const rawTokens = metrics.reduce((sum, m) => sum + (m.raw_tokens || 0), 0);
173
+ const compressedTokens = metrics.reduce((sum, m) => sum + (m.compressed_tokens || 0), 0);
174
+ const savedTokens = metrics.reduce((sum, m) => sum + (m.saved_tokens || 0), 0);
175
+ const savingsPct = rawTokens > 0 ? ((savedTokens / rawTokens) * 100).toFixed(2) : 0;
176
+
177
+ // Calculate vs baseline
178
+ const baselineTokens = workflow.baseline_tokens || 0;
179
+ const vsBaselinePct = baselineTokens > 0 ? (((baselineTokens - compressedTokens) / baselineTokens) * 100).toFixed(2) : 0;
180
+
181
+ // Get unique tools used
182
+ const toolsUsed = [...new Set(metrics.map((m) => m.tool))];
183
+
184
+ // Update workflow
185
+ const stmt = db.prepare(`
186
+ UPDATE workflow_metrics
187
+ SET end_time = ?,
188
+ duration_ms = ?,
189
+ tools_used_json = ?,
190
+ steps_count = ?,
191
+ raw_tokens = ?,
192
+ compressed_tokens = ?,
193
+ saved_tokens = ?,
194
+ savings_pct = ?,
195
+ vs_baseline_pct = ?
196
+ WHERE workflow_id = ?
197
+ `);
198
+
199
+ stmt.run(now, durationMs, JSON.stringify(toolsUsed), metrics.length, rawTokens, compressedTokens, savedTokens, savingsPct, vsBaselinePct, workflowId);
200
+
201
+ return {
202
+ workflowId,
203
+ workflowType: workflow.workflow_type,
204
+ durationMs,
205
+ toolsUsed,
206
+ stepsCount: metrics.length,
207
+ rawTokens,
208
+ compressedTokens,
209
+ savedTokens,
210
+ savingsPct: Number(savingsPct),
211
+ baselineTokens,
212
+ vsBaselinePct: Number(vsBaselinePct),
213
+ };
214
+ });
215
+ } catch {
216
+ return null;
217
+ }
218
+ };
219
+
220
+ /**
221
+ * Get workflow metrics summary
222
+ */
223
+ export const getWorkflowMetrics = (options = {}) => {
224
+ try {
225
+ return withStateDb((db) => {
226
+ if (!workflowTableExists(db)) {
227
+ return [];
228
+ }
229
+ let query = `
230
+ SELECT
231
+ workflow_id,
232
+ workflow_type,
233
+ session_id,
234
+ start_time,
235
+ end_time,
236
+ duration_ms,
237
+ tools_used_json,
238
+ steps_count,
239
+ raw_tokens,
240
+ compressed_tokens,
241
+ saved_tokens,
242
+ savings_pct,
243
+ baseline_tokens,
244
+ vs_baseline_pct,
245
+ metadata_json,
246
+ created_at
247
+ FROM workflow_metrics
248
+ WHERE 1=1
249
+ `;
250
+
251
+ const params = [];
252
+
253
+ if (options.workflowType) {
254
+ query += ' AND workflow_type = ?';
255
+ params.push(options.workflowType);
256
+ }
257
+
258
+ if (options.sessionId) {
259
+ query += ' AND session_id = ?';
260
+ params.push(options.sessionId);
261
+ }
262
+
263
+ if (options.completed !== undefined) {
264
+ if (options.completed) {
265
+ query += ' AND end_time IS NOT NULL';
266
+ } else {
267
+ query += ' AND end_time IS NULL';
268
+ }
269
+ }
270
+
271
+ query += ' ORDER BY created_at DESC';
272
+
273
+ if (options.limit) {
274
+ query += ' LIMIT ?';
275
+ params.push(options.limit);
276
+ }
277
+
278
+ const workflows = db.prepare(query).all(...params);
279
+
280
+ return workflows.map((w) => ({
281
+ ...w,
282
+ toolsUsed: JSON.parse(w.tools_used_json || '[]'),
283
+ metadata: JSON.parse(w.metadata_json || '{}'),
284
+ }));
285
+ });
286
+ } catch {
287
+ return [];
288
+ }
289
+ };
290
+
291
+ /**
292
+ * Get workflow summary by type
293
+ */
294
+ export const getWorkflowSummaryByType = () => {
295
+ try {
296
+ return withStateDb((db) => {
297
+ if (!workflowTableExists(db)) {
298
+ return [];
299
+ }
300
+ const summary = db
301
+ .prepare(
302
+ `
303
+ SELECT
304
+ workflow_type,
305
+ COUNT(*) as count,
306
+ SUM(raw_tokens) as total_raw_tokens,
307
+ SUM(compressed_tokens) as total_compressed_tokens,
308
+ SUM(saved_tokens) as total_saved_tokens,
309
+ AVG(savings_pct) as avg_savings_pct,
310
+ SUM(baseline_tokens) as total_baseline_tokens,
311
+ AVG(vs_baseline_pct) as avg_vs_baseline_pct,
312
+ AVG(duration_ms) as avg_duration_ms,
313
+ AVG(steps_count) as avg_steps_count
314
+ FROM workflow_metrics
315
+ WHERE end_time IS NOT NULL
316
+ GROUP BY workflow_type
317
+ ORDER BY count DESC
318
+ `,
319
+ )
320
+ .all();
321
+
322
+ return summary.map((s) => ({
323
+ ...s,
324
+ avgSavingsPct: Number(s.avg_savings_pct?.toFixed(2) || 0),
325
+ avgVsBaselinePct: Number(s.avg_vs_baseline_pct?.toFixed(2) || 0),
326
+ avgDurationMs: Math.round(s.avg_duration_ms || 0),
327
+ avgStepsCount: Math.round(s.avg_steps_count || 0),
328
+ }));
329
+ });
330
+ } catch {
331
+ return [];
332
+ }
333
+ };
334
+
335
+ /**
336
+ * Check if workflow_metrics table exists
337
+ */
338
+ const workflowTableExists = (db) => {
339
+ try {
340
+ const result = db
341
+ .prepare(
342
+ `
343
+ SELECT name FROM sqlite_master
344
+ WHERE type='table' AND name='workflow_metrics'
345
+ `,
346
+ )
347
+ .get();
348
+ return Boolean(result);
349
+ } catch {
350
+ return false;
351
+ }
352
+ };
353
+
354
+ /**
355
+ * Auto-detect and track workflow from session
356
+ */
357
+ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
358
+ try {
359
+ return withStateDb((db) => {
360
+ // Check if table exists (migration v5)
361
+ if (!workflowTableExists(db)) {
362
+ return null;
363
+ }
364
+
365
+ // Check if workflow already tracked for this session
366
+ const existing = db
367
+ .prepare(
368
+ `
369
+ SELECT workflow_id
370
+ FROM workflow_metrics
371
+ WHERE session_id = ? AND end_time IS NULL
372
+ ORDER BY created_at DESC
373
+ LIMIT 1
374
+ `,
375
+ )
376
+ .get(sessionId);
377
+
378
+ if (existing) {
379
+ return existing.workflow_id;
380
+ }
381
+
382
+ // Get tools used so far in this session
383
+ const metrics = db
384
+ .prepare(
385
+ `
386
+ SELECT DISTINCT tool
387
+ FROM metrics_events
388
+ WHERE session_id = ?
389
+ `,
390
+ )
391
+ .all(sessionId);
392
+
393
+ const toolsUsed = metrics.map((m) => m.tool);
394
+
395
+ // Detect workflow type
396
+ const workflowType = detectWorkflowType(sessionGoal, toolsUsed);
397
+
398
+ if (!workflowType) {
399
+ return null;
400
+ }
401
+
402
+ // Start tracking
403
+ return startWorkflow(workflowType, sessionId, { autoDetected: true, goal: sessionGoal });
404
+ });
405
+ } catch {
406
+ return null;
407
+ }
408
+ };
409
+
410
+ export { WORKFLOW_DEFINITIONS };