clementine-agent 1.18.175 → 1.18.177
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.js +1 -0
- package/dist/agent/run-agent.d.ts +4 -0
- package/dist/agent/run-agent.js +26 -4
- package/dist/agent/run-skill.js +3 -0
- package/dist/gateway/router.d.ts +5 -0
- package/dist/gateway/router.js +64 -6
- package/dist/tools/background-task-tools.js +3 -2
- package/dist/tools/mcp-server.d.ts +1 -1
- package/dist/tools/mcp-server.js +1 -1
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.js +2 -0
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -2343,6 +2343,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2343
2343
|
...process.env,
|
|
2344
2344
|
CLEMENTINE_HOME: BASE_DIR,
|
|
2345
2345
|
CLEMENTINE_TEAM_AGENT: profile?.slug ?? 'clementine',
|
|
2346
|
+
...(sessionKey ? { CLEMENTINE_SESSION_KEY: sessionKey } : {}),
|
|
2346
2347
|
CLEMENTINE_INTERACTION_SOURCE: sourceOverride ?? inferInteractionSource(sessionKey),
|
|
2347
2348
|
CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
|
|
2348
2349
|
CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
|
|
@@ -75,6 +75,10 @@ export interface RunAgentOptions {
|
|
|
75
75
|
/** Optional explicit allowedTools list. When unset, falls back to a sensible default
|
|
76
76
|
* including Agent (so subagents can be spawned) + core SDK tools + Clementine MCP. */
|
|
77
77
|
allowedTools?: string[];
|
|
78
|
+
/** Extra tools to pre-approve without making their built-in tools visible to
|
|
79
|
+
* the main agent. Useful when the main agent may only call Agent, but the
|
|
80
|
+
* forced subagent still needs pre-approved MCP/Clementine tools. */
|
|
81
|
+
permissionTools?: string[];
|
|
78
82
|
/** SDK permission mode. Defaults to dontAsk so allowedTools is enforceable.
|
|
79
83
|
* Only explicit operator/full-surface paths should request bypassPermissions. */
|
|
80
84
|
permissionMode?: ExecutionPermissionMode;
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -254,6 +254,27 @@ export async function runAgent(prompt, opts) {
|
|
|
254
254
|
clementineServerName: TOOLS_SERVER,
|
|
255
255
|
permissionMode: opts.permissionMode,
|
|
256
256
|
});
|
|
257
|
+
const permissionToolPolicy = opts.permissionTools
|
|
258
|
+
? buildExecutionToolPolicy({
|
|
259
|
+
requestedTools: opts.permissionTools,
|
|
260
|
+
defaultBuiltins: CORE_TOOLS_FOR_AGENT_PARENT,
|
|
261
|
+
mcpServerNames: policyMcpServerNames,
|
|
262
|
+
clementineServerName: TOOLS_SERVER,
|
|
263
|
+
permissionMode: opts.permissionMode,
|
|
264
|
+
})
|
|
265
|
+
: null;
|
|
266
|
+
const sdkAllowedTools = permissionToolPolicy
|
|
267
|
+
? Array.from(new Set([...toolPolicy.allowedTools, ...permissionToolPolicy.allowedTools])).sort()
|
|
268
|
+
: toolPolicy.allowedTools;
|
|
269
|
+
const clementineToolAllowlist = (() => {
|
|
270
|
+
if (!permissionToolPolicy)
|
|
271
|
+
return toolPolicy.clementineToolAllowlist;
|
|
272
|
+
const parts = [toolPolicy.clementineToolAllowlist, permissionToolPolicy.clementineToolAllowlist]
|
|
273
|
+
.flatMap(v => v.split(',').map(s => s.trim()).filter(Boolean));
|
|
274
|
+
if (parts.includes('*'))
|
|
275
|
+
return '*';
|
|
276
|
+
return Array.from(new Set(parts)).sort().join(',');
|
|
277
|
+
})();
|
|
257
278
|
// SDK accepts a Record<string, McpServerConfig> here. We cast on
|
|
258
279
|
// assignment because we mix the always-on Clementine stdio server
|
|
259
280
|
// with caller-supplied servers of various types.
|
|
@@ -266,8 +287,9 @@ export async function runAgent(prompt, opts) {
|
|
|
266
287
|
...subprocessEnv,
|
|
267
288
|
CLEMENTINE_HOME: BASE_DIR,
|
|
268
289
|
...(opts.profile?.slug ? { CLEMENTINE_TEAM_AGENT: opts.profile.slug } : {}),
|
|
290
|
+
CLEMENTINE_SESSION_KEY: opts.sessionKey,
|
|
269
291
|
CLEMENTINE_INTERACTION_SOURCE: interactionSourceForSession(opts.sessionKey, source),
|
|
270
|
-
CLEMENTINE_TOOL_ALLOWLIST:
|
|
292
|
+
CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
|
|
271
293
|
},
|
|
272
294
|
},
|
|
273
295
|
...baseMcpServers,
|
|
@@ -407,7 +429,7 @@ export async function runAgent(prompt, opts) {
|
|
|
407
429
|
// callers can mix stdio + http + sse server shapes.
|
|
408
430
|
mcpServers: mcpServers,
|
|
409
431
|
tools: toolPolicy.builtinTools,
|
|
410
|
-
allowedTools:
|
|
432
|
+
allowedTools: sdkAllowedTools,
|
|
411
433
|
permissionMode: toolPolicy.permissionMode,
|
|
412
434
|
...(sessionStore ? { sessionStore } : {}),
|
|
413
435
|
...(toolPolicy.allowDangerouslySkipPermissions
|
|
@@ -447,7 +469,7 @@ export async function runAgent(prompt, opts) {
|
|
|
447
469
|
maxBudgetUsd: maxBudgetUsd ?? 'uncapped',
|
|
448
470
|
agentCount: Object.keys(agents).length,
|
|
449
471
|
allowedToolCount: allowedTools.length,
|
|
450
|
-
sdkAllowedToolCount:
|
|
472
|
+
sdkAllowedToolCount: sdkAllowedTools.length,
|
|
451
473
|
builtinToolCount: toolPolicy.builtinTools.length,
|
|
452
474
|
permissionMode: toolPolicy.permissionMode,
|
|
453
475
|
mcpServerCount: Object.keys(mcpServers).length,
|
|
@@ -739,7 +761,7 @@ export async function runAgent(prompt, opts) {
|
|
|
739
761
|
...(usage ? { usage } : {}),
|
|
740
762
|
runId,
|
|
741
763
|
permissionMode: toolPolicy.permissionMode,
|
|
742
|
-
allowedToolsApplied:
|
|
764
|
+
allowedToolsApplied: sdkAllowedTools,
|
|
743
765
|
builtinToolsApplied: toolPolicy.builtinTools,
|
|
744
766
|
mcpServersApplied: Object.keys(mcpServers),
|
|
745
767
|
};
|
package/dist/agent/run-skill.js
CHANGED
|
@@ -417,6 +417,9 @@ export async function runSkill(name, options = {}) {
|
|
|
417
417
|
// the parent's context shape predictable and prevents it from
|
|
418
418
|
// doing data-heavy work itself even if the LLM disagrees.
|
|
419
419
|
allowedTools: ['Agent'],
|
|
420
|
+
// SDK permissions are session-level, so the worker's tools still
|
|
421
|
+
// need to be pre-approved even though the parent only sees Agent.
|
|
422
|
+
permissionTools: ['Agent', ...effectiveTools],
|
|
420
423
|
// Force-routing: SDK wraps the prompt with "Use the skill-worker
|
|
421
424
|
// agent to handle this request" so dispatch is the natural
|
|
422
425
|
// first action.
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ import type { NotificationDispatcher } from './notifications.js';
|
|
|
13
13
|
import { type ProactiveNotificationInput } from './notification-context.js';
|
|
14
14
|
import { type ToolsetName } from '../agent/toolsets.js';
|
|
15
15
|
export { isLiveUnleashedStatus } from './unleashed-status.js';
|
|
16
|
+
export declare function buildContextOverflowRetryPrompt(opts: {
|
|
17
|
+
chatPrompt: string;
|
|
18
|
+
turnContextPrefix?: string;
|
|
19
|
+
project?: ProjectMeta | null;
|
|
20
|
+
}): string;
|
|
16
21
|
export type ChatErrorKind = 'rate_limit' | 'one_million_context' | 'context_overflow' | 'auth' | 'billing' | 'transient' | 'unknown';
|
|
17
22
|
export declare function classifyChatError(err: unknown): ChatErrorKind;
|
|
18
23
|
/** Detect auth-like errors in response text that the SDK returned as "successful" results. */
|
package/dist/gateway/router.js
CHANGED
|
@@ -49,6 +49,8 @@ const CHAT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
49
49
|
* Safety net so no session runs forever, even if active.
|
|
50
50
|
* Primary guardrail is cost budget (maxBudgetUsd), not this timer. */
|
|
51
51
|
const CHAT_MAX_WALL_MS = 30 * 60 * 1000;
|
|
52
|
+
const CHAT_CONTEXT_RETRY_CONTEXT_MAX_CHARS = 6_000;
|
|
53
|
+
const CHAT_CONTEXT_RETRY_SYSTEM_MAX_CHARS = 16_000;
|
|
52
54
|
const BACKGROUND_TASK_ID_RE = /\bbg-[a-z0-9]+-[a-f0-9]{6}\b/i;
|
|
53
55
|
function collectRunToolNames(runId) {
|
|
54
56
|
if (!runId)
|
|
@@ -74,6 +76,26 @@ function compactToolNames(names) {
|
|
|
74
76
|
}
|
|
75
77
|
return out;
|
|
76
78
|
}
|
|
79
|
+
function trimContextRecoveryText(text, maxChars) {
|
|
80
|
+
if (!text || text.length <= maxChars)
|
|
81
|
+
return text;
|
|
82
|
+
return `${text.slice(0, maxChars - 80).trimEnd()}\n\n[context recovery: trimmed oversized carry-over context]`;
|
|
83
|
+
}
|
|
84
|
+
export function buildContextOverflowRetryPrompt(opts) {
|
|
85
|
+
const parts = [
|
|
86
|
+
'[Context recovery: the previous SDK session was too large, so this is a fresh session. Continue with the current user request. Do not ask the user to resend it.]',
|
|
87
|
+
];
|
|
88
|
+
if (opts.project?.path) {
|
|
89
|
+
const description = opts.project.description ? ` (${opts.project.description})` : '';
|
|
90
|
+
parts.push(`[Active project: ${opts.project.path}${description}]`);
|
|
91
|
+
}
|
|
92
|
+
const compactContext = trimContextRecoveryText((opts.turnContextPrefix ?? '').trim(), CHAT_CONTEXT_RETRY_CONTEXT_MAX_CHARS);
|
|
93
|
+
if (compactContext) {
|
|
94
|
+
parts.push(compactContext);
|
|
95
|
+
}
|
|
96
|
+
parts.push(opts.chatPrompt);
|
|
97
|
+
return parts.filter(Boolean).join('\n\n');
|
|
98
|
+
}
|
|
77
99
|
export function classifyChatError(err) {
|
|
78
100
|
const msg = String(err);
|
|
79
101
|
if (isCreditBalanceError(msg))
|
|
@@ -82,7 +104,7 @@ export function classifyChatError(err) {
|
|
|
82
104
|
return 'rate_limit';
|
|
83
105
|
if (looksLikeClaudeOneMillionContextError(msg))
|
|
84
106
|
return 'one_million_context';
|
|
85
|
-
if (/context.?length|token.?limit|maximum.?context|prompt.?too.?long|rapid_refill_breaker|autocompact|context.?refilled/i.test(msg))
|
|
107
|
+
if (/context.?length|token.?limit|maximum.?context|prompt(?:\s+is)?.?too.?long|input.?too.?long|rapid_refill_breaker|autocompact|context.?refilled/i.test(msg))
|
|
86
108
|
return 'context_overflow';
|
|
87
109
|
if (/\b401\b|\b403\b|auth|forbidden|invalid.?api.?key|permission|does not have access|please run \/login/i.test(msg))
|
|
88
110
|
return 'auth';
|
|
@@ -2117,6 +2139,7 @@ export class Gateway {
|
|
|
2117
2139
|
const chatSystemAppend = resolvedSkills && resolvedSkills.promptBlock
|
|
2118
2140
|
? (baseSystemAppend ? `${baseSystemAppend}\n\n${resolvedSkills.promptBlock}` : resolvedSkills.promptBlock)
|
|
2119
2141
|
: baseSystemAppend;
|
|
2142
|
+
const retrySystemAppend = trimContextRecoveryText(chatSystemAppend, CHAT_CONTEXT_RETRY_SYSTEM_MAX_CHARS);
|
|
2120
2143
|
// Per-turn context (recall + persistent learnings + silent
|
|
2121
2144
|
// blocks + security/toolset directives) — real chat only.
|
|
2122
2145
|
// Builder doesn't need recall of unrelated transcripts.
|
|
@@ -2156,7 +2179,7 @@ export class Gateway {
|
|
|
2156
2179
|
skillMatchNames: resolvedSkills?.matches.map(m => m.name) ?? [],
|
|
2157
2180
|
skillHintedMcpServers: resolvedSkills?.hintedMcpServers ?? [],
|
|
2158
2181
|
}, 'Routing chat through runAgent');
|
|
2159
|
-
const
|
|
2182
|
+
const buildRunAgentChatOptions = (opts) => ({
|
|
2160
2183
|
sessionKey: effectiveSessionKey,
|
|
2161
2184
|
source: 'chat',
|
|
2162
2185
|
profile: resolvedProfile,
|
|
@@ -2166,8 +2189,8 @@ export class Gateway {
|
|
|
2166
2189
|
...(maxTurns ? { maxTurns } : {}),
|
|
2167
2190
|
...(chatBudget !== undefined ? { maxBudgetUsd: chatBudget } : {}),
|
|
2168
2191
|
...(builderAllowedTools ? { allowedTools: builderAllowedTools } : {}),
|
|
2169
|
-
...(
|
|
2170
|
-
...(
|
|
2192
|
+
...(opts.systemPromptAppend ? { systemPromptAppend: opts.systemPromptAppend } : {}),
|
|
2193
|
+
...(opts.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {}),
|
|
2171
2194
|
...(chatMcp ? { extraMcpServers: chatMcp.servers } : {}),
|
|
2172
2195
|
onText: wrappedOnText,
|
|
2173
2196
|
onToolActivity: ({ tool, input }) => {
|
|
@@ -2178,6 +2201,38 @@ export class Gateway {
|
|
|
2178
2201
|
},
|
|
2179
2202
|
abortSignal: chatAc.signal,
|
|
2180
2203
|
});
|
|
2204
|
+
let runAgentResult;
|
|
2205
|
+
try {
|
|
2206
|
+
runAgentResult = await runAgent(finalPrompt, buildRunAgentChatOptions({
|
|
2207
|
+
...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
|
|
2208
|
+
...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
|
|
2209
|
+
}));
|
|
2210
|
+
}
|
|
2211
|
+
catch (err) {
|
|
2212
|
+
if (chatAc.signal.aborted || classifyChatError(err) !== 'context_overflow') {
|
|
2213
|
+
throw err;
|
|
2214
|
+
}
|
|
2215
|
+
const retryPrompt = buildContextOverflowRetryPrompt({
|
|
2216
|
+
chatPrompt,
|
|
2217
|
+
turnContextPrefix,
|
|
2218
|
+
project: sess?.project ?? null,
|
|
2219
|
+
});
|
|
2220
|
+
logger.info({
|
|
2221
|
+
sessionKey: effectiveSessionKey,
|
|
2222
|
+
hadResume: !!priorSdkSessionId,
|
|
2223
|
+
promptChars: finalPrompt.length,
|
|
2224
|
+
retryPromptChars: retryPrompt.length,
|
|
2225
|
+
systemAppendChars: chatSystemAppend.length,
|
|
2226
|
+
retrySystemAppendChars: retrySystemAppend.length,
|
|
2227
|
+
}, 'Context overflow — retrying current message in fresh SDK session');
|
|
2228
|
+
if (onProgress) {
|
|
2229
|
+
await onProgress('refreshing conversation context...').catch(() => { });
|
|
2230
|
+
}
|
|
2231
|
+
this.assistant.clearSession(effectiveSessionKey);
|
|
2232
|
+
runAgentResult = await runAgent(retryPrompt, buildRunAgentChatOptions({
|
|
2233
|
+
...(retrySystemAppend ? { systemPromptAppend: retrySystemAppend } : {}),
|
|
2234
|
+
}));
|
|
2235
|
+
}
|
|
2181
2236
|
if (ledgerRunMetadata) {
|
|
2182
2237
|
ledgerRunMetadata.runId = runAgentResult.runId;
|
|
2183
2238
|
ledgerRunMetadata.executionMode = ledgerRunMetadata.executionMode ?? 'inline';
|
|
@@ -2368,7 +2423,10 @@ export class Gateway {
|
|
|
2368
2423
|
if (scheduledSkillName) {
|
|
2369
2424
|
const { runSkill } = await import('../agent/run-skill.js');
|
|
2370
2425
|
const configuredCap = tier >= 2 ? BUDGET.cronT2 : BUDGET.cronT1;
|
|
2371
|
-
|
|
2426
|
+
// Pass 0 through intentionally. It means "uncapped" and must
|
|
2427
|
+
// override skill-local clementine.limits.maxBudgetUsd values when
|
|
2428
|
+
// the operator has disabled global cron budgets.
|
|
2429
|
+
const scheduledSkillBudget = configuredCap > 0 ? configuredCap : 0;
|
|
2372
2430
|
logger.info({ jobName, skill: scheduledSkillName, agentSlug, tier, wallMs, path: 'run_skill' }, 'Routing scheduled skill through runSkill');
|
|
2373
2431
|
const skillResult = await runSkill(scheduledSkillName, {
|
|
2374
2432
|
sessionKey: `cron:${jobName}`,
|
|
@@ -2380,7 +2438,7 @@ export class Gateway {
|
|
|
2380
2438
|
projectWorkDir: workDir,
|
|
2381
2439
|
model,
|
|
2382
2440
|
...(maxTurns ? { maxTurns } : {}),
|
|
2383
|
-
|
|
2441
|
+
maxBudgetUsd: scheduledSkillBudget,
|
|
2384
2442
|
abortSignal: cronAc.signal,
|
|
2385
2443
|
context: `[Scheduled skill: ${jobName}]`,
|
|
2386
2444
|
});
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { createBackgroundTask, listBackgroundTasks, loadBackgroundTask, } from '../agent/background-tasks.js';
|
|
16
|
-
import { ACTIVE_AGENT_SLUG, logger, textResult } from './shared.js';
|
|
16
|
+
import { ACTIVE_AGENT_SLUG, ACTIVE_SESSION_KEY, logger, textResult } from './shared.js';
|
|
17
17
|
const DEFAULT_MAX_MINUTES = 30;
|
|
18
18
|
export function registerBackgroundTaskTools(server) {
|
|
19
19
|
server.tool('start_background_task', 'Kick off a long-running autonomous task in the background. Use when the work would burn the chat context (deep research, multi-page extraction, batch processing) or take longer than a chat turn. Returns a task id immediately. The daemon picks the task up within seconds, runs it with your profile + tools, and posts the deliverable to your Discord channel when done.', {
|
|
@@ -30,8 +30,9 @@ export function registerBackgroundTaskTools(server) {
|
|
|
30
30
|
fromAgent,
|
|
31
31
|
prompt: trimmed,
|
|
32
32
|
maxMinutes: cap,
|
|
33
|
+
...(ACTIVE_SESSION_KEY ? { sessionKey: ACTIVE_SESSION_KEY } : {}),
|
|
33
34
|
});
|
|
34
|
-
logger.info({ id: task.id, fromAgent, maxMinutes: task.maxMinutes }, 'Background task queued');
|
|
35
|
+
logger.info({ id: task.id, fromAgent, sessionKey: task.sessionKey, maxMinutes: task.maxMinutes }, 'Background task queued');
|
|
35
36
|
return textResult(`Queued **${task.id}** (max ${task.maxMinutes} min). The daemon will pick it up within a few seconds and run it in the background. You'll get a notification in your channel when the deliverable lands. Use \`get_background_task\` to check status.`);
|
|
36
37
|
});
|
|
37
38
|
server.tool('get_background_task', 'Check the status of a background task. Returns its lifecycle state (pending|running|done|failed|aborted|interrupted), how long it has been running, and the result/error if terminal.', {
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
* Usage:
|
|
9
9
|
* npx tsx src/tools/mcp-server.ts
|
|
10
10
|
*/
|
|
11
|
-
export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG } from './shared.js';
|
|
11
|
+
export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG, ACTIVE_SESSION_KEY } from './shared.js';
|
|
12
12
|
export type { MemoryStoreType } from './shared.js';
|
|
13
13
|
//# sourceMappingURL=mcp-server.d.ts.map
|
package/dist/tools/mcp-server.js
CHANGED
|
@@ -15,7 +15,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { BASE_DIR, VAULT_DIR, env, getStore, getStoreSync, logger, textResult, externalResult, } from './shared.js';
|
|
17
17
|
// Re-export for any code that imports from mcp-server.ts directly
|
|
18
|
-
export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG } from './shared.js';
|
|
18
|
+
export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG, ACTIVE_SESSION_KEY } from './shared.js';
|
|
19
19
|
// ── Tool modules ────────────────────────────────────────────────────────
|
|
20
20
|
import { registerMemoryTools } from './memory-tools.js';
|
|
21
21
|
import { registerVaultTools } from './vault-tools.js';
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -671,6 +671,7 @@ export type MemoryStoreType = {
|
|
|
671
671
|
export declare function getStore(): Promise<MemoryStoreType>;
|
|
672
672
|
export declare function getStoreSync(): MemoryStoreType | null;
|
|
673
673
|
export declare const ACTIVE_AGENT_SLUG: string | null;
|
|
674
|
+
export declare const ACTIVE_SESSION_KEY: string | null;
|
|
674
675
|
export declare const GOALS_DIR: string;
|
|
675
676
|
export declare function agentTasksFile(slug: string | null): string;
|
|
676
677
|
export declare function agentWorkingMemoryFile(slug: string | null): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -59,6 +59,8 @@ export function getStoreSync() {
|
|
|
59
59
|
// ── Active Agent Slug ──────────────────────────────────────────────────
|
|
60
60
|
const _rawAgentSlug = process.env.CLEMENTINE_TEAM_AGENT || null;
|
|
61
61
|
export const ACTIVE_AGENT_SLUG = _rawAgentSlug === 'clementine' ? null : _rawAgentSlug;
|
|
62
|
+
const _rawSessionKey = process.env.CLEMENTINE_SESSION_KEY || null;
|
|
63
|
+
export const ACTIVE_SESSION_KEY = _rawSessionKey?.trim() || null;
|
|
62
64
|
// ── Agent-aware path helpers ───────────────────────────────────────────
|
|
63
65
|
// GOALS_DIR is defined in config.ts but not in shared.ts — define it here
|
|
64
66
|
export const GOALS_DIR = path.join(BASE_DIR, 'goals');
|