clementine-agent 1.0.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.
Files changed (190) hide show
  1. package/.env.example +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +795 -0
  4. package/dist/agent/agent-manager.d.ts +69 -0
  5. package/dist/agent/agent-manager.js +441 -0
  6. package/dist/agent/assistant.d.ts +225 -0
  7. package/dist/agent/assistant.js +3888 -0
  8. package/dist/agent/auto-update.d.ts +32 -0
  9. package/dist/agent/auto-update.js +186 -0
  10. package/dist/agent/daily-planner.d.ts +24 -0
  11. package/dist/agent/daily-planner.js +379 -0
  12. package/dist/agent/execution-advisor.d.ts +10 -0
  13. package/dist/agent/execution-advisor.js +272 -0
  14. package/dist/agent/hooks.d.ts +45 -0
  15. package/dist/agent/hooks.js +564 -0
  16. package/dist/agent/insight-engine.d.ts +66 -0
  17. package/dist/agent/insight-engine.js +225 -0
  18. package/dist/agent/intent-classifier.d.ts +48 -0
  19. package/dist/agent/intent-classifier.js +214 -0
  20. package/dist/agent/link-extractor.d.ts +19 -0
  21. package/dist/agent/link-extractor.js +90 -0
  22. package/dist/agent/mcp-bridge.d.ts +62 -0
  23. package/dist/agent/mcp-bridge.js +435 -0
  24. package/dist/agent/metacognition.d.ts +66 -0
  25. package/dist/agent/metacognition.js +221 -0
  26. package/dist/agent/orchestrator.d.ts +81 -0
  27. package/dist/agent/orchestrator.js +790 -0
  28. package/dist/agent/profiles.d.ts +22 -0
  29. package/dist/agent/profiles.js +91 -0
  30. package/dist/agent/prompt-cache.d.ts +24 -0
  31. package/dist/agent/prompt-cache.js +68 -0
  32. package/dist/agent/prompt-evolver.d.ts +28 -0
  33. package/dist/agent/prompt-evolver.js +279 -0
  34. package/dist/agent/role-scaffolds.d.ts +28 -0
  35. package/dist/agent/role-scaffolds.js +433 -0
  36. package/dist/agent/safe-restart.d.ts +41 -0
  37. package/dist/agent/safe-restart.js +150 -0
  38. package/dist/agent/self-improve.d.ts +66 -0
  39. package/dist/agent/self-improve.js +1706 -0
  40. package/dist/agent/session-event-log.d.ts +114 -0
  41. package/dist/agent/session-event-log.js +233 -0
  42. package/dist/agent/skill-extractor.d.ts +72 -0
  43. package/dist/agent/skill-extractor.js +435 -0
  44. package/dist/agent/source-mods.d.ts +61 -0
  45. package/dist/agent/source-mods.js +230 -0
  46. package/dist/agent/source-preflight.d.ts +25 -0
  47. package/dist/agent/source-preflight.js +100 -0
  48. package/dist/agent/stall-guard.d.ts +62 -0
  49. package/dist/agent/stall-guard.js +109 -0
  50. package/dist/agent/strategic-planner.d.ts +60 -0
  51. package/dist/agent/strategic-planner.js +352 -0
  52. package/dist/agent/team-bus.d.ts +89 -0
  53. package/dist/agent/team-bus.js +556 -0
  54. package/dist/agent/team-router.d.ts +26 -0
  55. package/dist/agent/team-router.js +37 -0
  56. package/dist/agent/tool-loop-detector.d.ts +59 -0
  57. package/dist/agent/tool-loop-detector.js +242 -0
  58. package/dist/agent/workflow-runner.d.ts +36 -0
  59. package/dist/agent/workflow-runner.js +317 -0
  60. package/dist/agent/workflow-variables.d.ts +16 -0
  61. package/dist/agent/workflow-variables.js +62 -0
  62. package/dist/channels/discord-agent-bot.d.ts +101 -0
  63. package/dist/channels/discord-agent-bot.js +881 -0
  64. package/dist/channels/discord-bot-manager.d.ts +80 -0
  65. package/dist/channels/discord-bot-manager.js +262 -0
  66. package/dist/channels/discord-utils.d.ts +51 -0
  67. package/dist/channels/discord-utils.js +293 -0
  68. package/dist/channels/discord.d.ts +12 -0
  69. package/dist/channels/discord.js +1832 -0
  70. package/dist/channels/slack-agent-bot.d.ts +73 -0
  71. package/dist/channels/slack-agent-bot.js +320 -0
  72. package/dist/channels/slack-bot-manager.d.ts +66 -0
  73. package/dist/channels/slack-bot-manager.js +236 -0
  74. package/dist/channels/slack-utils.d.ts +39 -0
  75. package/dist/channels/slack-utils.js +189 -0
  76. package/dist/channels/slack.d.ts +11 -0
  77. package/dist/channels/slack.js +196 -0
  78. package/dist/channels/telegram.d.ts +10 -0
  79. package/dist/channels/telegram.js +235 -0
  80. package/dist/channels/webhook.d.ts +9 -0
  81. package/dist/channels/webhook.js +78 -0
  82. package/dist/channels/whatsapp.d.ts +11 -0
  83. package/dist/channels/whatsapp.js +181 -0
  84. package/dist/cli/chat.d.ts +14 -0
  85. package/dist/cli/chat.js +220 -0
  86. package/dist/cli/cron.d.ts +17 -0
  87. package/dist/cli/cron.js +552 -0
  88. package/dist/cli/dashboard.d.ts +15 -0
  89. package/dist/cli/dashboard.js +17677 -0
  90. package/dist/cli/index.d.ts +3 -0
  91. package/dist/cli/index.js +2474 -0
  92. package/dist/cli/routes/delegations.d.ts +19 -0
  93. package/dist/cli/routes/delegations.js +154 -0
  94. package/dist/cli/routes/digest.d.ts +17 -0
  95. package/dist/cli/routes/digest.js +375 -0
  96. package/dist/cli/routes/goals.d.ts +14 -0
  97. package/dist/cli/routes/goals.js +258 -0
  98. package/dist/cli/routes/workflows.d.ts +18 -0
  99. package/dist/cli/routes/workflows.js +97 -0
  100. package/dist/cli/setup.d.ts +8 -0
  101. package/dist/cli/setup.js +619 -0
  102. package/dist/cli/tunnel.d.ts +35 -0
  103. package/dist/cli/tunnel.js +141 -0
  104. package/dist/config.d.ts +145 -0
  105. package/dist/config.js +278 -0
  106. package/dist/events/bus.d.ts +43 -0
  107. package/dist/events/bus.js +136 -0
  108. package/dist/gateway/cron-scheduler.d.ts +166 -0
  109. package/dist/gateway/cron-scheduler.js +1767 -0
  110. package/dist/gateway/delivery-queue.d.ts +30 -0
  111. package/dist/gateway/delivery-queue.js +110 -0
  112. package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
  113. package/dist/gateway/heartbeat-scheduler.js +1298 -0
  114. package/dist/gateway/heartbeat.d.ts +3 -0
  115. package/dist/gateway/heartbeat.js +3 -0
  116. package/dist/gateway/lanes.d.ts +24 -0
  117. package/dist/gateway/lanes.js +76 -0
  118. package/dist/gateway/notifications.d.ts +29 -0
  119. package/dist/gateway/notifications.js +75 -0
  120. package/dist/gateway/router.d.ts +210 -0
  121. package/dist/gateway/router.js +1330 -0
  122. package/dist/index.d.ts +12 -0
  123. package/dist/index.js +1015 -0
  124. package/dist/memory/chunker.d.ts +28 -0
  125. package/dist/memory/chunker.js +226 -0
  126. package/dist/memory/consolidation.d.ts +44 -0
  127. package/dist/memory/consolidation.js +171 -0
  128. package/dist/memory/context-assembler.d.ts +50 -0
  129. package/dist/memory/context-assembler.js +149 -0
  130. package/dist/memory/embeddings.d.ts +38 -0
  131. package/dist/memory/embeddings.js +180 -0
  132. package/dist/memory/graph-store.d.ts +66 -0
  133. package/dist/memory/graph-store.js +613 -0
  134. package/dist/memory/mmr.d.ts +21 -0
  135. package/dist/memory/mmr.js +75 -0
  136. package/dist/memory/search.d.ts +26 -0
  137. package/dist/memory/search.js +67 -0
  138. package/dist/memory/store.d.ts +530 -0
  139. package/dist/memory/store.js +2022 -0
  140. package/dist/security/integrity.d.ts +24 -0
  141. package/dist/security/integrity.js +58 -0
  142. package/dist/security/patterns.d.ts +34 -0
  143. package/dist/security/patterns.js +110 -0
  144. package/dist/security/scanner.d.ts +32 -0
  145. package/dist/security/scanner.js +263 -0
  146. package/dist/tools/admin-tools.d.ts +12 -0
  147. package/dist/tools/admin-tools.js +1278 -0
  148. package/dist/tools/external-tools.d.ts +11 -0
  149. package/dist/tools/external-tools.js +1327 -0
  150. package/dist/tools/goal-tools.d.ts +9 -0
  151. package/dist/tools/goal-tools.js +159 -0
  152. package/dist/tools/mcp-server.d.ts +13 -0
  153. package/dist/tools/mcp-server.js +141 -0
  154. package/dist/tools/memory-tools.d.ts +10 -0
  155. package/dist/tools/memory-tools.js +568 -0
  156. package/dist/tools/session-tools.d.ts +6 -0
  157. package/dist/tools/session-tools.js +146 -0
  158. package/dist/tools/shared.d.ts +216 -0
  159. package/dist/tools/shared.js +340 -0
  160. package/dist/tools/team-tools.d.ts +6 -0
  161. package/dist/tools/team-tools.js +447 -0
  162. package/dist/tools/tool-meta.d.ts +34 -0
  163. package/dist/tools/tool-meta.js +133 -0
  164. package/dist/tools/vault-tools.d.ts +8 -0
  165. package/dist/tools/vault-tools.js +457 -0
  166. package/dist/types.d.ts +716 -0
  167. package/dist/types.js +16 -0
  168. package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
  169. package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
  170. package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
  171. package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
  172. package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
  173. package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
  174. package/dist/vault-migrations/helpers.d.ts +14 -0
  175. package/dist/vault-migrations/helpers.js +44 -0
  176. package/dist/vault-migrations/runner.d.ts +14 -0
  177. package/dist/vault-migrations/runner.js +139 -0
  178. package/dist/vault-migrations/types.d.ts +42 -0
  179. package/dist/vault-migrations/types.js +9 -0
  180. package/install.sh +320 -0
  181. package/package.json +84 -0
  182. package/scripts/postinstall.js +125 -0
  183. package/vault/00-System/AGENTS.md +66 -0
  184. package/vault/00-System/CRON.md +71 -0
  185. package/vault/00-System/HEARTBEAT.md +58 -0
  186. package/vault/00-System/MEMORY.md +16 -0
  187. package/vault/00-System/SOUL.md +96 -0
  188. package/vault/05-Tasks/TASKS.md +19 -0
  189. package/vault/06-Templates/_Daily-Template.md +28 -0
  190. package/vault/06-Templates/_People-Template.md +22 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Clementine TypeScript — Insight Engine.
