clementine-agent 1.0.11 → 1.0.13
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/agent-manager.js +32 -0
- package/dist/agent/assistant.js +8 -22
- package/dist/agent/daily-planner.js +5 -15
- package/dist/agent/insight-engine.js +4 -14
- package/dist/agent/prompt-evolver.js +7 -15
- package/dist/agent/self-improve.js +20 -26
- package/dist/agent/strategic-planner.js +5 -26
- package/dist/agent/team-bus.js +5 -14
- package/dist/cli/dashboard.js +2 -3
- package/dist/cli/routes/digest.d.ts +0 -1
- package/dist/cli/routes/digest.js +6 -11
- package/dist/cli/routes/goals.d.ts +5 -2
- package/dist/cli/routes/goals.js +85 -74
- package/dist/gateway/cron-scheduler.js +31 -20
- package/dist/gateway/heartbeat-scheduler.d.ts +3 -5
- package/dist/gateway/heartbeat-scheduler.js +8 -16
- package/dist/tools/goal-tools.d.ts +4 -2
- package/dist/tools/goal-tools.js +46 -58
- package/dist/tools/session-tools.js +14 -21
- package/dist/tools/shared.d.ts +40 -0
- package/dist/tools/shared.js +84 -0
- package/package.json +1 -1
package/dist/tools/goal-tools.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clementine TypeScript — Goal MCP tools.
|
|
3
3
|
*
|
|
4
|
-
* Persistent goals that drive proactive
|
|
5
|
-
*
|
|
4
|
+
* Persistent goals that survive across sessions and drive proactive behavior.
|
|
5
|
+
* Goals live per-owner: Clementine's at ~/.clementine/goals/, each agent's at
|
|
6
|
+
* ~/.clementine/vault/00-System/agents/{slug}/goals/. Helpers in shared.ts
|
|
7
|
+
* handle routing so tools here don't need to know the layout.
|
|
6
8
|
*/
|
|
7
9
|
import { randomBytes } from 'node:crypto';
|
|
8
|
-
import { existsSync, mkdirSync,
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
9
11
|
import path from 'node:path';
|
|
10
12
|
import { z } from 'zod';
|
|
11
|
-
import { BASE_DIR, logger, textResult } from './shared.js';
|
|
12
|
-
const GOALS_DIR = path.join(BASE_DIR, 'goals');
|
|
13
|
+
import { BASE_DIR, logger, textResult, listAllGoals, findGoalPath, readGoalById, writeGoalForOwner, } from './shared.js';
|
|
13
14
|
const GOAL_TRIGGER_DIR = path.join(BASE_DIR, 'cron', 'goal-triggers');
|
|
14
|
-
function ensureGoalsDir() {
|
|
15
|
-
if (!existsSync(GOALS_DIR))
|
|
16
|
-
mkdirSync(GOALS_DIR, { recursive: true });
|
|
17
|
-
}
|
|
18
15
|
export function registerGoalTools(server) {
|
|
19
|
-
server.tool('goal_create', 'Create a new persistent goal that survives across sessions. Goals drive proactive agent behavior and can be linked to cron jobs.', {
|
|
16
|
+
server.tool('goal_create', 'Create a new persistent goal that survives across sessions. Goals drive proactive agent behavior and can be linked to cron jobs. Agent goals live in that agent\'s own directory.', {
|
|
20
17
|
title: z.string().describe('Short goal title'),
|
|
21
18
|
description: z.string().describe('Detailed description of what this goal aims to achieve'),
|
|
22
|
-
owner: z.string().optional().describe('Agent slug that owns this goal (default: "clementine")'),
|
|
19
|
+
owner: z.string().optional().describe('Agent slug that owns this goal (default: "clementine"). Goal file is routed to the owner\'s directory.'),
|
|
23
20
|
priority: z.enum(['high', 'medium', 'low']).optional().describe('Priority level (default: "medium")'),
|
|
24
21
|
targetDate: z.string().optional().describe('Target completion date (YYYY-MM-DD)'),
|
|
25
22
|
nextActions: z.array(z.string()).optional().describe('Initial next actions to take'),
|
|
@@ -27,7 +24,6 @@ export function registerGoalTools(server) {
|
|
|
27
24
|
linkedCronJobs: z.array(z.string()).optional().describe('Cron job names that contribute to this goal'),
|
|
28
25
|
autoSchedule: z.boolean().optional().describe('Allow the daily planner to auto-create/adjust cron jobs for this goal (default: false)'),
|
|
29
26
|
}, async ({ title, description, owner, priority, targetDate, nextActions, reviewFrequency, linkedCronJobs, autoSchedule }) => {
|
|
30
|
-
ensureGoalsDir();
|
|
31
27
|
const id = randomBytes(4).toString('hex');
|
|
32
28
|
const now = new Date().toISOString();
|
|
33
29
|
const goal = {
|
|
@@ -43,9 +39,9 @@ export function registerGoalTools(server) {
|
|
|
43
39
|
linkedCronJobs: linkedCronJobs || [],
|
|
44
40
|
...(autoSchedule ? { autoSchedule } : {}),
|
|
45
41
|
};
|
|
46
|
-
|
|
47
|
-
logger.info({ goalId: id, title }, 'Goal created');
|
|
48
|
-
return textResult(`Goal created: "${title}" (ID: ${id})`);
|
|
42
|
+
const filePath = writeGoalForOwner(goal);
|
|
43
|
+
logger.info({ goalId: id, title, owner: goal.owner, filePath }, 'Goal created');
|
|
44
|
+
return textResult(`Goal created: "${title}" (ID: ${id}) — owner: ${goal.owner}`);
|
|
49
45
|
});
|
|
50
46
|
server.tool('goal_update', 'Update an existing persistent goal — add progress notes, change status, update next actions, or add blockers.', {
|
|
51
47
|
id: z.string().describe('Goal ID'),
|
|
@@ -57,14 +53,16 @@ export function registerGoalTools(server) {
|
|
|
57
53
|
priority: z.enum(['high', 'medium', 'low']).optional().describe('Change priority'),
|
|
58
54
|
autoSchedule: z.boolean().optional().describe('Allow the daily planner to auto-create/adjust cron jobs for this goal'),
|
|
59
55
|
}, async ({ id, status, progressNote, nextActions, blockers, linkedCronJobs, priority, autoSchedule }) => {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
56
|
+
const found = findGoalPath(id);
|
|
57
|
+
if (!found)
|
|
58
|
+
return textResult(`Goal not found: ${id}`);
|
|
59
|
+
const goal = readGoalById(id);
|
|
60
|
+
if (!goal)
|
|
62
61
|
return textResult(`Goal not found: ${id}`);
|
|
63
|
-
const goal = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
64
62
|
if (status)
|
|
65
63
|
goal.status = status;
|
|
66
64
|
if (progressNote)
|
|
67
|
-
goal.progressNotes.push(`[${new Date().toISOString().slice(0, 16)}] ${progressNote}`);
|
|
65
|
+
(goal.progressNotes ||= []).push(`[${new Date().toISOString().slice(0, 16)}] ${progressNote}`);
|
|
68
66
|
if (nextActions)
|
|
69
67
|
goal.nextActions = nextActions;
|
|
70
68
|
if (blockers)
|
|
@@ -76,42 +74,32 @@ export function registerGoalTools(server) {
|
|
|
76
74
|
if (autoSchedule !== undefined)
|
|
77
75
|
goal.autoSchedule = autoSchedule;
|
|
78
76
|
goal.updatedAt = new Date().toISOString();
|
|
79
|
-
writeFileSync(filePath, JSON.stringify(goal, null, 2));
|
|
80
|
-
logger.info({ goalId: id, status: goal.status }, 'Goal updated');
|
|
77
|
+
writeFileSync(found.filePath, JSON.stringify(goal, null, 2));
|
|
78
|
+
logger.info({ goalId: id, status: goal.status, owner: found.owner }, 'Goal updated');
|
|
81
79
|
return textResult(`Goal "${goal.title}" updated (status: ${goal.status})`);
|
|
82
80
|
});
|
|
83
|
-
server.tool('goal_list', 'List persistent goals, optionally filtered by owner or status.', {
|
|
81
|
+
server.tool('goal_list', 'List persistent goals, optionally filtered by owner or status. Walks Clementine\'s global goals dir plus every agent\'s goals dir.', {
|
|
84
82
|
owner: z.string().optional().describe('Filter by owner agent slug'),
|
|
85
83
|
status: z.enum(['active', 'paused', 'completed', 'blocked']).optional().describe('Filter by status'),
|
|
86
84
|
}, async ({ owner, status }) => {
|
|
87
|
-
|
|
88
|
-
const files = readdirSync(GOALS_DIR).filter(f => f.endsWith('.json'));
|
|
89
|
-
let goals = files.map(f => {
|
|
90
|
-
try {
|
|
91
|
-
return JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}).filter(Boolean);
|
|
85
|
+
let entries = listAllGoals();
|
|
97
86
|
if (owner)
|
|
98
|
-
|
|
87
|
+
entries = entries.filter(e => e.owner === owner);
|
|
99
88
|
if (status)
|
|
100
|
-
|
|
101
|
-
if (
|
|
89
|
+
entries = entries.filter(e => e.goal.status === status);
|
|
90
|
+
if (entries.length === 0)
|
|
102
91
|
return textResult('No goals found matching the criteria.');
|
|
103
|
-
const lines =
|
|
104
|
-
const nextAct =
|
|
105
|
-
const linked =
|
|
106
|
-
return `- [${
|
|
92
|
+
const lines = entries.map(({ goal, owner: goalOwner }) => {
|
|
93
|
+
const nextAct = goal.nextActions?.length ? ` | Next: ${goal.nextActions[0]}` : '';
|
|
94
|
+
const linked = goal.linkedCronJobs?.length ? ` | Crons: ${goal.linkedCronJobs.join(', ')}` : '';
|
|
95
|
+
return `- [${String(goal.status).toUpperCase()}] **${goal.title}** (${goal.id}) — ${goal.priority} priority, owner: ${goalOwner}${nextAct}${linked}`;
|
|
107
96
|
});
|
|
108
|
-
return textResult(`Goals (${
|
|
97
|
+
return textResult(`Goals (${entries.length}):\n${lines.join('\n')}`);
|
|
109
98
|
});
|
|
110
99
|
server.tool('goal_get', 'Get a single persistent goal with full history — progress notes, next actions, blockers, and linked cron jobs.', { id: z.string().describe('Goal ID') }, async ({ id }) => {
|
|
111
|
-
const
|
|
112
|
-
if (!
|
|
100
|
+
const goal = readGoalById(id);
|
|
101
|
+
if (!goal)
|
|
113
102
|
return textResult(`Goal not found: ${id}`);
|
|
114
|
-
const goal = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
115
103
|
const sections = [
|
|
116
104
|
`# ${goal.title}`,
|
|
117
105
|
`**ID:** ${goal.id} | **Status:** ${goal.status} | **Priority:** ${goal.priority} | **Owner:** ${goal.owner}`,
|
|
@@ -119,29 +107,28 @@ export function registerGoalTools(server) {
|
|
|
119
107
|
`**Review:** ${goal.reviewFrequency}`,
|
|
120
108
|
`\n## Description\n${goal.description}`,
|
|
121
109
|
];
|
|
122
|
-
if (goal.progressNotes?.length
|
|
123
|
-
sections.push(`\n## Progress Notes\n${goal.progressNotes.map(
|
|
124
|
-
if (goal.nextActions?.length
|
|
125
|
-
sections.push(`\n## Next Actions\n${goal.nextActions.map(
|
|
126
|
-
if (goal.blockers?.length
|
|
127
|
-
sections.push(`\n## Blockers\n${goal.blockers.map(
|
|
128
|
-
if (goal.linkedCronJobs?.length
|
|
129
|
-
sections.push(`\n## Linked Cron Jobs\n${goal.linkedCronJobs.map(
|
|
110
|
+
if (goal.progressNotes?.length)
|
|
111
|
+
sections.push(`\n## Progress Notes\n${goal.progressNotes.map(n => `- ${n}`).join('\n')}`);
|
|
112
|
+
if (goal.nextActions?.length)
|
|
113
|
+
sections.push(`\n## Next Actions\n${goal.nextActions.map(a => `- [ ] ${a}`).join('\n')}`);
|
|
114
|
+
if (goal.blockers?.length)
|
|
115
|
+
sections.push(`\n## Blockers\n${goal.blockers.map(b => `- ${b}`).join('\n')}`);
|
|
116
|
+
if (goal.linkedCronJobs?.length)
|
|
117
|
+
sections.push(`\n## Linked Cron Jobs\n${goal.linkedCronJobs.map(c => `- ${c}`).join('\n')}`);
|
|
130
118
|
return textResult(sections.join('\n'));
|
|
131
119
|
});
|
|
132
|
-
server.tool('goal_work', 'Spawn a focused background work session on a specific goal. The daemon picks up the trigger and runs a goal-directed session
|
|
120
|
+
server.tool('goal_work', 'Spawn a focused background work session on a specific goal. The daemon picks up the trigger and runs a goal-directed session as the goal\'s owner (so output lands in that agent\'s channel with that agent\'s tools). Results delivered via notifications.', {
|
|
133
121
|
goal_id: z.string().describe('ID of the goal to work on'),
|
|
134
122
|
focus: z.string().optional().describe('Specific aspect to focus on. Defaults to the goal\'s first nextAction.'),
|
|
135
123
|
max_turns: z.number().optional().default(15).describe('Max agent turns for this work session'),
|
|
136
124
|
}, async ({ goal_id, focus, max_turns }) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!existsSync(goalPath))
|
|
125
|
+
const goal = readGoalById(goal_id);
|
|
126
|
+
if (!goal)
|
|
140
127
|
return textResult(`Goal not found: ${goal_id}. Use goal_list to see available goals.`);
|
|
141
|
-
const goal = JSON.parse(readFileSync(goalPath, 'utf-8'));
|
|
142
128
|
if (goal.status !== 'active')
|
|
143
129
|
return textResult(`Goal "${goal.title}" is ${goal.status} — only active goals can be worked on.`);
|
|
144
|
-
|
|
130
|
+
if (!existsSync(GOAL_TRIGGER_DIR))
|
|
131
|
+
mkdirSync(GOAL_TRIGGER_DIR, { recursive: true });
|
|
145
132
|
const trigger = {
|
|
146
133
|
goalId: goal_id,
|
|
147
134
|
focus: focus || goal.nextActions?.[0] || goal.description,
|
|
@@ -150,8 +137,9 @@ export function registerGoalTools(server) {
|
|
|
150
137
|
};
|
|
151
138
|
const triggerFile = path.join(GOAL_TRIGGER_DIR, `${Date.now()}-${goal_id}.trigger.json`);
|
|
152
139
|
writeFileSync(triggerFile, JSON.stringify(trigger, null, 2));
|
|
153
|
-
logger.info({ goalId: goal_id, focus: trigger.focus }, 'Goal work session triggered');
|
|
140
|
+
logger.info({ goalId: goal_id, owner: goal.owner, focus: trigger.focus }, 'Goal work session triggered');
|
|
154
141
|
return textResult(`Triggered goal work session for "${goal.title}" (${goal_id}).\n` +
|
|
142
|
+
`Owner: ${goal.owner}\n` +
|
|
155
143
|
`Focus: ${trigger.focus}\n` +
|
|
156
144
|
`The daemon will pick it up within a few seconds. Results delivered via notifications.`);
|
|
157
145
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { BASE_DIR, HANDOFFS_DIR, INBOX_DIR, TASKS_FILE, logger, textResult, todayStr, } from './shared.js';
|
|
7
|
+
import { BASE_DIR, HANDOFFS_DIR, INBOX_DIR, TASKS_FILE, logger, textResult, todayStr, listAllGoals, } from './shared.js';
|
|
8
8
|
function ensureHandoffsDir() {
|
|
9
9
|
if (!existsSync(HANDOFFS_DIR))
|
|
10
10
|
mkdirSync(HANDOFFS_DIR, { recursive: true });
|
|
@@ -66,26 +66,19 @@ export function registerSessionTools(server) {
|
|
|
66
66
|
}, async ({ agent_slug, limit }) => {
|
|
67
67
|
const maxItems = Math.min(limit ?? 10, 30);
|
|
68
68
|
const items = [];
|
|
69
|
-
// 1. Stale goals
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const urgency = Math.min(5, Math.floor(daysSinceUpdate / staleThreshold) + (goal.priority === 'high' ? 2 : goal.priority === 'medium' ? 1 : 0));
|
|
83
|
-
items.push({ type: 'stale-goal', urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
69
|
+
// 1. Stale goals (walks global + per-agent dirs)
|
|
70
|
+
for (const { goal, owner } of listAllGoals()) {
|
|
71
|
+
if (goal.status !== 'active')
|
|
72
|
+
continue;
|
|
73
|
+
if (agent_slug && owner !== agent_slug)
|
|
74
|
+
continue;
|
|
75
|
+
if (!goal.updatedAt)
|
|
76
|
+
continue;
|
|
77
|
+
const daysSinceUpdate = Math.floor((Date.now() - new Date(goal.updatedAt).getTime()) / 86400000);
|
|
78
|
+
const staleThreshold = goal.reviewFrequency === 'daily' ? 1 : goal.reviewFrequency === 'weekly' ? 7 : 30;
|
|
79
|
+
if (daysSinceUpdate > staleThreshold) {
|
|
80
|
+
const urgency = Math.min(5, Math.floor(daysSinceUpdate / staleThreshold) + (goal.priority === 'high' ? 2 : goal.priority === 'medium' ? 1 : 0));
|
|
81
|
+
items.push({ type: 'stale-goal', urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
|
|
89
82
|
}
|
|
90
83
|
}
|
|
91
84
|
// 2. Failing cron jobs
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -169,6 +169,46 @@ export declare function agentTasksFile(slug: string | null): string;
|
|
|
169
169
|
export declare function agentWorkingMemoryFile(slug: string | null): string;
|
|
170
170
|
export declare function agentGoalsDir(slug: string | null): string;
|
|
171
171
|
export declare function agentDailyNotesDir(slug: string | null): string;
|
|
172
|
+
export type GoalRecord = {
|
|
173
|
+
id: string;
|
|
174
|
+
title: string;
|
|
175
|
+
description?: string;
|
|
176
|
+
owner: string;
|
|
177
|
+
status?: string;
|
|
178
|
+
priority?: string;
|
|
179
|
+
createdAt?: string;
|
|
180
|
+
updatedAt?: string;
|
|
181
|
+
progressNotes?: string[];
|
|
182
|
+
nextActions?: string[];
|
|
183
|
+
blockers?: string[];
|
|
184
|
+
reviewFrequency?: string;
|
|
185
|
+
linkedCronJobs?: string[];
|
|
186
|
+
[key: string]: unknown;
|
|
187
|
+
};
|
|
188
|
+
/** Return the directory where a goal owned by `owner` should live. */
|
|
189
|
+
export declare function goalDirForOwner(owner: string): string;
|
|
190
|
+
/**
|
|
191
|
+
* Walk Clementine's global goals dir AND every per-agent goals dir.
|
|
192
|
+
* Returns {goal, path, owner} for each goal found. Owner is derived from
|
|
193
|
+
* the goal's `owner` field if set, else inferred from which directory it was in.
|
|
194
|
+
*/
|
|
195
|
+
export declare function listAllGoals(): Array<{
|
|
196
|
+
goal: GoalRecord;
|
|
197
|
+
filePath: string;
|
|
198
|
+
owner: string;
|
|
199
|
+
}>;
|
|
200
|
+
/** Find a goal's file path by id across global + all agent dirs. */
|
|
201
|
+
export declare function findGoalPath(id: string): {
|
|
202
|
+
filePath: string;
|
|
203
|
+
owner: string;
|
|
204
|
+
} | null;
|
|
205
|
+
/** Read a goal by id; returns null if not found. */
|
|
206
|
+
export declare function readGoalById(id: string): GoalRecord | null;
|
|
207
|
+
/**
|
|
208
|
+
* Write a goal to the correct directory based on its owner field.
|
|
209
|
+
* Creates the target directory if needed. Returns the path written.
|
|
210
|
+
*/
|
|
211
|
+
export declare function writeGoalForOwner(goal: GoalRecord): string;
|
|
172
212
|
export declare function todayStr(): string;
|
|
173
213
|
export declare function yesterdayStr(): string;
|
|
174
214
|
export declare function nowTime(): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -96,6 +96,90 @@ export function agentDailyNotesDir(slug) {
|
|
|
96
96
|
return DAILY_NOTES_DIR;
|
|
97
97
|
return path.join(AGENTS_DIR, slug, 'daily-notes');
|
|
98
98
|
}
|
|
99
|
+
/** Return the directory where a goal owned by `owner` should live. */
|
|
100
|
+
export function goalDirForOwner(owner) {
|
|
101
|
+
if (!owner || owner === 'clementine')
|
|
102
|
+
return GOALS_DIR;
|
|
103
|
+
return path.join(AGENTS_DIR, owner, 'goals');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Walk Clementine's global goals dir AND every per-agent goals dir.
|
|
107
|
+
* Returns {goal, path, owner} for each goal found. Owner is derived from
|
|
108
|
+
* the goal's `owner` field if set, else inferred from which directory it was in.
|
|
109
|
+
*/
|
|
110
|
+
export function listAllGoals() {
|
|
111
|
+
const results = [];
|
|
112
|
+
const readDir = (dir, inferredOwner) => {
|
|
113
|
+
if (!existsSync(dir))
|
|
114
|
+
return;
|
|
115
|
+
let files;
|
|
116
|
+
try {
|
|
117
|
+
files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const f of files) {
|
|
123
|
+
const fp = path.join(dir, f);
|
|
124
|
+
try {
|
|
125
|
+
const goal = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
126
|
+
if (!goal.id)
|
|
127
|
+
continue;
|
|
128
|
+
const owner = (typeof goal.owner === 'string' && goal.owner) || inferredOwner;
|
|
129
|
+
results.push({ goal, filePath: fp, owner });
|
|
130
|
+
}
|
|
131
|
+
catch { /* skip malformed */ }
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
readDir(GOALS_DIR, 'clementine');
|
|
135
|
+
if (existsSync(AGENTS_DIR)) {
|
|
136
|
+
let agentDirs = [];
|
|
137
|
+
try {
|
|
138
|
+
agentDirs = readdirSync(AGENTS_DIR, { withFileTypes: true })
|
|
139
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('_'))
|
|
140
|
+
.map(d => d.name);
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
for (const slug of agentDirs) {
|
|
144
|
+
readDir(path.join(AGENTS_DIR, slug, 'goals'), slug);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
/** Find a goal's file path by id across global + all agent dirs. */
|
|
150
|
+
export function findGoalPath(id) {
|
|
151
|
+
for (const entry of listAllGoals()) {
|
|
152
|
+
if (entry.goal.id === id) {
|
|
153
|
+
return { filePath: entry.filePath, owner: entry.owner };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/** Read a goal by id; returns null if not found. */
|
|
159
|
+
export function readGoalById(id) {
|
|
160
|
+
const found = findGoalPath(id);
|
|
161
|
+
if (!found)
|
|
162
|
+
return null;
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(readFileSync(found.filePath, 'utf-8'));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Write a goal to the correct directory based on its owner field.
|
|
172
|
+
* Creates the target directory if needed. Returns the path written.
|
|
173
|
+
*/
|
|
174
|
+
export function writeGoalForOwner(goal) {
|
|
175
|
+
const owner = goal.owner || 'clementine';
|
|
176
|
+
const dir = goalDirForOwner(owner);
|
|
177
|
+
if (!existsSync(dir))
|
|
178
|
+
mkdirSync(dir, { recursive: true });
|
|
179
|
+
const fp = path.join(dir, `${goal.id}.json`);
|
|
180
|
+
writeFileSync(fp, JSON.stringify(goal, null, 2));
|
|
181
|
+
return fp;
|
|
182
|
+
}
|
|
99
183
|
// ── Date/Time helpers ───────────────────────────────���──────────────────
|
|
100
184
|
export function todayStr() {
|
|
101
185
|
const d = new Date();
|