clementine-agent 1.18.56 → 1.18.58

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.
@@ -25,6 +25,7 @@ import { type RunAgentResult } from './run-agent.js';
25
25
  export interface CronPostTaskHooks {
26
26
  triggerCronReflection: (jobName: string, jobPrompt: string, deliverable: string, successCriteria?: string[]) => Promise<void>;
27
27
  triggerSkillExtractionFromExecution: (source: 'unleashed' | 'cron' | 'chat', jobName: string, prompt: string, output: string, durationMs: number, agentSlug?: string) => Promise<void>;
28
+ triggerMemoryExtractionPostExchange: (userMessage: string, assistantResponse: string, sessionKey?: string, profile?: AgentProfile) => Promise<void>;
28
29
  }
29
30
  export interface RunAgentCronOptions {
30
31
  /** Job name from CRON.md. Used for telemetry, progress lookup, skill match. */
@@ -319,11 +319,13 @@ export async function runAgentCron(opts) {
319
319
  logger.debug({ err, job: opts.jobName }, 'runAgentCron: transcript mirror failed (non-fatal)');
320
320
  }
321
321
  }
322
- // ── Post-task hooks: reflection + skill extraction ────────────────
323
- // Both fire-and-forget — never block the cron deliverable on these.
324
- // They are the same passes the legacy runCronJob fires; without them
325
- // the new path would lose the success-grading + procedural-memory
326
- // growth that makes Clementine self-improving.
322
+ // ── Post-task hooks: reflection + skill extraction + memory ──────
323
+ // All fire-and-forget — never block the cron deliverable on these.
324
+ // Reflection grades the run, skill extraction banks repeatable
325
+ // procedures, memory extraction distills facts the agent learned
326
+ // (e.g. "Mark Finizio is now the buyer at FamilyCenter") into the
327
+ // agent's MEMORY.md. The legacy runCronJob fired reflection +
328
+ // skill but never memory extraction; that gap is closed now.
327
329
  if (opts.postTaskHooks && deliverable && deliverable.trim() !== '__NOTHING__') {
328
330
  const durationMs = Date.now() - startedAt;
329
331
  opts.postTaskHooks
@@ -332,6 +334,9 @@ export async function runAgentCron(opts) {
332
334
  opts.postTaskHooks
333
335
  .triggerSkillExtractionFromExecution('cron', opts.jobName, opts.jobPrompt, deliverable, durationMs, agentSlug)
334
336
  .catch(err => logger.debug({ err, job: opts.jobName }, 'runAgentCron: skill extraction failed (non-fatal)'));
337
+ opts.postTaskHooks
338
+ .triggerMemoryExtractionPostExchange(opts.jobPrompt, deliverable, `cron:${opts.jobName}`, opts.profile ?? undefined)
339
+ .catch(err => logger.debug({ err, job: opts.jobName }, 'runAgentCron: memory extraction failed (non-fatal)'));
335
340
  }
336
341
  return {
337
342
  ...result,
@@ -2,6 +2,12 @@ import type { AgentProfile } from '../types.js';
2
2
  import type { AgentManager } from './agent-manager.js';
3
3
  import type { MemoryStore } from '../memory/store.js';
4
4
  import { type RunAgentResult } from './run-agent.js';
5
+ /** Minimal post-task hook interface. The PersonalAssistant implements
6
+ * this directly; passing it through keeps the wrapper decoupled from
7
+ * the full assistant graph. */
8
+ export interface TeamTaskPostHooks {
9
+ triggerMemoryExtractionPostExchange: (userMessage: string, assistantResponse: string, sessionKey?: string, profile?: AgentProfile) => Promise<void>;
10
+ }
5
11
  export interface RunAgentTeamTaskOptions {
6
12
  fromName: string;
7
13
  fromSlug: string;
@@ -17,6 +23,9 @@ export interface RunAgentTeamTaskOptions {
17
23
  maxBudgetUsd?: number;
18
24
  /** Optional max-turns cap. Default: undefined (SDK runs until done, bounded by budget). */
19
25
  maxTurns?: number;
26
+ /** Post-task hooks (memory extraction). Pass the PersonalAssistant.
27
+ * Optional so the helper still works in tests. */
28
+ postTaskHooks?: TeamTaskPostHooks | null;
20
29
  }
21
30
  export interface RunAgentTeamTaskResult extends RunAgentResult {
22
31
  builtPrompt: string;
@@ -82,6 +82,15 @@ export async function runAgentTeamTask(opts) {
82
82
  /* non-fatal */
83
83
  }
84
84
  }
85
+ // Auto-memory extraction — distill any new facts the recipient
86
+ // learned during the task into their MEMORY.md. Fire-and-forget,
87
+ // scoped to the recipient's profile so writes route to
88
+ // agents/<slug>/MEMORY.md, not the global one.
89
+ if (opts.postTaskHooks && result.text?.trim()) {
90
+ opts.postTaskHooks
91
+ .triggerMemoryExtractionPostExchange(opts.content, result.text, sessionKey, opts.profile)
92
+ .catch(err => logger.debug({ err, fromSlug: opts.fromSlug, toSlug: opts.profile.slug }, 'runAgentTeamTask: memory extraction failed (non-fatal)'));
93
+ }
85
94
  return {
86
95
  ...result,
87
96
  builtPrompt,
@@ -25906,10 +25906,10 @@ function renderBuilderPreview(artifact, type) {
25906
25906
  + '<option value="2"' + (artifact.tier === 2 ? ' selected' : '') + '>2 — Read+Write</option>'
25907
25907
  + '<option value="3"' + (artifact.tier === 3 ? ' selected' : '') + '>3 — Full</option>'
25908
25908
  + '</select></div>'
25909
- + '<div class="preview-field"><label>Mode</label><select onchange="builderArtifact.mode=this.value">'
25910
- + '<option value="standard"' + (artifact.mode !== 'unleashed' ? ' selected' : '') + '>Standard</option>'
25911
- + '<option value="unleashed"' + (artifact.mode === 'unleashed' ? ' selected' : '') + '>Unleashed</option>'
25912
- + '</select></div>'
25909
+ + '<div class="preview-field"><label>Linked Tools</label>'
25910
+ + '<div id="builder-tools-panel" style="max-height:180px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px 8px;background:var(--bg-primary);margin-bottom:4px"></div>'
25911
+ + '<div style="font-size:10px;color:var(--text-muted)">Pick tools the cron should use. The chat sees these as a hint, and they steer how the prompt is written.</div>'
25912
+ + '</div>'
25913
25913
  + '<div class="preview-field"><label>Prompt</label><textarea rows="12" onchange="builderArtifact.prompt=this.value">' + esc(artifact.prompt || '') + '</textarea></div>'
25914
25914
  + '<div class="preview-field"><label>Reference Files</label>'
25915
25915
  + '<div id="builder-attachments-list"></div>'
@@ -25919,6 +25919,7 @@ function renderBuilderPreview(artifact, type) {
25919
25919
  + '</label>'
25920
25920
  + '<div style="font-size:10px;color:var(--text-muted);margin-top:4px">Files injected into the agent prompt at runtime</div>'
25921
25921
  + '</div>';
25922
+ setTimeout(function() { loadBuilderToolOptions(artifact.toolsUsed || _builderLinkedTools); }, 50);
25922
25923
  } else if (type === 'agent') {
25923
25924
  html = '<div class="preview-field"><label>Name</label><input type="text" value="' + esc(artifact.name || '') + '" onchange="builderArtifact.name=this.value"></div>'
25924
25925
  + '<div class="preview-field"><label>Description / Role</label><input type="text" value="' + esc(artifact.description || '') + '" onchange="builderArtifact.description=this.value"></div>'
@@ -25929,13 +25930,22 @@ function renderBuilderPreview(artifact, type) {
25929
25930
  + '<option value="opus"' + (artifact.model === 'opus' ? ' selected' : '') + '>Opus</option>'
25930
25931
  + '</select></div>'
25931
25932
  + '<div class="preview-field"><label>Personality / System Prompt</label><textarea rows="8" onchange="builderArtifact.personality=this.value">' + esc(artifact.personality || '') + '</textarea></div>'
25932
- + '<div class="preview-field"><label>Tools (comma-separated)</label><input type="text" value="' + esc((artifact.tools || []).join(', ')) + '" onchange="builderArtifact.tools=this.value.split(\\x27,\\x27).map(function(t){return t.trim()}).filter(Boolean)"></div>'
25933
+ + '<div class="preview-field"><label>Allowed Tools</label>'
25934
+ + '<div id="builder-tools-panel" style="max-height:180px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px 8px;background:var(--bg-primary);margin-bottom:4px"></div>'
25935
+ + '<div style="font-size:10px;color:var(--text-muted)">These become the agent\\x27s allowedTools — they\\x27re honored at the main-agent level when this agent runs as a profile.</div>'
25936
+ + '</div>'
25933
25937
  + '<div class="preview-field"><label>Channel</label><input type="text" value="' + esc(artifact.channel || '') + '" onchange="builderArtifact.channel=this.value" placeholder="e.g. discord, slack"></div>';
25938
+ setTimeout(function() { loadBuilderToolOptions(artifact.tools || _builderLinkedTools); }, 50);
25934
25939
  } else if (type === 'workflow') {
25935
25940
  html = '<div class="preview-field"><label>Workflow Name</label><input type="text" value="' + esc(artifact.name || '') + '" onchange="builderArtifact.name=this.value"></div>'
25936
25941
  + '<div class="preview-field"><label>Description</label><input type="text" value="' + esc(artifact.description || '') + '" onchange="builderArtifact.description=this.value"></div>'
25937
25942
  + '<div class="preview-field"><label>Trigger Schedule (cron, optional)</label><input type="text" value="' + esc(artifact.schedule || '') + '" onchange="builderArtifact.schedule=this.value" placeholder="e.g. 0 9 * * 1 (Mondays at 9am)"></div>'
25943
+ + '<div class="preview-field"><label>Linked Tools</label>'
25944
+ + '<div id="builder-tools-panel" style="max-height:180px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px 8px;background:var(--bg-primary);margin-bottom:4px"></div>'
25945
+ + '<div style="font-size:10px;color:var(--text-muted)">Tools the trick will use. The chat sees these as a hint and weaves them into the steps.</div>'
25946
+ + '</div>'
25938
25947
  + '<div class="preview-field"><label>Steps (YAML/Markdown)</label><textarea rows="14" onchange="builderArtifact.steps=this.value">' + esc(artifact.steps || '') + '</textarea></div>';
25948
+ setTimeout(function() { loadBuilderToolOptions(artifact.toolsUsed || _builderLinkedTools); }, 50);
25939
25949
  }
25940
25950
  preview.innerHTML = html;
25941
25951
  // Load existing attachments if editing
@@ -25993,8 +26003,13 @@ async function saveBuilderArtifact() {
25993
26003
  var type = document.getElementById('builder-type').value;
25994
26004
  try {
25995
26005
  var agentSlug = (document.getElementById('builder-agent') || {}).value || '';
25996
- // Sync linked tools into artifact before saving
25997
- if (type === 'skill') builderArtifact.toolsUsed = _builderLinkedTools;
26006
+ // Sync linked tools into artifact before saving — picker is shared
26007
+ // across types but the persisted field name differs.
26008
+ if (type === 'agent') {
26009
+ builderArtifact.tools = _builderLinkedTools;
26010
+ } else {
26011
+ builderArtifact.toolsUsed = _builderLinkedTools;
26012
+ }
25998
26013
  var r = await apiJson('POST', '/api/builder/save', {
25999
26014
  artifactType: type,
26000
26015
  artifact: builderArtifact,
@@ -26081,8 +26096,18 @@ async function loadBuilderToolOptions(selectedTools) {
26081
26096
 
26082
26097
  function syncBuilderLinkedTools() {
26083
26098
  _builderLinkedTools = Array.from(document.querySelectorAll('.builder-tool-cb:checked')).map(function(cb) { return cb.value; });
26084
- // Also sync into the artifact so it saves with toolsUsed
26085
- if (builderArtifact) builderArtifact.toolsUsed = _builderLinkedTools;
26099
+ if (!builderArtifact) return;
26100
+ // Field name differs per artifact type: agent stores into "tools"
26101
+ // (which becomes profile.team.allowedTools on save). Skill, cron and
26102
+ // workflow store into "toolsUsed" — carried for reference, since the
26103
+ // runtime LINKED TOOLS context block is rebuilt from picker state
26104
+ // each turn.
26105
+ var type = (document.getElementById('builder-type') || {}).value;
26106
+ if (type === 'agent') {
26107
+ builderArtifact.tools = _builderLinkedTools;
26108
+ } else {
26109
+ builderArtifact.toolsUsed = _builderLinkedTools;
26110
+ }
26086
26111
  }
26087
26112
 
26088
26113
  // ── Builder File Attachments ──────────────
@@ -16,8 +16,9 @@
16
16
  */
17
17
  import { createHash } from 'node:crypto';
18
18
  import Anthropic from '@anthropic-ai/sdk';
19
+ import { query } from '@anthropic-ai/claude-agent-sdk';
19
20
  import pino from 'pino';
20
- import { MODELS } from '../config.js';
21
+ import { MODELS, BASE_DIR, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY } from '../config.js';
21
22
  import { fingerprintCommitment, parseRelativeDue, } from './commitments.js';
22
23
  const logger = pino({
23
24
  name: 'clementine.episodic-consolidation',
@@ -140,11 +141,65 @@ export function fingerprintLearnedFact(kind, text) {
140
141
  function getAnthropicClient(opts) {
141
142
  if (opts.anthropicClient)
142
143
  return opts.anthropicClient;
143
- const apiKey = process.env.ANTHROPIC_API_KEY;
144
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? ANTHROPIC_API_KEY;
144
145
  if (!apiKey)
145
146
  return null;
146
147
  return new Anthropic({ apiKey });
147
148
  }
149
+ /**
150
+ * One-shot LLM call via the SDK's `query()`. OAuth-aware (uses
151
+ * CLAUDE_CODE_OAUTH_TOKEN when no API key is set), so works on
152
+ * installs that haven't configured ANTHROPIC_API_KEY. Returns the
153
+ * concatenated assistant text — empty string on failure.
154
+ *
155
+ * Used as a fallback when no Anthropic SDK client is available
156
+ * (i.e. the prior path returned null and the entire consolidation
157
+ * pass silently no-op'd).
158
+ */
159
+ async function runConsolidationViaSdk(systemPrompt, userPrompt, model) {
160
+ const env = {
161
+ PATH: process.env.PATH ?? '',
162
+ HOME: process.env.HOME ?? '',
163
+ CLEMENTINE_HOME: BASE_DIR,
164
+ };
165
+ const oauth = CLAUDE_CODE_OAUTH_TOKEN || process.env.CLAUDE_CODE_OAUTH_TOKEN;
166
+ const apiKey = ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
167
+ if (oauth)
168
+ env.CLAUDE_CODE_OAUTH_TOKEN = oauth;
169
+ else if (apiKey)
170
+ env.ANTHROPIC_API_KEY = apiKey;
171
+ let text = '';
172
+ try {
173
+ const stream = query({
174
+ prompt: userPrompt,
175
+ options: {
176
+ systemPrompt,
177
+ model,
178
+ permissionMode: 'bypassPermissions',
179
+ allowDangerouslySkipPermissions: true,
180
+ allowedTools: [],
181
+ cwd: BASE_DIR,
182
+ env,
183
+ maxTurns: 1,
184
+ maxBudgetUsd: 0.10,
185
+ },
186
+ });
187
+ for await (const message of stream) {
188
+ if (message.type === 'assistant') {
189
+ const blocks = (message.message?.content ?? []);
190
+ for (const block of blocks) {
191
+ if (block.type === 'text' && typeof block.text === 'string')
192
+ text += block.text;
193
+ }
194
+ }
195
+ }
196
+ }
197
+ catch (err) {
198
+ logger.warn({ err }, 'SDK consolidation call failed');
199
+ return '';
200
+ }
201
+ return text;
202
+ }
148
203
  /**
149
204
  * Consolidate a single candidate session range. Returns the new episode id
150
205
  * + chunk id on success, or null on failure (the caller bumps the failure
@@ -155,10 +210,10 @@ export async function consolidateOneSession(store, candidate, opts = {}) {
155
210
  if (turns.length === 0)
156
211
  return null;
157
212
  const client = getAnthropicClient(opts);
158
- if (!client) {
159
- logger.debug({ sessionKey: candidate.sessionKey }, 'No Anthropic client available — skipping consolidation');
160
- return null;
161
- }
213
+ // No client means no API key. We still try via the SDK's query()
214
+ // which uses OAuth when available — that's the canonical path for
215
+ // installs that haven't configured ANTHROPIC_API_KEY. Tests that
216
+ // pass an explicit anthropicClient will still hit the direct path.
162
217
  // Pull a small snapshot of existing learned facts so the LLM can
163
218
  // detect contradictions and emit supersedes hints. Best-effort —
164
219
  // empty list is fine for first-ever consolidation.
@@ -169,18 +224,28 @@ export async function consolidateOneSession(store, candidate, opts = {}) {
169
224
  }
170
225
  }
171
226
  catch { /* fact snapshot is best-effort */ }
227
+ const userPrompt = buildUserPrompt(turns.map(t => ({ role: t.role, content: t.content, createdAt: t.createdAt })), existingFactsForPrompt);
228
+ const model = opts.model ?? MODELS.haiku;
172
229
  let extraction = null;
173
230
  try {
174
- const response = await client.messages.create({
175
- model: opts.model ?? MODELS.haiku,
176
- max_tokens: 1500,
177
- system: SYSTEM_PROMPT,
178
- messages: [{
179
- role: 'user',
180
- content: buildUserPrompt(turns.map(t => ({ role: t.role, content: t.content, createdAt: t.createdAt })), existingFactsForPrompt),
181
- }],
182
- });
183
- const text = (response.content ?? []).map((b) => b.type === 'text' ? (b.text ?? '') : '').join('');
231
+ let text = '';
232
+ if (client) {
233
+ const response = await client.messages.create({
234
+ model,
235
+ max_tokens: 1500,
236
+ system: SYSTEM_PROMPT,
237
+ messages: [{ role: 'user', content: userPrompt }],
238
+ });
239
+ text = (response.content ?? []).map((b) => b.type === 'text' ? (b.text ?? '') : '').join('');
240
+ }
241
+ else {
242
+ // No API client — fall through to the SDK (OAuth-aware).
243
+ text = await runConsolidationViaSdk(SYSTEM_PROMPT, userPrompt, model);
244
+ }
245
+ if (!text) {
246
+ logger.debug({ sessionKey: candidate.sessionKey }, 'Empty consolidation response — skipping');
247
+ return null;
248
+ }
184
249
  extraction = parseEpisodeJson(text);
185
250
  }
186
251
  catch (err) {
@@ -42,6 +42,8 @@ export declare class HeartbeatScheduler {
42
42
  private runLog;
43
43
  private lastDenseBackfillAt;
44
44
  private denseBackfillInFlight;
45
+ private lastTranscriptDenseBackfillAt;
46
+ private transcriptDenseBackfillInFlight;
45
47
  private lastSalienceDecayDate;
46
48
  private lastMemoryPulseDate;
47
49
  private lastEpisodicConsolidationAt;
@@ -70,6 +72,14 @@ export declare class HeartbeatScheduler {
70
72
  * Coverage climbs over hours/days without user action.
71
73
  */
72
74
  private maybeIdleDenseBackfill;
75
+ /**
76
+ * Sibling of maybeIdleDenseBackfill that targets the transcripts table.
77
+ * Same gates (cooldown + chat-lane idle + dense model ready), separate
78
+ * in-flight + cadence so the two backfills don't starve each other.
79
+ * Without this, new chat/cron/heartbeat turns get FTS5-indexed but
80
+ * never embedded, and the dense leg of recall silently returns 0 hits.
81
+ */
82
+ private maybeIdleTranscriptDenseBackfill;
73
83
  /**
74
84
  * Episodic consolidation pass. Turns idle session transcript ranges into
75
85
  * durable episodes via a small Haiku call per session. Same shape as
@@ -52,6 +52,8 @@ export class HeartbeatScheduler {
52
52
  runLog = new CronRunLog();
53
53
  lastDenseBackfillAt = 0;
54
54
  denseBackfillInFlight = false;
55
+ lastTranscriptDenseBackfillAt = 0;
56
+ transcriptDenseBackfillInFlight = false;
55
57
  lastSalienceDecayDate = '';
56
58
  lastMemoryPulseDate = '';
57
59
  lastEpisodicConsolidationAt = 0;
@@ -155,6 +157,14 @@ export class HeartbeatScheduler {
155
157
  this.maybeIdleDenseBackfill().catch(err => {
156
158
  logger.debug({ err }, 'Idle dense backfill failed (non-fatal)');
157
159
  });
160
+ // Transcript dense backfill — separate cadence from chunks. Transcript
161
+ // turns from chat/cron/heartbeat/team-task accumulate continuously
162
+ // and need their own dense vectors so the recall block's dense leg
163
+ // returns hits for them too. Without this, hybrid recall silently
164
+ // degrades to lexical-only for transcripts.
165
+ this.maybeIdleTranscriptDenseBackfill().catch(err => {
166
+ logger.debug({ err }, 'Idle transcript dense backfill failed (non-fatal)');
167
+ });
158
168
  // Daily salience decay — fades stale, unaccessed chunks so retrieval
159
169
  // doesn't keep boosting facts that aren't earning their context budget.
160
170
  // Pinned + soft-deleted + superseded chunks are exempt. One UPDATE per
@@ -823,6 +833,45 @@ export class HeartbeatScheduler {
823
833
  this.denseBackfillInFlight = false;
824
834
  }
825
835
  }
836
+ /**
837
+ * Sibling of maybeIdleDenseBackfill that targets the transcripts table.
838
+ * Same gates (cooldown + chat-lane idle + dense model ready), separate
839
+ * in-flight + cadence so the two backfills don't starve each other.
840
+ * Without this, new chat/cron/heartbeat turns get FTS5-indexed but
841
+ * never embedded, and the dense leg of recall silently returns 0 hits.
842
+ */
843
+ async maybeIdleTranscriptDenseBackfill() {
844
+ if (this.transcriptDenseBackfillInFlight)
845
+ return;
846
+ const sinceLastMs = Date.now() - this.lastTranscriptDenseBackfillAt;
847
+ if (sinceLastMs < 10 * 60 * 1000)
848
+ return;
849
+ const { lanes } = await import('./lanes.js');
850
+ if (lanes.status().chat.active > 0)
851
+ return;
852
+ const store = this.gateway.getMemoryStore();
853
+ if (!store)
854
+ return;
855
+ const s = store;
856
+ if (typeof s.backfillTranscriptDenseEmbeddings !== 'function')
857
+ return;
858
+ const embeddings = await import('../memory/embeddings.js');
859
+ if (!embeddings.isDenseReady()) {
860
+ embeddings.probeDenseReady().catch(() => { });
861
+ return;
862
+ }
863
+ this.transcriptDenseBackfillInFlight = true;
864
+ this.lastTranscriptDenseBackfillAt = Date.now();
865
+ try {
866
+ const result = await s.backfillTranscriptDenseEmbeddings({ limit: 50 });
867
+ if (result.embedded > 0) {
868
+ logger.info({ embedded: result.embedded, failed: result.failed, model: result.model }, 'Idle transcript dense backfill batch complete');
869
+ }
870
+ }
871
+ finally {
872
+ this.transcriptDenseBackfillInFlight = false;
873
+ }
874
+ }
826
875
  /**
827
876
  * Episodic consolidation pass. Turns idle session transcript ranges into
828
877
  * durable episodes via a small Haiku call per session. Same shape as
@@ -2064,6 +2064,10 @@ export class Gateway {
2064
2064
  agentManager: this.getAgentManager(),
2065
2065
  memoryStore: this.assistant.getMemoryStore?.() ?? null,
2066
2066
  abortSignal: abortController?.signal,
2067
+ // Post-task auto-memory extraction so anything the recipient
2068
+ // learned during the task (new contact, preference, status)
2069
+ // distills into their agents/<slug>/MEMORY.md.
2070
+ postTaskHooks: this.assistant,
2067
2071
  });
2068
2072
  scanner.refreshIntegrity();
2069
2073
  logger.info({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.56",
3
+ "version": "1.18.58",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",