clementine-agent 1.0.68 → 1.0.70
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.
- package/dist/agent/assistant.d.ts +42 -0
- package/dist/agent/assistant.js +241 -29
- package/dist/agent/session-store-adapter.d.ts +14 -0
- package/dist/agent/session-store-adapter.js +69 -0
- package/dist/brain/adapters/common.d.ts +12 -0
- package/dist/brain/adapters/common.js +29 -0
- package/dist/brain/adapters/csv.d.ts +10 -0
- package/dist/brain/adapters/csv.js +55 -0
- package/dist/brain/adapters/docx.d.ts +10 -0
- package/dist/brain/adapters/docx.js +35 -0
- package/dist/brain/adapters/email.d.ts +9 -0
- package/dist/brain/adapters/email.js +84 -0
- package/dist/brain/adapters/index.d.ts +9 -0
- package/dist/brain/adapters/index.js +24 -0
- package/dist/brain/adapters/json.d.ts +10 -0
- package/dist/brain/adapters/json.js +100 -0
- package/dist/brain/adapters/markdown.d.ts +10 -0
- package/dist/brain/adapters/markdown.js +49 -0
- package/dist/brain/adapters/pdf.d.ts +9 -0
- package/dist/brain/adapters/pdf.js +59 -0
- package/dist/brain/adapters/rest.d.ts +29 -0
- package/dist/brain/adapters/rest.js +139 -0
- package/dist/brain/batch-summary.d.ts +30 -0
- package/dist/brain/batch-summary.js +129 -0
- package/dist/brain/format-detector.d.ts +16 -0
- package/dist/brain/format-detector.js +153 -0
- package/dist/brain/graph-extractor.d.ts +15 -0
- package/dist/brain/graph-extractor.js +61 -0
- package/dist/brain/ingest-scheduler.d.ts +32 -0
- package/dist/brain/ingest-scheduler.js +123 -0
- package/dist/brain/ingestion-pipeline.d.ts +47 -0
- package/dist/brain/ingestion-pipeline.js +357 -0
- package/dist/brain/intelligence.d.ts +67 -0
- package/dist/brain/intelligence.js +291 -0
- package/dist/brain/llm-client.d.ts +38 -0
- package/dist/brain/llm-client.js +92 -0
- package/dist/brain/source-registry.d.ts +38 -0
- package/dist/brain/source-registry.js +121 -0
- package/dist/cli/dashboard.js +1230 -10
- package/dist/cli/index.js +23 -0
- package/dist/cli/ingest.d.ts +19 -0
- package/dist/cli/ingest.js +151 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +80 -0
- package/dist/index.js +8 -0
- package/dist/memory/store.d.ts +190 -0
- package/dist/memory/store.js +674 -6
- package/dist/tools/artifact-tools.d.ts +11 -0
- package/dist/tools/artifact-tools.js +83 -0
- package/dist/tools/mcp-server.js +2 -0
- package/dist/tools/shared.d.ts +135 -0
- package/dist/types.d.ts +103 -0
- package/package.json +11 -3
|
@@ -40,6 +40,21 @@ export declare function getLinkedProjects(): ProjectMeta[];
|
|
|
40
40
|
export declare function addProject(projectPath: string, description?: string, keywords?: string[]): void;
|
|
41
41
|
/** Remove a project from the linked projects list. Returns true if removed. */
|
|
42
42
|
export declare function removeProject(projectPath: string): boolean;
|
|
43
|
+
export interface ProactiveGoalInput {
|
|
44
|
+
goal: {
|
|
45
|
+
title: string;
|
|
46
|
+
priority?: string;
|
|
47
|
+
owner?: string;
|
|
48
|
+
nextActions?: string[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the compact "active goals" block that gets injected when no goal
|
|
53
|
+
* keyword matches the user's prompt. Pure so it can be tested without the
|
|
54
|
+
* full Assistant/vault setup.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildActiveGoalsBlock(goals: ProactiveGoalInput[], agentSlug?: string | null, maxEntries?: number): string;
|
|
57
|
+
export declare function chunkReferencedInResponse(chunkContent: string, responseLower: string): boolean;
|
|
43
58
|
export declare class PersonalAssistant {
|
|
44
59
|
static readonly MAX_SESSION_EXCHANGES = 40;
|
|
45
60
|
private sessions;
|
|
@@ -69,6 +84,14 @@ export declare class PersonalAssistant {
|
|
|
69
84
|
private _compactedSessions;
|
|
70
85
|
/** Last auto-matched project per session — exposed for CLI display. */
|
|
71
86
|
private _lastMatchedProject;
|
|
87
|
+
/**
|
|
88
|
+
* Chunks retrieved on the most recent turn per session, kept so the
|
|
89
|
+
* post-response outcome scorer can check which actually got referenced.
|
|
90
|
+
* Cleared after each scoring pass.
|
|
91
|
+
*/
|
|
92
|
+
private _lastRetrievedChunks;
|
|
93
|
+
/** Lazy-built SessionStore adapter that mirrors SDK transcripts to SQLite. */
|
|
94
|
+
private _sessionStore;
|
|
72
95
|
/** Hot correction buffer — explicit behavioral corrections applied before nightly SI. */
|
|
73
96
|
private hotCorrections;
|
|
74
97
|
constructor();
|
|
@@ -91,6 +114,12 @@ export declare class PersonalAssistant {
|
|
|
91
114
|
/** Inject a background work result into the session so the next chat naturally references it. */
|
|
92
115
|
injectPendingContext(sessionKey: string, userPrompt: string, result: string): void;
|
|
93
116
|
private initMemoryStore;
|
|
117
|
+
/**
|
|
118
|
+
* Return the cached SessionStore adapter. Null until initMemoryStore
|
|
119
|
+
* completes, in which case the SDK falls back to local-only sessions —
|
|
120
|
+
* no crash on cold boot.
|
|
121
|
+
*/
|
|
122
|
+
private getSessionStore;
|
|
94
123
|
/**
|
|
95
124
|
* Seed the in-memory hotCorrections ring buffer from persisted behavioral
|
|
96
125
|
* patterns (corrections that recurred across ≥2 sessions in the last 30d).
|
|
@@ -123,6 +152,13 @@ export declare class PersonalAssistant {
|
|
|
123
152
|
* or empty string if no goals match.
|
|
124
153
|
*/
|
|
125
154
|
private matchGoals;
|
|
155
|
+
/**
|
|
156
|
+
* Compact always-on block of active goals. Used when no keyword match
|
|
157
|
+
* fires so the agent still sees what it's supposed to be working on.
|
|
158
|
+
* Scoped: for agent sessions, includes that agent's goals plus any
|
|
159
|
+
* clementine-owned goals it might contribute to.
|
|
160
|
+
*/
|
|
161
|
+
private formatActiveGoalsBlock;
|
|
126
162
|
chat(text: string, sessionKey?: string | null, options?: {
|
|
127
163
|
onText?: OnTextCallback;
|
|
128
164
|
onToolActivity?: OnToolActivityCallback;
|
|
@@ -134,6 +170,12 @@ export declare class PersonalAssistant {
|
|
|
134
170
|
verboseLevel?: VerboseLevel;
|
|
135
171
|
abortController?: AbortController;
|
|
136
172
|
}): Promise<[string, string]>;
|
|
173
|
+
/**
|
|
174
|
+
* Compare retrieved chunks against the response text and record which
|
|
175
|
+
* were referenced. Uses a distinctive-token overlap heuristic — cheap,
|
|
176
|
+
* deterministic, no extra LLM calls. Called right after a turn completes.
|
|
177
|
+
*/
|
|
178
|
+
private scoreRetrievalOutcomes;
|
|
137
179
|
private static readonly RATE_LIMIT_MAX_RETRIES;
|
|
138
180
|
private static readonly RATE_LIMIT_BACKOFF;
|
|
139
181
|
private runQuery;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { query as rawQuery, listSubagents, getSubagentMessages, } from '@anthropic-ai/claude-agent-sdk';
|
|
14
|
+
import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, PROFILES_DIR, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, ENABLE_1M_CONTEXT, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, } from '../config.js';
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, PROFILES_DIR, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, TASK_BUDGET_TOKENS, ENABLE_1M_CONTEXT, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, } from '../config.js';
|
|
17
17
|
import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
|
|
18
18
|
import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, setProfileTier, setProfileAllowedTools, setAgentDir, setSendPolicy, setInteractionSource, logAuditJsonl, } from './hooks.js';
|
|
19
19
|
import { scanner } from '../security/scanner.js';
|
|
@@ -242,6 +242,9 @@ const query = ((args) => {
|
|
|
242
242
|
if (typeof opts.systemPrompt === 'string') {
|
|
243
243
|
newOpts.systemPrompt = stripLoneSurrogates(opts.systemPrompt);
|
|
244
244
|
}
|
|
245
|
+
else if (Array.isArray(opts.systemPrompt)) {
|
|
246
|
+
newOpts.systemPrompt = opts.systemPrompt.map((s) => typeof s === 'string' ? stripLoneSurrogates(s) : s);
|
|
247
|
+
}
|
|
245
248
|
if (typeof opts.appendSystemPrompt === 'string') {
|
|
246
249
|
newOpts.appendSystemPrompt = stripLoneSurrogates(opts.appendSystemPrompt);
|
|
247
250
|
}
|
|
@@ -636,6 +639,72 @@ export function removeProject(projectPath) {
|
|
|
636
639
|
_projectsMetaCacheTime = 0; // invalidate cache
|
|
637
640
|
return true;
|
|
638
641
|
}
|
|
642
|
+
// ── Retrieval Outcome Heuristic ─────────────────────────────────────
|
|
643
|
+
/**
|
|
644
|
+
* Decide whether a retrieved memory chunk shows up in the assistant's
|
|
645
|
+
* response. We key on distinctive tokens (multi-letter capitalized words,
|
|
646
|
+
* numbers of 2+ digits) that are unlikely to appear in the response unless
|
|
647
|
+
* the chunk's content actually influenced what was said.
|
|
648
|
+
*
|
|
649
|
+
* Intentionally a cheap local heuristic — no LLM call. False positives are
|
|
650
|
+
* tolerable since the outcome score is bounded and averaged over many
|
|
651
|
+
* observations.
|
|
652
|
+
*/
|
|
653
|
+
const OUTCOME_STOPWORDS = new Set([
|
|
654
|
+
'there', 'these', 'those', 'their', 'where', 'which', 'while',
|
|
655
|
+
'would', 'could', 'should', 'about', 'being', 'after', 'before',
|
|
656
|
+
'again', 'against', 'because',
|
|
657
|
+
]);
|
|
658
|
+
/**
|
|
659
|
+
* Build the compact "active goals" block that gets injected when no goal
|
|
660
|
+
* keyword matches the user's prompt. Pure so it can be tested without the
|
|
661
|
+
* full Assistant/vault setup.
|
|
662
|
+
*/
|
|
663
|
+
export function buildActiveGoalsBlock(goals, agentSlug, maxEntries = 6) {
|
|
664
|
+
if (goals.length === 0)
|
|
665
|
+
return '';
|
|
666
|
+
const filtered = goals.filter(({ goal }) => {
|
|
667
|
+
if (!agentSlug)
|
|
668
|
+
return true;
|
|
669
|
+
return goal.owner === agentSlug || goal.owner === 'clementine';
|
|
670
|
+
});
|
|
671
|
+
if (filtered.length === 0)
|
|
672
|
+
return '';
|
|
673
|
+
const rank = { high: 0, medium: 1, low: 2 };
|
|
674
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
675
|
+
const ra = rank[a.goal.priority ?? 'medium'] ?? 1;
|
|
676
|
+
const rb = rank[b.goal.priority ?? 'medium'] ?? 1;
|
|
677
|
+
return ra - rb;
|
|
678
|
+
});
|
|
679
|
+
const top = sorted.slice(0, maxEntries);
|
|
680
|
+
const lines = top.map(({ goal }) => {
|
|
681
|
+
const next = goal.nextActions?.[0];
|
|
682
|
+
const nextBit = next ? ` → ${String(next).slice(0, 80)}` : '';
|
|
683
|
+
return `- [${goal.priority ?? 'medium'}] ${goal.title}${nextBit}`;
|
|
684
|
+
});
|
|
685
|
+
return `\n\n## Active Goals (background context)\n${lines.join('\n')}\n`;
|
|
686
|
+
}
|
|
687
|
+
export function chunkReferencedInResponse(chunkContent, responseLower) {
|
|
688
|
+
if (!chunkContent || !responseLower)
|
|
689
|
+
return false;
|
|
690
|
+
const distinctive = new Set();
|
|
691
|
+
const capMatches = chunkContent.match(/\b[A-Z][a-zA-Z]{3,}\b/g) ?? [];
|
|
692
|
+
for (const m of capMatches) {
|
|
693
|
+
const lower = m.toLowerCase();
|
|
694
|
+
if (!OUTCOME_STOPWORDS.has(lower))
|
|
695
|
+
distinctive.add(lower);
|
|
696
|
+
}
|
|
697
|
+
const numMatches = chunkContent.match(/\b\d{2,}\b/g) ?? [];
|
|
698
|
+
for (const m of numMatches)
|
|
699
|
+
distinctive.add(m);
|
|
700
|
+
if (distinctive.size === 0)
|
|
701
|
+
return false;
|
|
702
|
+
for (const tok of distinctive) {
|
|
703
|
+
if (responseLower.includes(tok))
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
639
708
|
// ── PersonalAssistant ───────────────────────────────────────────────
|
|
640
709
|
export class PersonalAssistant {
|
|
641
710
|
static MAX_SESSION_EXCHANGES = MAX_SESSION_EXCHANGES;
|
|
@@ -666,6 +735,14 @@ export class PersonalAssistant {
|
|
|
666
735
|
_compactedSessions = new Set();
|
|
667
736
|
/** Last auto-matched project per session — exposed for CLI display. */
|
|
668
737
|
_lastMatchedProject = new Map();
|
|
738
|
+
/**
|
|
739
|
+
* Chunks retrieved on the most recent turn per session, kept so the
|
|
740
|
+
* post-response outcome scorer can check which actually got referenced.
|
|
741
|
+
* Cleared after each scoring pass.
|
|
742
|
+
*/
|
|
743
|
+
_lastRetrievedChunks = new Map();
|
|
744
|
+
/** Lazy-built SessionStore adapter that mirrors SDK transcripts to SQLite. */
|
|
745
|
+
_sessionStore = null;
|
|
669
746
|
/** Hot correction buffer — explicit behavioral corrections applied before nightly SI. */
|
|
670
747
|
hotCorrections = [];
|
|
671
748
|
constructor() {
|
|
@@ -816,11 +893,27 @@ export class PersonalAssistant {
|
|
|
816
893
|
this.memoryStore = new MemoryStore(MEMORY_DB_PATH, VAULT_DIR);
|
|
817
894
|
this.memoryStore.initialize();
|
|
818
895
|
this.primeHotCorrections();
|
|
896
|
+
// Build the SDK SessionStore adapter now that the store is live.
|
|
897
|
+
try {
|
|
898
|
+
const { createMemorySessionStore } = await import('./session-store-adapter.js');
|
|
899
|
+
this._sessionStore = createMemorySessionStore(this.memoryStore);
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
902
|
+
logger.warn({ err }, 'SessionStore adapter init failed — SDK will use local-only sessions');
|
|
903
|
+
}
|
|
819
904
|
}
|
|
820
905
|
catch (err) {
|
|
821
906
|
logger.warn({ err }, 'Memory store init failed — falling back to static prompts');
|
|
822
907
|
}
|
|
823
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Return the cached SessionStore adapter. Null until initMemoryStore
|
|
911
|
+
* completes, in which case the SDK falls back to local-only sessions —
|
|
912
|
+
* no crash on cold boot.
|
|
913
|
+
*/
|
|
914
|
+
getSessionStore() {
|
|
915
|
+
return this._sessionStore;
|
|
916
|
+
}
|
|
824
917
|
/**
|
|
825
918
|
* Seed the in-memory hotCorrections ring buffer from persisted behavioral
|
|
826
919
|
* patterns (corrections that recurred across ≥2 sessions in the last 30d).
|
|
@@ -1637,22 +1730,29 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1637
1730
|
// Capture source at build time so concurrent queries don't race on the global
|
|
1638
1731
|
const capturedSource = sourceOverride;
|
|
1639
1732
|
// Build combined system prompt (custom + security rules).
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
//
|
|
1733
|
+
// Stable prefix (SOUL/AGENTS/personality/skills + security rules) is
|
|
1734
|
+
// deterministic per-session and cacheable across turns; the volatile
|
|
1735
|
+
// suffix (retrieved memory, active goals, current date/time, integration
|
|
1736
|
+
// status) changes per-turn and must NOT be in the cached prefix.
|
|
1737
|
+
//
|
|
1738
|
+
// The SDK's string[] systemPrompt with SYSTEM_PROMPT_DYNAMIC_BOUNDARY
|
|
1739
|
+
// (added in @anthropic-ai/claude-agent-sdk 0.2.119) tells the prompt
|
|
1740
|
+
// cache exactly where the boundary is, so cross-turn cache hits work
|
|
1741
|
+
// even when our per-turn goals/memory block changes.
|
|
1648
1742
|
const { stable, volatile: volatilePromptPart } = this.buildSystemPrompt({
|
|
1649
1743
|
isHeartbeat, cronTier: isPlanStep ? null : cronTier, retrievalContext, profile, sessionKey, model, verboseLevel, intentClassification,
|
|
1650
1744
|
});
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
volatilePromptPart
|
|
1655
|
-
|
|
1745
|
+
const stablePrefixParts = [stable, securityPrompt]
|
|
1746
|
+
.filter(s => s && s.trim().length > 0);
|
|
1747
|
+
const volatileSuffix = volatilePromptPart && volatilePromptPart.trim().length > 0
|
|
1748
|
+
? volatilePromptPart
|
|
1749
|
+
: '';
|
|
1750
|
+
// If there is no volatile content, a plain string keeps the call simple
|
|
1751
|
+
// and behaves identically for the cache. Only use the array form when
|
|
1752
|
+
// we actually have dynamic content to split off.
|
|
1753
|
+
const fullSystemPrompt = volatileSuffix
|
|
1754
|
+
? [...stablePrefixParts, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, volatileSuffix]
|
|
1755
|
+
: stablePrefixParts.join('\n\n');
|
|
1656
1756
|
// ── Compute effort level ──────────────────────────────────────
|
|
1657
1757
|
const computedEffort = effort ?? (isHeartbeat && !isCron ? 'low'
|
|
1658
1758
|
: isCron && (cronTier ?? 0) < 2 ? 'low'
|
|
@@ -1669,10 +1769,31 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1669
1769
|
: isCron ? BUDGET.cronT2
|
|
1670
1770
|
: BUDGET.chat);
|
|
1671
1771
|
void computedBudget; // reserved for future cost telemetry — not enforced
|
|
1772
|
+
// ── Task budget (tokens) ──────────────────────────────────────
|
|
1773
|
+
// Soft brake — the SDK tells the model its remaining token budget so it
|
|
1774
|
+
// paces tool use. Prevents runaway loops in autonomous contexts without
|
|
1775
|
+
// killing long, legitimate work. Interactive chat stays uncapped.
|
|
1776
|
+
const computedTaskBudget = isPlanStep
|
|
1777
|
+
? TASK_BUDGET_TOKENS.planStep
|
|
1778
|
+
: isUnleashed
|
|
1779
|
+
? TASK_BUDGET_TOKENS.unleashedPhase
|
|
1780
|
+
: isCron && (cronTier ?? 0) < 2
|
|
1781
|
+
? TASK_BUDGET_TOKENS.cronT1
|
|
1782
|
+
: isCron
|
|
1783
|
+
? TASK_BUDGET_TOKENS.cronT2
|
|
1784
|
+
: isHeartbeat
|
|
1785
|
+
? TASK_BUDGET_TOKENS.heartbeat
|
|
1786
|
+
: TASK_BUDGET_TOKENS.chat;
|
|
1672
1787
|
// ── Compute adaptive thinking ─────────────────────────────────
|
|
1673
1788
|
const supportsThinking = !resolvedModel.includes('haiku');
|
|
1674
1789
|
const needsThinking = !isHeartbeat && (isPlanStep || isUnleashed || !isCron);
|
|
1675
1790
|
const computedThinking = thinking ?? (supportsThinking && needsThinking ? { type: 'adaptive' } : undefined);
|
|
1791
|
+
// Haiku rejects user-configurable task budgets with a 400 ("This model
|
|
1792
|
+
// does not support user-configurable task budgets"). Only pass
|
|
1793
|
+
// taskBudget to models that accept it — otherwise every Haiku cron
|
|
1794
|
+
// run dies on arrival and (historically) got mis-classified as a
|
|
1795
|
+
// permanent "budget exceeded" failure.
|
|
1796
|
+
const supportsTaskBudget = !resolvedModel.includes('haiku');
|
|
1676
1797
|
// 1M context beta: enable for Sonnet when toggled and context-heavy work benefits
|
|
1677
1798
|
const isSonnet = resolvedModel.includes('sonnet');
|
|
1678
1799
|
const computedBetas = ENABLE_1M_CONTEXT && isSonnet
|
|
@@ -1691,12 +1812,17 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1691
1812
|
// Permission mode: always 'bypassPermissions' — this is a daemon/harness with no interactive
|
|
1692
1813
|
// terminal, so 'auto' mode (which requires plan support + human approval) doesn't apply.
|
|
1693
1814
|
const effectivePermissionMode = 'bypassPermissions';
|
|
1815
|
+
// SessionStore adapter: mirror SDK transcripts into our SQLite store.
|
|
1816
|
+
// Resume then works from the durable store, not just local JSONL.
|
|
1817
|
+
const sessionStore = this.getSessionStore();
|
|
1694
1818
|
return {
|
|
1695
1819
|
systemPrompt: fullSystemPrompt,
|
|
1696
1820
|
model: resolvedModel,
|
|
1697
1821
|
...(fallback ? { fallbackModel: fallback } : {}),
|
|
1698
1822
|
permissionMode: effectivePermissionMode,
|
|
1699
1823
|
allowDangerouslySkipPermissions: true,
|
|
1824
|
+
...(sessionStore ? { sessionStore } : {}),
|
|
1825
|
+
...(computedTaskBudget && supportsTaskBudget ? { taskBudget: { total: computedTaskBudget } } : {}),
|
|
1700
1826
|
// SDK field semantics (per node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts):
|
|
1701
1827
|
// - `tools` → which built-in tools the model can see (Read, Bash, Task, …)
|
|
1702
1828
|
// - `mcpServers` → MCP servers to spawn; all their declared tools are exposed automatically
|
|
@@ -1802,6 +1928,17 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1802
1928
|
// Non-fatal
|
|
1803
1929
|
}
|
|
1804
1930
|
}
|
|
1931
|
+
// Stash chunks for post-response outcome scoring. Only populate if
|
|
1932
|
+
// we have a sessionKey to key against — chunks with no session can't
|
|
1933
|
+
// be attributed to a response.
|
|
1934
|
+
if (sessionKey) {
|
|
1935
|
+
const stash = results
|
|
1936
|
+
.filter((r) => typeof r.chunkId === 'number' && r.chunkId !== 0 && typeof r.content === 'string')
|
|
1937
|
+
.map((r) => ({ id: r.chunkId, content: r.content }));
|
|
1938
|
+
if (stash.length > 0) {
|
|
1939
|
+
this._lastRetrievedChunks.set(sessionKey, stash);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1805
1942
|
}
|
|
1806
1943
|
// Resolve skill + graph context in parallel (independent of each other)
|
|
1807
1944
|
const [skillContext, graphContext] = await Promise.all([
|
|
@@ -1949,6 +2086,20 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1949
2086
|
return '';
|
|
1950
2087
|
}
|
|
1951
2088
|
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Compact always-on block of active goals. Used when no keyword match
|
|
2091
|
+
* fires so the agent still sees what it's supposed to be working on.
|
|
2092
|
+
* Scoped: for agent sessions, includes that agent's goals plus any
|
|
2093
|
+
* clementine-owned goals it might contribute to.
|
|
2094
|
+
*/
|
|
2095
|
+
formatActiveGoalsBlock(agentSlug) {
|
|
2096
|
+
try {
|
|
2097
|
+
return buildActiveGoalsBlock(this.loadGoalsFromCache(), agentSlug);
|
|
2098
|
+
}
|
|
2099
|
+
catch {
|
|
2100
|
+
return '';
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
1952
2103
|
// ── Chat ──────────────────────────────────────────────────────────
|
|
1953
2104
|
async chat(text, sessionKey, options) {
|
|
1954
2105
|
const onText = options?.onText;
|
|
@@ -2182,8 +2333,38 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2182
2333
|
this.worthExtracting(text, responseText)) {
|
|
2183
2334
|
this.spawnMemoryExtraction(text, responseText, key, profile).catch(err => logger.debug({ err }, 'Memory extraction failed'));
|
|
2184
2335
|
}
|
|
2336
|
+
// Score outcome-driven salience: for the chunks we retrieved this turn,
|
|
2337
|
+
// check which actually showed up in the response and adjust their
|
|
2338
|
+
// `last_outcome_score`. Fire-and-forget; failure is non-fatal.
|
|
2339
|
+
if (key && responseText && !isApiError) {
|
|
2340
|
+
this.scoreRetrievalOutcomes(key, responseText);
|
|
2341
|
+
}
|
|
2185
2342
|
return [responseText, sessionId];
|
|
2186
2343
|
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Compare retrieved chunks against the response text and record which
|
|
2346
|
+
* were referenced. Uses a distinctive-token overlap heuristic — cheap,
|
|
2347
|
+
* deterministic, no extra LLM calls. Called right after a turn completes.
|
|
2348
|
+
*/
|
|
2349
|
+
scoreRetrievalOutcomes(sessionKey, responseText) {
|
|
2350
|
+
const stash = this._lastRetrievedChunks.get(sessionKey);
|
|
2351
|
+
if (!stash || stash.length === 0)
|
|
2352
|
+
return;
|
|
2353
|
+
this._lastRetrievedChunks.delete(sessionKey);
|
|
2354
|
+
if (!this.memoryStore || typeof this.memoryStore.recordOutcome !== 'function')
|
|
2355
|
+
return;
|
|
2356
|
+
try {
|
|
2357
|
+
const responseLower = responseText.toLowerCase();
|
|
2358
|
+
const outcomes = stash.map(({ id, content }) => {
|
|
2359
|
+
const referenced = chunkReferencedInResponse(content, responseLower);
|
|
2360
|
+
return { chunkId: id, referenced };
|
|
2361
|
+
});
|
|
2362
|
+
this.memoryStore.recordOutcome(outcomes, sessionKey);
|
|
2363
|
+
}
|
|
2364
|
+
catch (err) {
|
|
2365
|
+
logger.debug({ err, sessionKey }, 'Outcome scoring failed');
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2187
2368
|
// ── Run Query ─────────────────────────────────────────────────────
|
|
2188
2369
|
static RATE_LIMIT_MAX_RETRIES = 3;
|
|
2189
2370
|
static RATE_LIMIT_BACKOFF = [5000, 15000, 30000];
|
|
@@ -2237,11 +2418,19 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2237
2418
|
const projDesc = matchedProject.description ? ` — ${matchedProject.description}` : '';
|
|
2238
2419
|
retrievalContext = `## Active Project: ${projName}${projDesc}\n\nYou are operating in the context of the **${projName}** project at \`${matchedProject.path}\`. You have access to this project's tools, MCP servers, and configuration.\n\n${retrievalContext}`;
|
|
2239
2420
|
}
|
|
2240
|
-
// Inject matching goal context so the agent is goal-aware without tool calls
|
|
2421
|
+
// Inject matching goal context so the agent is goal-aware without tool calls.
|
|
2422
|
+
// If no keyword match, fall back to a compact always-on block so active
|
|
2423
|
+
// goals stay in context even when the user message doesn't mention them —
|
|
2424
|
+
// this is what keeps multi-session work coherent across tangential turns.
|
|
2241
2425
|
const goalContext = this.matchGoals(prompt);
|
|
2242
2426
|
if (goalContext) {
|
|
2243
2427
|
retrievalContext += goalContext;
|
|
2244
2428
|
}
|
|
2429
|
+
else {
|
|
2430
|
+
const proactive = this.formatActiveGoalsBlock(profile?.slug);
|
|
2431
|
+
if (proactive)
|
|
2432
|
+
retrievalContext += proactive;
|
|
2433
|
+
}
|
|
2245
2434
|
// Timeout: abort the query after timeoutMs to prevent hour-long stalls.
|
|
2246
2435
|
// Works with or without an existing abortController from the gateway.
|
|
2247
2436
|
let timeoutHandle;
|
|
@@ -2269,8 +2458,15 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2269
2458
|
if (sessionKey && this.sessions.has(sessionKey)) {
|
|
2270
2459
|
sdkOptions.resume = this.sessions.get(sessionKey);
|
|
2271
2460
|
}
|
|
2272
|
-
// Context window guard: estimate token usage and bail if too tight
|
|
2273
|
-
|
|
2461
|
+
// Context window guard: estimate token usage and bail if too tight.
|
|
2462
|
+
// systemPrompt may be a plain string or a string[] with a boundary
|
|
2463
|
+
// sentinel — sum across the array elements so the estimate is honest.
|
|
2464
|
+
const sp = sdkOptions.systemPrompt;
|
|
2465
|
+
const systemPromptText = typeof sp === 'string'
|
|
2466
|
+
? sp
|
|
2467
|
+
: Array.isArray(sp)
|
|
2468
|
+
? sp.filter((s) => typeof s === 'string' && s !== SYSTEM_PROMPT_DYNAMIC_BOUNDARY).join('\n\n')
|
|
2469
|
+
: '';
|
|
2274
2470
|
const systemPromptTokens = estimateTokens(systemPromptText);
|
|
2275
2471
|
const promptTokens = estimateTokens(prompt);
|
|
2276
2472
|
const totalEstimate = systemPromptTokens + promptTokens;
|
|
@@ -2404,7 +2600,11 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2404
2600
|
const errorText = 'errors' in result ? result.errors.join('; ') : ('result' in result ? result.result : '');
|
|
2405
2601
|
if (errorText) {
|
|
2406
2602
|
const lower = errorText.toLowerCase();
|
|
2407
|
-
|
|
2603
|
+
// Strict match — only fire on the actual dollar-budget
|
|
2604
|
+
// marker. The bare word "budget" was matching Anthropic's
|
|
2605
|
+
// unrelated "does not support user-configurable task
|
|
2606
|
+
// budgets" 400, which killed Haiku chats.
|
|
2607
|
+
if (lower.includes('max_budget_usd')) {
|
|
2408
2608
|
logger.warn({ sessionKey }, 'Chat query hit budget cap');
|
|
2409
2609
|
responseText = responseText || (`I hit the $${BUDGET.chat.toFixed(2)} cost cap for this query. Options:\n` +
|
|
2410
2610
|
`• Break it into smaller requests\n` +
|
|
@@ -3050,7 +3250,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3050
3250
|
cwd: BASE_DIR,
|
|
3051
3251
|
env: SAFE_ENV,
|
|
3052
3252
|
effort: 'low',
|
|
3053
|
-
|
|
3253
|
+
// Budgets are opt-in. If BUDGET.summarization is undefined we
|
|
3254
|
+
// must NOT include the key — some SDK codepaths treat a present
|
|
3255
|
+
// undefined as a budget=0 cap.
|
|
3256
|
+
...(BUDGET.summarization ? { maxBudgetUsd: BUDGET.summarization } : {}),
|
|
3054
3257
|
},
|
|
3055
3258
|
});
|
|
3056
3259
|
for await (const message of stream) {
|
|
@@ -3368,7 +3571,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3368
3571
|
cwd: BASE_DIR,
|
|
3369
3572
|
env: SAFE_ENV,
|
|
3370
3573
|
effort: 'low',
|
|
3371
|
-
maxBudgetUsd: BUDGET.memoryExtraction,
|
|
3574
|
+
...(BUDGET.memoryExtraction ? { maxBudgetUsd: BUDGET.memoryExtraction } : {}),
|
|
3372
3575
|
},
|
|
3373
3576
|
});
|
|
3374
3577
|
const collectedText = [];
|
|
@@ -3812,11 +4015,16 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3812
4015
|
const result = message;
|
|
3813
4016
|
// Capture terminal reason for execution advisor
|
|
3814
4017
|
this._lastTerminalReason = result.terminal_reason ?? undefined;
|
|
3815
|
-
// Detect budget
|
|
4018
|
+
// Detect ACTUAL dollar-budget cap — treat as permanent so cron
|
|
4019
|
+
// doesn't retry when we've intentionally capped spend. Use a
|
|
4020
|
+
// strict marker ("max_budget_usd") because the bare word
|
|
4021
|
+
// "budget" was catching Anthropic's unrelated "does not support
|
|
4022
|
+
// user-configurable task budgets" error and pinning perfectly
|
|
4023
|
+
// healthy Haiku jobs as permanent failures.
|
|
3816
4024
|
if (result.is_error && 'result' in result) {
|
|
3817
4025
|
const exitText = String(result.result ?? '');
|
|
3818
|
-
if (exitText.includes('max_budget_usd')
|
|
3819
|
-
logger.warn({ job: jobName }, 'Cron job hit budget cap — treating as permanent error');
|
|
4026
|
+
if (exitText.includes('max_budget_usd')) {
|
|
4027
|
+
logger.warn({ job: jobName }, 'Cron job hit dollar budget cap — treating as permanent error');
|
|
3820
4028
|
throw new Error(`Budget exceeded for cron job '${jobName}'`);
|
|
3821
4029
|
}
|
|
3822
4030
|
}
|
|
@@ -3919,7 +4127,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3919
4127
|
cwd: BASE_DIR,
|
|
3920
4128
|
env: SAFE_ENV,
|
|
3921
4129
|
effort: 'low',
|
|
3922
|
-
maxBudgetUsd: BUDGET.reflection,
|
|
4130
|
+
...(BUDGET.reflection ? { maxBudgetUsd: BUDGET.reflection } : {}),
|
|
3923
4131
|
outputFormat: {
|
|
3924
4132
|
type: 'json_schema',
|
|
3925
4133
|
schema: {
|
|
@@ -4065,7 +4273,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4065
4273
|
model: model ?? null,
|
|
4066
4274
|
enableTeams: true,
|
|
4067
4275
|
isUnleashed: true,
|
|
4068
|
-
|
|
4276
|
+
// buildOptions intentionally drops this before reaching the SDK
|
|
4277
|
+
// (line ~2100 comment). Passing it here only matters if someone
|
|
4278
|
+
// later re-enables the SDK knob.
|
|
4279
|
+
...(BUDGET.unleashedPhase ? { maxBudgetUsd: BUDGET.unleashedPhase } : {}),
|
|
4069
4280
|
stallGuard: phaseGuard,
|
|
4070
4281
|
profile: unleashedProfile,
|
|
4071
4282
|
});
|
|
@@ -4255,11 +4466,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4255
4466
|
// Capture terminal reason for execution advisor
|
|
4256
4467
|
this._lastTerminalReason = result.terminal_reason ?? undefined;
|
|
4257
4468
|
this.logQueryResult(result, 'unleashed', `unleashed:${jobName}`, jobName);
|
|
4258
|
-
// Detect budget exceeded
|
|
4469
|
+
// Detect dollar-budget exceeded (strict marker — see cron
|
|
4470
|
+
// handler above for the reasoning).
|
|
4259
4471
|
if (result.is_error && 'result' in result) {
|
|
4260
4472
|
const exitText = String(result.result ?? '');
|
|
4261
|
-
if (exitText.includes('max_budget_usd')
|
|
4262
|
-
logger.warn({ job: jobName, phase }, 'Unleashed phase hit budget cap');
|
|
4473
|
+
if (exitText.includes('max_budget_usd')) {
|
|
4474
|
+
logger.warn({ job: jobName, phase }, 'Unleashed phase hit dollar budget cap');
|
|
4263
4475
|
appendProgress({ event: 'budget_exceeded', phase });
|
|
4264
4476
|
}
|
|
4265
4477
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStore adapter: mirrors the Claude Agent SDK's JSONL session
|
|
3
|
+
* transcript into Clementine's SQLite memory store so resume works from
|
|
4
|
+
* the durable store instead of local files.
|
|
5
|
+
*
|
|
6
|
+
* Introduced after upgrading to @anthropic-ai/claude-agent-sdk 0.2.119.
|
|
7
|
+
* The SDK still writes to local disk first (durability is guaranteed
|
|
8
|
+
* before our adapter sees the batch); this adapter is the secondary
|
|
9
|
+
* copy and is the source of truth for long-term resume.
|
|
10
|
+
*/
|
|
11
|
+
import { type SessionStore } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
+
import type { MemoryStoreType } from '../tools/shared.js';
|
|
13
|
+
export declare function createMemorySessionStore(store: MemoryStoreType): SessionStore;
|
|
14
|
+
//# sourceMappingURL=session-store-adapter.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStore adapter: mirrors the Claude Agent SDK's JSONL session
|
|
3
|
+
* transcript into Clementine's SQLite memory store so resume works from
|
|
4
|
+
* the durable store instead of local files.
|
|
5
|
+
*
|
|
6
|
+
* Introduced after upgrading to @anthropic-ai/claude-agent-sdk 0.2.119.
|
|
7
|
+
* The SDK still writes to local disk first (durability is guaranteed
|
|
8
|
+
* before our adapter sees the batch); this adapter is the secondary
|
|
9
|
+
* copy and is the source of truth for long-term resume.
|
|
10
|
+
*/
|
|
11
|
+
import { foldSessionSummary, } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
+
function subkey(key) {
|
|
13
|
+
return key.subpath ?? '';
|
|
14
|
+
}
|
|
15
|
+
export function createMemorySessionStore(store) {
|
|
16
|
+
const s = store;
|
|
17
|
+
return {
|
|
18
|
+
async append(key, entries) {
|
|
19
|
+
if (entries.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
const sub = subkey(key);
|
|
22
|
+
// Persist the raw entries first so load() is coherent even if the
|
|
23
|
+
// summary sidecar fold throws.
|
|
24
|
+
s.appendSessionEntries(key.sessionId, key.projectKey, sub, entries);
|
|
25
|
+
// Maintain the incrementally-folded summary for cheap listing.
|
|
26
|
+
try {
|
|
27
|
+
const existing = s
|
|
28
|
+
.listSdkSessionSummaries(key.projectKey)
|
|
29
|
+
.find(row => row.sessionId === key.sessionId && row.subpath === sub);
|
|
30
|
+
const prev = existing
|
|
31
|
+
? {
|
|
32
|
+
sessionId: existing.sessionId,
|
|
33
|
+
mtime: existing.mtime,
|
|
34
|
+
data: existing.data,
|
|
35
|
+
}
|
|
36
|
+
: undefined;
|
|
37
|
+
const next = foldSessionSummary(prev, key, entries);
|
|
38
|
+
s.upsertSessionSummary(key.sessionId, sub, key.projectKey, Date.now(), next.data);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Non-fatal — summary is a convenience, not a correctness concern.
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async load(key) {
|
|
45
|
+
const rows = s.loadSessionEntries(key.sessionId, subkey(key));
|
|
46
|
+
if (rows === null)
|
|
47
|
+
return null;
|
|
48
|
+
return rows;
|
|
49
|
+
},
|
|
50
|
+
async listSessions(projectKey) {
|
|
51
|
+
return s.listSdkSessions(projectKey);
|
|
52
|
+
},
|
|
53
|
+
async listSessionSummaries(projectKey) {
|
|
54
|
+
return s
|
|
55
|
+
.listSdkSessionSummaries(projectKey)
|
|
56
|
+
.filter(r => r.subpath === '')
|
|
57
|
+
.map(r => ({ sessionId: r.sessionId, mtime: r.mtime, data: r.data }));
|
|
58
|
+
},
|
|
59
|
+
async delete(key) {
|
|
60
|
+
// SDK passes per-key deletes; we scope the delete to all subpaths
|
|
61
|
+
// under the session so a top-level delete wipes subagent trails too.
|
|
62
|
+
s.deleteSdkSession(key.sessionId);
|
|
63
|
+
},
|
|
64
|
+
async listSubkeys(key) {
|
|
65
|
+
return s.listSdkSessionSubkeys(key.sessionId);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=session-store-adapter.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine — Adapter common helpers.
|
|
3
|
+
*/
|
|
4
|
+
/** Truncated SHA-256 content hash, hex, first 16 chars. */
|
|
5
|
+
export declare function contentHash(text: string): string;
|
|
6
|
+
/** Build a stable externalId fallback from (source-hint, index, content). */
|
|
7
|
+
export declare function fallbackExternalId(hint: string, index: number, content: string): string;
|
|
8
|
+
/** Detect whether a value looks like a stable identifier column. */
|
|
9
|
+
export declare function looksLikeIdKey(key: string): boolean;
|
|
10
|
+
/** Pick a likely id column from a record's keys (for structured adapters). */
|
|
11
|
+
export declare function pickIdField(keys: string[]): string | null;
|
|
12
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine — Adapter common helpers.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
/** Truncated SHA-256 content hash, hex, first 16 chars. */
|
|
6
|
+
export function contentHash(text) {
|
|
7
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
8
|
+
}
|
|
9
|
+
/** Build a stable externalId fallback from (source-hint, index, content). */
|
|
10
|
+
export function fallbackExternalId(hint, index, content) {
|
|
11
|
+
return `${hint}-${index}-${contentHash(content)}`;
|
|
12
|
+
}
|
|
13
|
+
/** Detect whether a value looks like a stable identifier column. */
|
|
14
|
+
export function looksLikeIdKey(key) {
|
|
15
|
+
const lower = key.toLowerCase();
|
|
16
|
+
return (lower === 'id' ||
|
|
17
|
+
lower.endsWith('_id') ||
|
|
18
|
+
lower.endsWith('id') && lower.length <= 6 ||
|
|
19
|
+
lower === 'uuid' || lower === 'guid' || lower === 'uid' ||
|
|
20
|
+
lower === 'email' || lower === 'message_id' || lower === 'sfid');
|
|
21
|
+
}
|
|
22
|
+
/** Pick a likely id column from a record's keys (for structured adapters). */
|
|
23
|
+
export function pickIdField(keys) {
|
|
24
|
+
for (const k of keys)
|
|
25
|
+
if (looksLikeIdKey(k))
|
|
26
|
+
return k;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=common.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine — CSV adapter.
|
|
3
|
+
*
|
|
4
|
+
* Streams rows from a CSV file (comma- or tab-separated). Each row is a
|
|
5
|
+
* RawRecord with stringified JSON content so the downstream pipeline can
|
|
6
|
+
* template/distill it the same way as any other structured source.
|
|
7
|
+
*/
|
|
8
|
+
import type { RawRecord } from '../../types.js';
|
|
9
|
+
export declare function parseCsv(filePath: string): AsyncIterable<RawRecord>;
|
|
10
|
+
//# sourceMappingURL=csv.d.ts.map
|