3
+ *
4
+ * Proactive conversation initiation: runs during heartbeat ticks to
5
+ * identify events or patterns the user should know about. Generates
6
+ * urgency-rated insights and dispatches via NotificationDispatcher.
7
+ *
8
+ * Throttling: max 3 proactive messages per day, minimum 2-hour cooldown.
9
+ * Adapts based on user acknowledgment (doubles cooldown after 3 ignored).
10
+ */
11
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import pino from 'pino';
14
+ import { GOALS_DIR, BASE_DIR } from '../config.js';
15
+ const logger = pino({ name: 'clementine.insight-engine' });
16
+ const BASE_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
17
+ const MAX_DAILY_INSIGHTS = 3;
18
+ const UNACKED_THRESHOLD = 3; // double cooldown after this many ignored
19
+ /**
20
+ * Check if it's too soon to send another proactive message.
21
+ */
22
+ export function canSendInsight(state) {
23
+ const today = new Date().toISOString().slice(0, 10);
24
+ // Reset daily count on new day
25
+ if (state.currentDate !== today) {
26
+ state.sentToday = [];
27
+ state.currentDate = today;
28
+ // Don't reset cooldownMultiplier — that persists across days
29
+ }
30
+ // Daily limit
31
+ if (state.sentToday.length >= MAX_DAILY_INSIGHTS) {
32
+ return false;
33
+ }
34
+ // Cooldown check
35
+ if (state.lastSentAt) {
36
+ const elapsed = Date.now() - new Date(state.lastSentAt).getTime();
37
+ const cooldown = BASE_COOLDOWN_MS * state.cooldownMultiplier;
38
+ if (elapsed < cooldown) {
39
+ return false;
40
+ }
41
+ }
42
+ return true;
43
+ }
44
+ /**
45
+ * Record that a proactive message was sent.
46
+ */
47
+ export function recordInsightSent(state) {
48
+ const now = new Date().toISOString();
49
+ state.sentToday.push(now);
50
+ state.lastSentAt = now;
51
+ state.unackedCount++;
52
+ }
53
+ /**
54
+ * Record that the user acknowledged a proactive message (replied to it).
55
+ * Resets the unacked counter and lowers cooldown.
56
+ */
57
+ export function recordInsightAcked(state) {
58
+ state.unackedCount = 0;
59
+ state.cooldownMultiplier = 1;
60
+ }
61
+ /**
62
+ * Check if cooldown should be increased due to ignored messages.
63
+ */
64
+ export function maybeIncreaseCooldown(state) {
65
+ if (state.unackedCount >= UNACKED_THRESHOLD) {
66
+ state.cooldownMultiplier = Math.min(state.cooldownMultiplier * 2, 8); // cap at 16 hours
67
+ state.unackedCount = 0; // reset counter after adjustment
68
+ logger.info({ multiplier: state.cooldownMultiplier }, 'Proactive message cooldown increased — messages going unacknowledged');
69
+ }
70
+ }
71
+ /**
72
+ * Gather raw signals for insight generation (no LLM call — pure data).
73
+ * Returns structured event summaries that can be passed to an LLM for urgency rating.
74
+ */
75
+ export function gatherInsightSignals(gateway) {
76
+ const signals = [];
77
+ const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
78
+ // 1. Check for goal progress/failures
79
+ try {
80
+ const progressDir = path.join(GOALS_DIR, 'progress');
81
+ if (existsSync(progressDir)) {
82
+ const files = readdirSync(progressDir).filter(f => f.endsWith('.progress.jsonl'));
83
+ for (const file of files.slice(0, 5)) {
84
+ const lines = readFileSync(path.join(progressDir, file), 'utf-8').trim().split('\n').filter(Boolean);
85
+ const recent = lines.slice(-3);
86
+ for (const line of recent) {
87
+ try {
88
+ const entry = JSON.parse(line);
89
+ if (new Date(entry.timestamp) > new Date(twoHoursAgo)) {
90
+ if (entry.status === 'error') {
91
+ signals.push(`Goal work session failed: ${entry.focus} (${entry.resultSnippet?.slice(0, 100)})`);
92
+ }
93
+ else if (entry.status === 'progress') {
94
+ signals.push(`Goal progress: ${entry.focus}`);
95
+ }
96
+ }
97
+ }
98
+ catch {
99
+ continue;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch { /* non-fatal */ }
106
+ // 2. Check for cron job failures
107
+ try {
108
+ const runLogDir = path.join(BASE_DIR, 'cron', 'run-log');
109
+ if (existsSync(runLogDir)) {
110
+ const logFiles = readdirSync(runLogDir).filter(f => f.endsWith('.jsonl')).slice(0, 10);
111
+ for (const file of logFiles) {
112
+ const lines = readFileSync(path.join(runLogDir, file), 'utf-8').trim().split('\n').filter(Boolean);
113
+ const lastLine = lines[lines.length - 1];
114
+ if (!lastLine)
115
+ continue;
116
+ try {
117
+ const entry = JSON.parse(lastLine);
118
+ if (new Date(entry.timestamp) > new Date(twoHoursAgo) && entry.status === 'error') {
119
+ signals.push(`Cron job "${entry.jobName || file.replace('.jsonl', '')}" failed: ${(entry.error || '').slice(0, 100)}`);
120
+ }
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ catch { /* non-fatal */ }
129
+ // 3. Check recent activity for patterns worth surfacing
130
+ try {
131
+ const activity = gateway.getRecentActivity(twoHoursAgo);
132
+ const sessionCount = new Set(activity.map(a => a.sessionKey)).size;
133
+ if (sessionCount === 0) {
134
+ // No recent activity — could note quiet period if there are pending goals
135
+ try {
136
+ if (existsSync(GOALS_DIR)) {
137
+ const goals = readdirSync(GOALS_DIR).filter(f => f.endsWith('.json'));
138
+ const activeHighPriority = goals.filter(f => {
139
+ try {
140
+ const g = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
141
+ return g.status === 'active' && g.priority === 'high';
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ });
147
+ if (activeHighPriority.length > 0) {
148
+ signals.push(`Quiet period: ${activeHighPriority.length} high-priority goal(s) active but no recent user interaction`);
149
+ }
150
+ }
151
+ }
152
+ catch { /* non-fatal */ }
153
+ }
154
+ }
155
+ catch { /* non-fatal */ }
156
+ // 4. Check advisor events (model escalations, circuit breakers)
157
+ try {
158
+ const eventsPath = path.join(BASE_DIR, 'cron', 'advisor-events.jsonl');
159
+ if (existsSync(eventsPath)) {
160
+ const lines = readFileSync(eventsPath, 'utf-8').trim().split('\n').filter(Boolean);
161
+ const recent = lines.slice(-5);
162
+ for (const line of recent) {
163
+ try {
164
+ const evt = JSON.parse(line);
165
+ if (new Date(evt.timestamp) > new Date(twoHoursAgo)) {
166
+ if (evt.type === 'circuit-breaker') {
167
+ signals.push(`Circuit breaker tripped for "${evt.jobName}": ${evt.detail}`);
168
+ }
169
+ else if (evt.type === 'escalation') {
170
+ signals.push(`Job "${evt.jobName}" escalated: ${evt.detail}`);
171
+ }
172
+ }
173
+ }
174
+ catch {
175
+ continue;
176
+ }
177
+ }
178
+ }
179
+ }
180
+ catch { /* non-fatal */ }
181
+ return signals;
182
+ }
183
+ /**
184
+ * Build a prompt for urgency rating (to be sent to a lightweight LLM).
185
+ * Returns null if there are no signals worth evaluating.
186
+ */
187
+ export function buildInsightPrompt(signals) {
188
+ if (signals.length === 0)
189
+ return null;
190
+ return (`You are a proactive assistant analyzing recent events for your owner.\n\n` +
191
+ `## Recent Events\n${signals.map(s => `- ${s}`).join('\n')}\n\n` +
192
+ `## Task\n` +
193
+ `Based on these events, determine if the user should be notified.\n\n` +
194
+ `Rate urgency 1-5:\n` +
195
+ `1 = trivial (don't notify)\n` +
196
+ `2 = low (don't notify)\n` +
197
+ `3 = informational (batch into next check-in)\n` +
198
+ `4 = important (notify during active hours)\n` +
199
+ `5 = critical (notify immediately)\n\n` +
200
+ `If urgency >= 3, write a brief, conversational message (1-3 sentences) for the user.\n\n` +
201
+ `Output ONLY JSON:\n` +
202
+ `{ "urgency": <1-5>, "message": "<text or null>", "source": "<which event triggered this>" }`);
203
+ }
204
+ /**
205
+ * Parse the LLM response into an InsightResult.
206
+ */
207
+ export function parseInsightResponse(response) {
208
+ try {
209
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
210
+ if (!jsonMatch)
211
+ return null;
212
+ const parsed = JSON.parse(jsonMatch[0]);
213
+ if (!parsed.urgency || parsed.urgency < 3 || !parsed.message)
214
+ return null;
215
+ return {
216
+ message: parsed.message,
217
+ urgency: parsed.urgency,
218
+ source: parsed.source || 'unknown',
219
+ };
220
+ }
221
+ catch {
222
+ return null;
223
+ }
224
+ }
225
+ //# sourceMappingURL=insight-engine.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Clementine TypeScript — Intent Classifier & Response Strategy Router.
3
+ *
4
+ * Lightweight heuristic classifier that determines message intent BEFORE
5
+ * the main SDK query, enabling dynamic tuning of maxTurns, effort level,
6
+ * and response formatting guidance.
7
+ */
8
+ export type IntentType = 'question' | 'task' | 'feedback' | 'casual' | 'followup' | 'correction';
9
+ export type ResponseStrategy = 'brief-answer' | 'structured-response' | 'plan-and-execute' | 'acknowledge-and-adapt' | 'conversational';
10
+ export interface IntentClassification {
11
+ type: IntentType;
12
+ confidence: number;
13
+ suggestedStrategy: ResponseStrategy;
14
+ /** Suggested maxTurns for this intent type */
15
+ suggestedMaxTurns: number;
16
+ /** Suggested effort level for the SDK */
17
+ suggestedEffort: 'low' | 'medium' | 'high';
18
+ }
19
+ /**
20
+ * Classify a user message's intent using heuristics.
21
+ *
22
+ * @param text - The user's raw message
23
+ * @param recentExchanges - Recent conversation history for followup detection
24
+ */
25
+ export declare function classifyIntent(text: string, recentExchanges?: Array<{
26
+ user: string;
27
+ assistant: string;
28
+ }>): IntentClassification;
29
+ /**
30
+ * Generate system prompt guidance text based on the response strategy.
31
+ * Injected into the system prompt to steer the agent's response style.
32
+ */
33
+ export declare function getStrategyGuidance(strategy: ResponseStrategy): string;
34
+ /**
35
+ * Generate a follow-up suggestion prompt suffix based on completed work.
36
+ *
37
+ * @param toolCallCount - Number of tool calls made during the query
38
+ * @param responseLength - Length of the response text
39
+ * @param hasActiveGoals - Whether there are relevant active goals
40
+ * @param pendingWorkItems - Number of pending items in the work queue
41
+ */
42
+ export declare function buildFollowUpContext(opts: {
43
+ toolCallCount: number;
44
+ responseLength: number;
45
+ hasActiveGoals: boolean;
46
+ pendingWorkItems: number;
47
+ }): string | null;
48
+ //# sourceMappingURL=intent-classifier.d.ts.map
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Clementine TypeScript — Intent Classifier & Response Strategy Router.
3
+ *
4
+ * Lightweight heuristic classifier that determines message intent BEFORE
5
+ * the main SDK query, enabling dynamic tuning of maxTurns, effort level,
6
+ * and response formatting guidance.
7
+ */
8
+ // ── Constants ─────────────────────────────────────────────────────────
9
+ const QUESTION_STARTERS = /^(what|who|where|when|why|how|is|are|was|were|do|does|did|can|could|would|should|will|which|have|has|tell me)\b/i;
10
+ const ACTION_VERBS = /\b(create|write|send|update|deploy|build|fix|add|remove|delete|install|configure|set up|draft|schedule|implement|refactor|change|move|rename|edit|modify|generate|make|run|execute|push|pull|check|review|analyze|search|find|fetch|get|list|show)\b/i;
11
+ const FEEDBACK_POSITIVE = /^(thanks|thank you|ty|thx|great|perfect|nice|awesome|love it|good job|well done|excellent|looks good|lgtm|cool|sweet|beautiful|brilliant|exactly|yes|yep|yeah|correct|right|ok)\b/i;
12
+ const FEEDBACK_NEGATIVE = /^(no|nope|wrong|not right|that's not|that isn't|ugh|nah|bad|incorrect|stop|don't|actually no|wait no)\b/i;
13
+ const CORRECTION_PATTERNS = /\b(actually|instead|rather|not that|I meant|I said|what I wanted|correction|try again|redo|no I|that's wrong)\b/i;
14
+ const CASUAL_PATTERNS = /^(hi|hey|hello|good morning|good afternoon|good evening|sup|yo|what's up|howdy|morning|gm|gn|good night)\b/i;
15
+ // ── Classifier ────────────────────────────────────────────────────────
16
+ /**
17
+ * Classify a user message's intent using heuristics.
18
+ *
19
+ * @param text - The user's raw message
20
+ * @param recentExchanges - Recent conversation history for followup detection
21
+ */
22
+ export function classifyIntent(text, recentExchanges) {
23
+ const trimmed = text.trim();
24
+ const wordCount = trimmed.split(/\s+/).length;
25
+ const endsWithQuestion = trimmed.endsWith('?');
26
+ const hasRecentContext = (recentExchanges?.length ?? 0) > 0;
27
+ // Score each intent type
28
+ const scores = {
29
+ question: 0,
30
+ task: 0,
31
+ feedback: 0,
32
+ casual: 0,
33
+ followup: 0,
34
+ correction: 0,
35
+ };
36
+ // ── Question signals ──
37
+ if (endsWithQuestion)
38
+ scores.question += 0.4;
39
+ if (QUESTION_STARTERS.test(trimmed))
40
+ scores.question += 0.35;
41
+ if (wordCount < 15 && endsWithQuestion)
42
+ scores.question += 0.15;
43
+ // ── Task signals ──
44
+ if (ACTION_VERBS.test(trimmed))
45
+ scores.task += 0.35;
46
+ if (wordCount > 20)
47
+ scores.task += 0.1; // Longer messages tend to be tasks
48
+ if (trimmed.includes('\n') || trimmed.includes('```'))
49
+ scores.task += 0.15; // Multi-line or code blocks
50
+ if (/\b(please|can you|could you|I need|I want)\b/i.test(trimmed))
51
+ scores.task += 0.2;
52
+ // If it has action verbs AND doesn't end with ?, lean toward task over question
53
+ if (ACTION_VERBS.test(trimmed) && !endsWithQuestion)
54
+ scores.task += 0.15;
55
+ // ── Feedback signals ──
56
+ if (FEEDBACK_POSITIVE.test(trimmed))
57
+ scores.feedback += 0.5;
58
+ if (FEEDBACK_NEGATIVE.test(trimmed))
59
+ scores.feedback += 0.4;
60
+ if (wordCount <= 5)
61
+ scores.feedback += 0.15; // Short messages are often feedback
62
+ // ── Casual signals ──
63
+ if (CASUAL_PATTERNS.test(trimmed))
64
+ scores.casual += 0.6;
65
+ if (wordCount <= 3 && !endsWithQuestion && !ACTION_VERBS.test(trimmed))
66
+ scores.casual += 0.2;
67
+ // ── Correction signals ──
68
+ if (CORRECTION_PATTERNS.test(trimmed) && hasRecentContext)
69
+ scores.correction += 0.5;
70
+ if (FEEDBACK_NEGATIVE.test(trimmed) && hasRecentContext && wordCount > 5) {
71
+ scores.correction += 0.3; // Negative feedback + more detail = likely correction
72
+ scores.feedback -= 0.2; // Reduce feedback score
73
+ }
74
+ // ── Followup signals ──
75
+ if (hasRecentContext) {
76
+ const lastAssistant = recentExchanges[recentExchanges.length - 1]?.assistant ?? '';
77
+ // References to previous response content
78
+ if (/\b(that|this|it|those|these|the one|above|earlier)\b/i.test(trimmed)) {
79
+ scores.followup += 0.25;
80
+ }
81
+ // Short messages in active conversation are likely followups
82
+ if (wordCount < 10 && !CASUAL_PATTERNS.test(trimmed)) {
83
+ scores.followup += 0.15;
84
+ }
85
+ // If the assistant asked a question and the user responds briefly
86
+ if (lastAssistant.endsWith('?') && wordCount < 20) {
87
+ scores.followup += 0.3;
88
+ }
89
+ }
90
+ // ── Pick winner ──
91
+ let bestType = 'question';
92
+ let bestScore = 0;
93
+ for (const [type, score] of Object.entries(scores)) {
94
+ if (score > bestScore) {
95
+ bestType = type;
96
+ bestScore = score;
97
+ }
98
+ }
99
+ // Default fallback: if no strong signal, classify based on simple heuristics
100
+ if (bestScore < 0.2) {
101
+ if (endsWithQuestion)
102
+ bestType = 'question';
103
+ else if (wordCount > 15)
104
+ bestType = 'task';
105
+ else
106
+ bestType = 'question'; // Safe default
107
+ bestScore = 0.3;
108
+ }
109
+ const confidence = Math.min(bestScore, 1);
110
+ return {
111
+ type: bestType,
112
+ confidence,
113
+ suggestedStrategy: intentToStrategy(bestType),
114
+ suggestedMaxTurns: intentToMaxTurns(bestType),
115
+ suggestedEffort: intentToEffort(bestType),
116
+ };
117
+ }
118
+ // ── Strategy Mapping ──────────────────────────────────────────────────
119
+ function intentToStrategy(intent) {
120
+ switch (intent) {
121
+ case 'question': return 'brief-answer';
122
+ case 'task': return 'plan-and-execute';
123
+ case 'feedback': return 'acknowledge-and-adapt';
124
+ case 'casual': return 'conversational';
125
+ case 'followup': return 'brief-answer';
126
+ case 'correction': return 'acknowledge-and-adapt';
127
+ }
128
+ }
129
+ function intentToMaxTurns(intent) {
130
+ switch (intent) {
131
+ case 'question': return 8;
132
+ case 'task': return 15;
133
+ case 'feedback': return 3;
134
+ case 'casual': return 3;
135
+ case 'followup': return 8;
136
+ case 'correction': return 10;
137
+ }
138
+ }
139
+ function intentToEffort(intent) {
140
+ switch (intent) {
141
+ case 'question': return 'medium';
142
+ case 'task': return 'high';
143
+ case 'feedback': return 'low';
144
+ case 'casual': return 'low';
145
+ case 'followup': return 'medium';
146
+ case 'correction': return 'medium';
147
+ }
148
+ }
149
+ // ── Response Strategy Guidance ────────────────────────────────────────
150
+ /**
151
+ * Generate system prompt guidance text based on the response strategy.
152
+ * Injected into the system prompt to steer the agent's response style.
153
+ */
154
+ export function getStrategyGuidance(strategy) {
155
+ switch (strategy) {
156
+ case 'brief-answer':
157
+ return `## Response Style: Direct Answer
158
+ Respond concisely — 1-3 sentences for simple questions, a short paragraph with key details for complex ones.
159
+ If you know the answer, give it immediately — no preamble.
160
+ If you need to look something up, say what you're checking and why ("Let me check memory for that — I think we discussed it last week"), then share what you found.`;
161
+ case 'structured-response':
162
+ return `## Response Style: Structured
163
+ Use headers, bullet points, or numbered lists for clarity.
164
+ Start with a brief summary, then provide details.
165
+ Include a "Next Steps" or "Summary" section at the end if applicable.`;
166
+ case 'plan-and-execute':
167
+ return `## Response Style: Agentic Execution
168
+ This is a task — work through it like a capable assistant who owns the outcome.
169
+
170
+ **Assess scope first**: Before diving in, quickly assess whether this is a 2-minute task or a 20-minute job.
171
+ - **Simple task** (one file, one API call, one edit): Just do it. No need to explain the plan.
172
+ - **Multi-step task** (several items to process, research + action, multiple files): Tell the user what you're going to do, then use sub-agents (Agent tool) to parallelize the work. Example: "This touches 3 accounts — I'll spin up research on each in parallel and have results in a few minutes."
173
+ - **Complex/long task**: Offer to run it in the background: "This is going to take some real work. Want me to kick it off in deep mode? I'll keep you posted as I go."
174
+
175
+ **During execution**: Narrate findings and decisions, not tool calls. If something unexpected happens, explain what you're doing about it.
176
+ **End**: Summarize what you did and suggest natural next steps.
177
+
178
+ **Parallelization**: When processing multiple items (prospects, files, accounts), ALWAYS use the Agent tool to spawn sub-agents that work in parallel. Don't process 10 things one at a time when you can batch them across 3-5 sub-agents.`;
179
+ case 'acknowledge-and-adapt':
180
+ return `## Response Style: Adaptive
181
+ The user is giving feedback or correcting you. Acknowledge it briefly and naturally.
182
+ If it's positive feedback: a brief "glad that worked" type response. Don't be effusive.
183
+ If it's a correction: acknowledge what was wrong, explain briefly what you understand now, then fix it or adjust. Show that you understood the correction, don't just say "sorry."`;
184
+ case 'conversational':
185
+ return `## Response Style: Casual
186
+ Keep it natural and brief. Match the user's energy.
187
+ No tool calls needed. Just be conversational.
188
+ If there's relevant context from recent work or pending items, briefly mention it.`;
189
+ }
190
+ }
191
+ /**
192
+ * Generate a follow-up suggestion prompt suffix based on completed work.
193
+ *
194
+ * @param toolCallCount - Number of tool calls made during the query
195
+ * @param responseLength - Length of the response text
196
+ * @param hasActiveGoals - Whether there are relevant active goals
197
+ * @param pendingWorkItems - Number of pending items in the work queue
198
+ */
199
+ export function buildFollowUpContext(opts) {
200
+ // Only suggest follow-ups after substantive work
201
+ if (opts.toolCallCount < 5 || opts.responseLength < 200)
202
+ return null;
203
+ const parts = [];
204
+ if (opts.hasActiveGoals) {
205
+ parts.push('There are active goals that may relate to this work.');
206
+ }
207
+ if (opts.pendingWorkItems > 0) {
208
+ parts.push(`There are ${opts.pendingWorkItems} pending work items in the queue.`);
209
+ }
210
+ if (parts.length === 0)
211
+ return null;
212
+ return `\n\n[POST_WORK_CONTEXT: ${parts.join(' ')} Consider suggesting 1-2 natural next steps as questions, if they'd be genuinely useful. Don't force it.]`;
213
+ }
214
+ //# sourceMappingURL=intent-classifier.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Clementine TypeScript — Proactive link understanding.
3
+ *
4
+ * Extracts URLs from user messages, fetches their content, and returns
5
+ * readable text so the agent has context without needing a tool call.
6
+ */
7
+ export interface LinkContext {
8
+ url: string;
9
+ title: string;
10
+ content: string;
11
+ error?: string;
12
+ }
13
+ /**
14
+ * Extract and fetch URLs found in the given text.
15
+ * Returns content for up to LINK_EXTRACT_MAX_URLS unique URLs.
16
+ * Never throws — errors are captured per-URL.
17
+ */
18
+ export declare function extractLinks(text: string): Promise<LinkContext[]>;
19
+ //# sourceMappingURL=link-extractor.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Clementine TypeScript — Proactive link understanding.
3
+ *
4
+ * Extracts URLs from user messages, fetches their content, and returns
5
+ * readable text so the agent has context without needing a tool call.
6
+ */
7
+ import { LINK_EXTRACT_MAX_URLS, LINK_EXTRACT_MAX_CHARS } from '../config.js';
8
+ import { PRIVATE_URL_PATTERNS } from './hooks.js';
9
+ // Match https:// URLs, skip common image/video/audio extensions
10
+ const URL_RE = /https?:\/\/[^\s<>"')\]]+/gi;
11
+ const SKIP_EXT_RE = /\.(png|jpg|jpeg|gif|webp|svg|ico|mp4|mp3|wav|webm|avi|mov|pdf)(\?[^\s]*)?$/i;
12
+ /** Strip HTML tags of a given type (including content). */
13
+ function stripTags(html, ...tags) {
14
+ let result = html;
15
+ for (const tag of tags) {
16
+ result = result.replace(new RegExp(`<${tag}[\\s>][\\s\\S]*?</${tag}>`, 'gi'), '');
17
+ }
18
+ return result;
19
+ }
20
+ /** Extract the <title> text from HTML. */
21
+ function extractTitle(html) {
22
+ const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
23
+ return match ? match[1].replace(/\s+/g, ' ').trim() : '';
24
+ }
25
+ /** Check if a URL targets a private/internal network. */
26
+ function isPrivateUrl(url) {
27
+ return PRIVATE_URL_PATTERNS.some(p => p.test(url));
28
+ }
29
+ async function fetchLink(url) {
30
+ if (isPrivateUrl(url)) {
31
+ return { url, title: '', content: '', error: 'private/internal URL blocked' };
32
+ }
33
+ try {
34
+ const response = await fetch(url, {
35
+ headers: { 'User-Agent': 'Clementine/1.0' },
36
+ signal: AbortSignal.timeout(10_000),
37
+ redirect: 'follow',
38
+ });
39
+ if (!response.ok) {
40
+ return { url, title: '', content: '', error: `HTTP ${response.status}` };
41
+ }
42
+ const contentType = response.headers.get('content-type') ?? '';
43
+ if (!contentType.includes('text/html') && !contentType.includes('text/plain')) {
44
+ return { url, title: '', content: '', error: `unsupported content-type: ${contentType}` };
45
+ }
46
+ const html = await response.text();
47
+ const title = extractTitle(html);
48
+ // Strip non-content tags, then all remaining HTML
49
+ let text = stripTags(html, 'script', 'style', 'nav', 'footer', 'header', 'aside');
50
+ text = text.replace(/<[^>]+>/g, ' ');
51
+ // Collapse whitespace
52
+ text = text.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
53
+ // Truncate
54
+ if (text.length > LINK_EXTRACT_MAX_CHARS) {
55
+ text = text.slice(0, LINK_EXTRACT_MAX_CHARS) + '…';
56
+ }
57
+ return { url, title, content: text };
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ return { url, title: '', content: '', error: message };
62
+ }
63
+ }
64
+ /**
65
+ * Extract and fetch URLs found in the given text.
66
+ * Returns content for up to LINK_EXTRACT_MAX_URLS unique URLs.
67
+ * Never throws — errors are captured per-URL.
68
+ */
69
+ export async function extractLinks(text) {
70
+ const matches = text.match(URL_RE);
71
+ if (!matches)
72
+ return [];
73
+ // Deduplicate and filter
74
+ const seen = new Set();
75
+ const urls = [];
76
+ for (const raw of matches) {
77
+ // Strip trailing punctuation that's likely not part of the URL
78
+ const url = raw.replace(/[.,;:!?)]+$/, '');
79
+ if (seen.has(url) || SKIP_EXT_RE.test(url))
80
+ continue;
81
+ seen.add(url);
82
+ urls.push(url);
83
+ if (urls.length >= LINK_EXTRACT_MAX_URLS)
84
+ break;
85
+ }
86
+ if (!urls.length)
87
+ return [];
88
+ return Promise.all(urls.map(fetchLink));
89
+ }
90
+ //# sourceMappingURL=link-extractor.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Clementine TypeScript — MCP Server Bridge.
3
+ *
4
+ * Discovers external MCP servers from Claude Desktop config, Claude Code
5
+ * settings, and user-managed config. Merges them into the SDK mcpServers
6
+ * option alongside Clementine's own MCP server.
7
+ */
8
+ import type { ManagedMcpServer } from '../types.js';
9
+ /** Discover all available MCP servers from Claude Desktop, Claude Code, and user config. */
10
+ export declare function discoverMcpServers(): ManagedMcpServer[];
11
+ /** Get enabled MCP servers as SDK-compatible config, filtered by allowed list. */
12
+ export declare function getMcpServersForAgent(allowedMcpServers?: string[]): Record<string, any>;
13
+ export declare function loadUserMcpServers(): Record<string, any>;
14
+ export declare function saveUserMcpServers(servers: Record<string, any>): void;
15
+ export declare function upsertMcpServer(name: string, config: Partial<ManagedMcpServer>): void;
16
+ export declare function removeMcpServer(name: string): void;
17
+ export interface PermissionStatus {
18
+ server: string;
19
+ resource: string;
20
+ granted: boolean;
21
+ settingsLabel: string;
22
+ }
23
+ /** Check macOS permissions for extensions that need them. Returns only servers with permission issues. */
24
+ export declare function checkPermissions(): PermissionStatus[];
25
+ /** Run permission checks on startup and log warnings for any issues. */
26
+ export declare function checkPermissionsOnStartup(): void;
27
+ /** Get a user-friendly error message when a tool fails due to permissions. */
28
+ export declare function getPermissionErrorMessage(serverName: string): string | null;
29
+ export interface ClaudeIntegration {
30
+ /** Integration name, e.g. "Microsoft_365" */
31
+ name: string;
32
+ /** Human-friendly label */
33
+ label: string;
34
+ /** Tools discovered for this integration */
35
+ tools: string[];
36
+ /** When first seen */
37
+ firstSeen: string;
38
+ /** When last used */
39
+ lastUsed: string;
40
+ /** Whether the user has it connected (true if we've seen it work) */
41
+ connected: boolean;
42
+ }
43
+ /**
44
+ * Check if a tool name is a Claude Desktop integration tool.
45
+ * Format: mcp__claude_ai_<IntegrationName>__<tool_name>
46
+ */
47
+ export declare function isClaudeDesktopTool(toolName: string): boolean;
48
+ /** Load persisted integrations from disk. */
49
+ export declare function loadClaudeIntegrations(): Record<string, ClaudeIntegration>;
50
+ /**
51
+ * Record a Claude Desktop integration tool use.
52
+ * Call this whenever a mcp__claude_ai_* tool is seen in a tool_use block.
53
+ */
54
+ export declare function recordClaudeIntegrationUse(toolName: string): void;
55
+ /** Get all discovered Claude Desktop integrations as a list. */
56
+ export declare function getClaudeIntegrations(): ClaudeIntegration[];
57
+ /**
58
+ * Bootstrap integrations from the audit log.
59
+ * Call once on startup to seed the integrations file from historical data.
60
+ */
61
+ export declare function bootstrapClaudeIntegrationsFromAuditLog(auditLogPath: string): void;
62
+ //# sourceMappingURL=mcp-bridge.d.ts.map