flint-agent 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +108 -0
- package/CHANGELOG.md +55 -0
- package/FEATURES.md +298 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/bin/flint.js +47 -0
- package/config/classifier-prompt.md +218 -0
- package/config/models-curated.json +4 -0
- package/config/providers.json +74 -0
- package/package.json +92 -0
- package/patches/ink+6.8.0.patch +78 -0
- package/profiles/desktop.md +65 -0
- package/profiles/generic.md +20 -0
- package/profiles/marketer.md +20 -0
- package/profiles/profiles.json +34 -0
- package/profiles/ux-reviewer.md +25 -0
- package/src/agent/agent.js +1743 -0
- package/src/agent/auto.js +346 -0
- package/src/agent/backoff.js +143 -0
- package/src/agent/compression.js +310 -0
- package/src/agent/content-resolver.js +180 -0
- package/src/agent/flow-controller.js +309 -0
- package/src/agent/intent-manifest.js +231 -0
- package/src/agent/intent-timeout.js +46 -0
- package/src/agent/intent.js +633 -0
- package/src/agent/knowledge.js +114 -0
- package/src/agent/learning.js +180 -0
- package/src/agent/modes.js +187 -0
- package/src/agent/outcome-ask.js +91 -0
- package/src/agent/project-context.js +76 -0
- package/src/agent/prompt-budget.js +117 -0
- package/src/agent/reflection-extractor.js +140 -0
- package/src/agent/steering.js +86 -0
- package/src/agent/supervisor.js +430 -0
- package/src/agent/swap.js +443 -0
- package/src/agent/system-prompt.js +446 -0
- package/src/agent/time-stamp.js +48 -0
- package/src/agent/tool-guard.js +201 -0
- package/src/agent/toolcall-text.js +162 -0
- package/src/agent/usage.js +297 -0
- package/src/agent/vision.js +94 -0
- package/src/agent/watchdog.js +139 -0
- package/src/agent/workspace-changes.js +177 -0
- package/src/api/address.js +14 -0
- package/src/api/client.js +280 -0
- package/src/api/server.js +535 -0
- package/src/api/stream-pipe.js +113 -0
- package/src/app-state.js +39 -0
- package/src/bootstrap.js +501 -0
- package/src/bus/drain-loop.js +497 -0
- package/src/bus/index.js +270 -0
- package/src/bus/plugins.js +65 -0
- package/src/child-idle.js +14 -0
- package/src/cli.js +118 -0
- package/src/commands/commands.js +1297 -0
- package/src/commands/registry.js +132 -0
- package/src/components/App.js +491 -0
- package/src/components/CarefulMenu.js +145 -0
- package/src/components/HistoryWriter.js +86 -0
- package/src/components/LineInput.js +69 -0
- package/src/components/LiveZone.js +294 -0
- package/src/components/OverlayMenu.js +179 -0
- package/src/components/SystemPanel.js +156 -0
- package/src/components/Table.js +54 -0
- package/src/config.js +249 -0
- package/src/free-models.js +230 -0
- package/src/index.js +1111 -0
- package/src/input-handler.js +13 -0
- package/src/input-text.js +123 -0
- package/src/launcher.js +129 -0
- package/src/logging/api-log.js +95 -0
- package/src/logging/chat-log-follower.js +113 -0
- package/src/logging/chat-log.js +15 -0
- package/src/logging/log-collector.js +182 -0
- package/src/logging/logger.js +112 -0
- package/src/logging/tool-log.js +20 -0
- package/src/mcp-client.js +314 -0
- package/src/memory/conversation-digest.js +113 -0
- package/src/memory/extract-facts.js +98 -0
- package/src/memory/facts.js +181 -0
- package/src/memory/inbox.js +63 -0
- package/src/memory/markdown.js +38 -0
- package/src/memory/patterns.js +185 -0
- package/src/memory/project.js +66 -0
- package/src/memory/reflections.js +74 -0
- package/src/memory/retrieval.js +84 -0
- package/src/memory/rules.js +105 -0
- package/src/memory/session-facts.js +125 -0
- package/src/memory/skills.js +191 -0
- package/src/memory/sqlite-store.js +653 -0
- package/src/memory/store.js +208 -0
- package/src/memory/tools.js +196 -0
- package/src/memory/user-model.js +86 -0
- package/src/message-handler.js +775 -0
- package/src/model-check.js +218 -0
- package/src/plugins/loader.js +120 -0
- package/src/plugins/manager.js +88 -0
- package/src/production-env.js +22 -0
- package/src/profiles.js +42 -0
- package/src/providers/adapters/anthropic.js +270 -0
- package/src/providers/adapters/openai.js +120 -0
- package/src/providers/keys-dpapi.js +41 -0
- package/src/providers/keys-fallback.js +31 -0
- package/src/providers/keys.js +132 -0
- package/src/providers/models.js +154 -0
- package/src/providers/registry.js +56 -0
- package/src/providers/state.js +56 -0
- package/src/registry.js +96 -0
- package/src/restart.js +29 -0
- package/src/sandbox/backend.js +130 -0
- package/src/security/api-auth.js +132 -0
- package/src/security/audit.js +98 -0
- package/src/security/child-policy.js +41 -0
- package/src/security/command-guard.js +173 -0
- package/src/security/content-fence.js +250 -0
- package/src/security/content-validator.js +132 -0
- package/src/security/index.js +143 -0
- package/src/security/network-guard.js +126 -0
- package/src/security/pairing.js +180 -0
- package/src/security/path-guard.js +140 -0
- package/src/security/persona-guard.js +67 -0
- package/src/security/policies.js +452 -0
- package/src/security/safety-constants.js +34 -0
- package/src/security/watchdog.js +107 -0
- package/src/sessions.js +130 -0
- package/src/spend.js +97 -0
- package/src/startup-watchdog.js +59 -0
- package/src/stdio/args.js +71 -0
- package/src/stdio/guard.js +59 -0
- package/src/stdio/protocol.js +167 -0
- package/src/stdio/run.js +106 -0
- package/src/stdio/session.js +180 -0
- package/src/store/agent-slice.js +306 -0
- package/src/store/dataset-slice.js +73 -0
- package/src/store/index.js +22 -0
- package/src/store/process-slice.js +135 -0
- package/src/store/session-slice.js +191 -0
- package/src/store/ui-slice.js +119 -0
- package/src/tasks/db.js +184 -0
- package/src/tasks/queries.js +589 -0
- package/src/tools/agent-tools.js +473 -0
- package/src/tools/checkpoint.js +152 -0
- package/src/tools/command-approvals.js +180 -0
- package/src/tools/dataset.js +50 -0
- package/src/tools/filesystem.js +682 -0
- package/src/tools/inbox-tools.js +48 -0
- package/src/tools/mesh.js +135 -0
- package/src/tools/own-env.js +136 -0
- package/src/tools/permissions.js +681 -0
- package/src/tools/plugin-tools.js +123 -0
- package/src/tools/process-tools.js +595 -0
- package/src/tools/registry.js +307 -0
- package/src/tools/swap-tools.js +72 -0
- package/src/tools/system.js +662 -0
- package/src/tools/tasks.js +532 -0
- package/src/tools/tool-search.js +171 -0
- package/src/ui/header.js +140 -0
- package/src/ui/input-cursor.js +23 -0
- package/src/ui/last-line.js +25 -0
- package/src/ui/line-edit.js +135 -0
- package/src/ui/output.js +399 -0
- package/src/ui/paste-tokens.js +131 -0
- package/src/ui/prompt-attention.js +134 -0
- package/src/ui/render-options.js +13 -0
- package/src/ui/replay.js +94 -0
- package/src/ui/splash.js +49 -0
- package/src/ui/status-level.js +36 -0
- package/src/ui/tool-ledger.js +203 -0
- package/src/ui/window-title.js +150 -0
- package/src/update.js +205 -0
- package/system.md +63 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
// Task planning tools for Flint — backed by SQLite
|
|
2
|
+
// AI creates plans for complex tasks and tracks progress persistently
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createGoal, getGoal, getActiveGoal, getActiveGoals, completeGoal, abandonGoal, listGoals,
|
|
6
|
+
createTask, createSubtask, getSubtasks, getTasksByGoal, updateTaskStatus, getTaskStats,
|
|
7
|
+
linkFile, addNote, getTaskFiles, getTaskNotes, getTask,
|
|
8
|
+
goalToPlan, syncPlanToStore,
|
|
9
|
+
createReminder, listReminders,
|
|
10
|
+
touchSession, setFocusedGoal, getFocusedGoal,
|
|
11
|
+
getAllActivePlans, getProjectStats,
|
|
12
|
+
setDailyFocus, clearDailyFocus, getTodayTasks,
|
|
13
|
+
} from "../tasks/queries.js";
|
|
14
|
+
|
|
15
|
+
function parseDelay(delay) {
|
|
16
|
+
const match = delay.match(/^(\d+)\s*(s|m|h|d|w)$/i);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
const value = parseInt(match[1], 10);
|
|
19
|
+
const unit = match[2].toLowerCase();
|
|
20
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000, w: 604800000 };
|
|
21
|
+
return value * multipliers[unit];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createTaskTools({ getStore }) {
|
|
25
|
+
function sync() {
|
|
26
|
+
return syncPlanToStore(getStore());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tools = [
|
|
30
|
+
{
|
|
31
|
+
type: "function",
|
|
32
|
+
function: {
|
|
33
|
+
name: "create_plan",
|
|
34
|
+
description:
|
|
35
|
+
"Create a persistent plan for a complex multi-step task. " +
|
|
36
|
+
"Stored in SQLite — survives restarts. " +
|
|
37
|
+
"Multiple plans can coexist. New plan becomes the focused goal.",
|
|
38
|
+
parameters: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
goal: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "The overall goal of the plan",
|
|
44
|
+
},
|
|
45
|
+
project: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Project tag (e.g. 'flint', 'website'). Default: 'default'",
|
|
48
|
+
},
|
|
49
|
+
tasks: {
|
|
50
|
+
type: "array",
|
|
51
|
+
items: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
title: { type: "string", description: "Short task title" },
|
|
55
|
+
description: { type: "string", description: "Optional details" },
|
|
56
|
+
priority: { type: "number", description: "Priority (higher = first). Default 0" },
|
|
57
|
+
},
|
|
58
|
+
required: ["title"],
|
|
59
|
+
},
|
|
60
|
+
description: "List of tasks to complete",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ["goal", "tasks"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: "function",
|
|
69
|
+
function: {
|
|
70
|
+
name: "update_task",
|
|
71
|
+
description:
|
|
72
|
+
"Update a task by its global ID (shown in [#ID] in plan output). " +
|
|
73
|
+
"For the focused goal, relative index (1, 2, 3...) also works.",
|
|
74
|
+
parameters: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
id: {
|
|
78
|
+
type: "number",
|
|
79
|
+
description: "Task ID (global #ID or relative 1..N within focused goal)",
|
|
80
|
+
},
|
|
81
|
+
status: {
|
|
82
|
+
type: "string",
|
|
83
|
+
enum: ["done", "in_progress", "skipped"],
|
|
84
|
+
description: "New status for the task",
|
|
85
|
+
},
|
|
86
|
+
result: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Optional note about the result",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ["id", "status"],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "function",
|
|
97
|
+
function: {
|
|
98
|
+
name: "list_tasks",
|
|
99
|
+
description:
|
|
100
|
+
"Show the current plan with task statuses from SQLite. " +
|
|
101
|
+
"Use this to review progress on the current plan.",
|
|
102
|
+
parameters: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "function",
|
|
110
|
+
function: {
|
|
111
|
+
name: "add_task",
|
|
112
|
+
description:
|
|
113
|
+
"Add a standalone task (with optional schedule). " +
|
|
114
|
+
"Use for reminders, recurring jobs, or tasks outside a plan. " +
|
|
115
|
+
"Examples: 'check email every 5m', 'remind me at 15:00', 'buy groceries'.",
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
title: { type: "string", description: "Task title" },
|
|
120
|
+
description: { type: "string", description: "Optional details" },
|
|
121
|
+
next_run: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: 'When to fire: delay ("5m","1h","2d") or ISO datetime. Omit for non-scheduled tasks.',
|
|
124
|
+
},
|
|
125
|
+
repeat: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: 'Repeat interval: "5m","1h","1d","1w". Omit for one-time.',
|
|
128
|
+
},
|
|
129
|
+
project: { type: "string", description: "Project tag. Default: 'default'" },
|
|
130
|
+
scope: {
|
|
131
|
+
type: "string",
|
|
132
|
+
enum: ["session", "project", "global"],
|
|
133
|
+
description: "Scope: session (dies with session), project (survives sessions), global (always visible). Recurring tasks auto-set to global.",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
required: ["title"],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: "function",
|
|
142
|
+
function: {
|
|
143
|
+
name: "add_task_note",
|
|
144
|
+
description: "Add a note/log entry to a specific task.",
|
|
145
|
+
parameters: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
task_id: { type: "number", description: "Task ID" },
|
|
149
|
+
note: { type: "string", description: "Note text" },
|
|
150
|
+
},
|
|
151
|
+
required: ["task_id", "note"],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: "function",
|
|
157
|
+
function: {
|
|
158
|
+
name: "link_task_file",
|
|
159
|
+
description: "Link a file to a task (created, modified, or related).",
|
|
160
|
+
parameters: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
task_id: { type: "number", description: "Task ID" },
|
|
164
|
+
path: { type: "string", description: "File path" },
|
|
165
|
+
role: {
|
|
166
|
+
type: "string",
|
|
167
|
+
enum: ["created", "modified", "related"],
|
|
168
|
+
description: "How the file relates to the task. Default: related",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
required: ["task_id", "path"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: "function",
|
|
177
|
+
function: {
|
|
178
|
+
name: "create_subtask",
|
|
179
|
+
description:
|
|
180
|
+
"Add subtask(s) to an existing task. Parent auto-completes when all subtasks are done. Max 2 levels (task -> subtask).",
|
|
181
|
+
parameters: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
parent_id: { type: "number", description: "Parent task ID (global #ID)" },
|
|
185
|
+
subtasks: {
|
|
186
|
+
type: "array",
|
|
187
|
+
items: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
title: { type: "string", description: "Subtask title" },
|
|
191
|
+
description: { type: "string", description: "Optional details" },
|
|
192
|
+
},
|
|
193
|
+
required: ["title"],
|
|
194
|
+
},
|
|
195
|
+
description: "List of subtasks to add",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
required: ["parent_id", "subtasks"],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: "function",
|
|
204
|
+
function: {
|
|
205
|
+
name: "focus_goal",
|
|
206
|
+
description:
|
|
207
|
+
"Switch focus to a different active goal. The focused goal's tasks are shown in detail in the prompt.",
|
|
208
|
+
parameters: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
goal_id: { type: "number", description: "Goal ID to focus on" },
|
|
212
|
+
},
|
|
213
|
+
required: ["goal_id"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: "function",
|
|
219
|
+
function: {
|
|
220
|
+
name: "task_stats",
|
|
221
|
+
description:
|
|
222
|
+
"Get overview stats without loading all tasks. " +
|
|
223
|
+
"Shows per-project or per-goal counts (pending, in_progress, done). " +
|
|
224
|
+
"Use for dashboard view before drilling into details.",
|
|
225
|
+
parameters: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
project: { type: "string", description: "Filter by project. Omit for all projects." },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: "function",
|
|
235
|
+
function: {
|
|
236
|
+
name: "list_goals",
|
|
237
|
+
description:
|
|
238
|
+
"List all goals (active, completed, abandoned). " +
|
|
239
|
+
"Use to find goal IDs for focus_goal or to review history.",
|
|
240
|
+
parameters: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
status: {
|
|
244
|
+
type: "string",
|
|
245
|
+
enum: ["active", "completed", "abandoned"],
|
|
246
|
+
description: "Filter by status. Omit for all.",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: "function",
|
|
254
|
+
function: {
|
|
255
|
+
name: "today",
|
|
256
|
+
description:
|
|
257
|
+
"Show tasks planned for today, or set tasks for today's focus. " +
|
|
258
|
+
"Without task_ids: shows today's tasks across all goals. " +
|
|
259
|
+
"With task_ids: marks those tasks for today.",
|
|
260
|
+
parameters: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {
|
|
263
|
+
task_ids: {
|
|
264
|
+
type: "array",
|
|
265
|
+
items: { type: "number" },
|
|
266
|
+
description: "Task IDs to mark for today. Omit to just view today's tasks.",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
function resolveTaskId(id) {
|
|
275
|
+
const activeGoal = getActiveGoal();
|
|
276
|
+
if (activeGoal) {
|
|
277
|
+
const goalTasks = getTasksByGoal(activeGoal.id);
|
|
278
|
+
if (id >= 1 && id <= goalTasks.length) {
|
|
279
|
+
return goalTasks[id - 1].id;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return id;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const handlers = {
|
|
286
|
+
async create_plan({ goal, tasks, project }) {
|
|
287
|
+
if (!tasks || tasks.length === 0) {
|
|
288
|
+
return "Error: tasks array is empty. You must provide at least one task with a title. Example: tasks: [{title: 'Step 1'}, {title: 'Step 2'}]";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const sessionId = getStore().getState().sessionId;
|
|
292
|
+
touchSession(sessionId);
|
|
293
|
+
|
|
294
|
+
// Multi-goal: do NOT abandon previous goals
|
|
295
|
+
const g = createGoal(goal, project || "default", sessionId);
|
|
296
|
+
|
|
297
|
+
for (const t of tasks) {
|
|
298
|
+
const title = t.title || t.description || t.name || "Untitled task";
|
|
299
|
+
createTask(g.id, title, t.description || null, t.priority || 0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Auto-focus the new goal
|
|
303
|
+
setFocusedGoal(sessionId, g.id);
|
|
304
|
+
|
|
305
|
+
sync();
|
|
306
|
+
const createdTasks = getTasksByGoal(g.id);
|
|
307
|
+
const taskList = createdTasks.map((t, i) => ` ${i + 1}. [#${t.id}] ${t.title}`).join("\n");
|
|
308
|
+
return `Plan created: ${goal} [goal #${g.id}] (focused)\nTasks:\n${taskList}`;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
async update_task({ id, status, result }) {
|
|
312
|
+
const sessionId = getStore().getState().sessionId;
|
|
313
|
+
touchSession(sessionId);
|
|
314
|
+
|
|
315
|
+
// Resolve relative task index (1-based) within focused goal
|
|
316
|
+
const resolvedId = resolveTaskId(id);
|
|
317
|
+
const task = updateTaskStatus(resolvedId, status, result || null);
|
|
318
|
+
if (!task) return `Error: task ${id} not found (resolved to #${resolvedId})`;
|
|
319
|
+
|
|
320
|
+
// Check if all tasks in the goal are done/skipped
|
|
321
|
+
if (task.goal_id) {
|
|
322
|
+
const stats = getTaskStats(task.goal_id);
|
|
323
|
+
if (stats.pending === 0 && stats.in_progress === 0) {
|
|
324
|
+
completeGoal(task.goal_id);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
sync();
|
|
329
|
+
return `Task #${resolvedId}: ${task.title} -> ${status}${result ? ` (${result})` : ""}`;
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
async list_tasks() {
|
|
333
|
+
const sessionId = getStore().getState().sessionId;
|
|
334
|
+
const plan = sync(); // focused goal
|
|
335
|
+
const allPlans = getAllActivePlans();
|
|
336
|
+
const scheduled = listReminders();
|
|
337
|
+
const parts = [];
|
|
338
|
+
|
|
339
|
+
// Focused goal — full detail
|
|
340
|
+
if (plan) {
|
|
341
|
+
parts.push(formatPlanForPrompt(plan) + " [FOCUSED]");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Other active goals — one-line summaries
|
|
345
|
+
const focusedId = plan ? plan.goalId : null;
|
|
346
|
+
for (const p of allPlans) {
|
|
347
|
+
if (p.goalId === focusedId) continue;
|
|
348
|
+
const s = p.stats;
|
|
349
|
+
parts.push(`[goal #${p.goalId}] ${p.goal} (${p.project}) — ${s.done}/${s.total} done, ${s.pending} pending`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (scheduled.length) {
|
|
353
|
+
parts.push("SCHEDULED:");
|
|
354
|
+
for (const t of scheduled) {
|
|
355
|
+
const repeatTag = t.repeat ? ` [every ${t.repeat}]` : "";
|
|
356
|
+
parts.push(` [#${t.id}] ${t.title} — next: ${t.next_run}${repeatTag}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!parts.length) return "No active tasks. Use create_plan or add_task to create.";
|
|
360
|
+
return parts.join("\n");
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
async add_task({ title, description, next_run, repeat, project, scope }) {
|
|
364
|
+
let nextRunISO = null;
|
|
365
|
+
if (next_run) {
|
|
366
|
+
const ms = parseDelay(next_run);
|
|
367
|
+
if (ms) {
|
|
368
|
+
nextRunISO = new Date(Date.now() + ms).toISOString();
|
|
369
|
+
} else {
|
|
370
|
+
const parsed = new Date(next_run);
|
|
371
|
+
if (!isNaN(parsed.getTime())) {
|
|
372
|
+
nextRunISO = parsed.toISOString();
|
|
373
|
+
} else {
|
|
374
|
+
return `Error: invalid next_run "${next_run}". Use delay ("5m","1h") or ISO datetime.`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (repeat) {
|
|
379
|
+
const ms = parseDelay(repeat);
|
|
380
|
+
if (!ms) return `Error: invalid repeat "${repeat}". Use "5m","1h","1d","1w".`;
|
|
381
|
+
if (!nextRunISO) {
|
|
382
|
+
// If repeat but no next_run, fire first time after one interval
|
|
383
|
+
nextRunISO = new Date(Date.now() + ms).toISOString();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Create under active goal or standalone (goal_id = null)
|
|
388
|
+
let goalId = null;
|
|
389
|
+
const active = getActiveGoal(project || "default");
|
|
390
|
+
if (active) goalId = active.id;
|
|
391
|
+
|
|
392
|
+
// Determine scope: recurring = global, one-time with repeat = project, else session
|
|
393
|
+
const taskScope = scope || (repeat ? "global" : "session");
|
|
394
|
+
const task = createReminder(title, nextRunISO, repeat || null, goalId, taskScope);
|
|
395
|
+
|
|
396
|
+
const parts = [`Task #${task.id}: "${title}" [${taskScope}]`];
|
|
397
|
+
if (nextRunISO) parts.push(`next run: ${nextRunISO}`);
|
|
398
|
+
if (repeat) parts.push(`repeats: every ${repeat}`);
|
|
399
|
+
return parts.join(" | ");
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
async add_task_note({ task_id, note }) {
|
|
403
|
+
const resolved = resolveTaskId(task_id);
|
|
404
|
+
addNote(resolved, note);
|
|
405
|
+
return `Note added to task ${task_id}`;
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
async link_task_file({ task_id, path, role }) {
|
|
409
|
+
const resolved = resolveTaskId(task_id);
|
|
410
|
+
linkFile(resolved, path, role || "related");
|
|
411
|
+
return `File linked to task ${task_id}: ${path} (${role || "related"})`;
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
async create_subtask({ parent_id, subtasks }) {
|
|
415
|
+
if (!subtasks || subtasks.length === 0) {
|
|
416
|
+
return "Error: subtasks array is empty.";
|
|
417
|
+
}
|
|
418
|
+
const parent = getTask(parent_id);
|
|
419
|
+
if (!parent) return `Error: task #${parent_id} not found`;
|
|
420
|
+
// Prevent 3+ levels
|
|
421
|
+
if (parent.parent_task_id) {
|
|
422
|
+
return `Error: task #${parent_id} is already a subtask. Max 2 levels allowed.`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const created = [];
|
|
426
|
+
for (const st of subtasks) {
|
|
427
|
+
const sub = createSubtask(parent_id, st.title, st.description || null);
|
|
428
|
+
if (sub) created.push(sub);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
sync();
|
|
432
|
+
const lines = created.map(s => ` [#${s.id}] ${s.title}`).join("\n");
|
|
433
|
+
return `Added ${created.length} subtask(s) to task #${parent_id} "${parent.title}":\n${lines}`;
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
async focus_goal({ goal_id }) {
|
|
437
|
+
const sessionId = getStore().getState().sessionId;
|
|
438
|
+
const goal = getGoal(goal_id);
|
|
439
|
+
if (!goal) return `Error: goal #${goal_id} not found`;
|
|
440
|
+
if (goal.status !== "active") return `Error: goal #${goal_id} is ${goal.status}, not active`;
|
|
441
|
+
setFocusedGoal(sessionId, goal_id);
|
|
442
|
+
sync();
|
|
443
|
+
const tasks = getTasksByGoal(goal_id);
|
|
444
|
+
const stats = getTaskStats(goal_id);
|
|
445
|
+
return `Focused on goal #${goal_id}: ${goal.title} (${goal.project})\n${stats.done}/${stats.total} done, ${stats.pending} pending, ${stats.in_progress} in progress`;
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
async task_stats({ project } = {}) {
|
|
449
|
+
if (project) {
|
|
450
|
+
const stats = getProjectStats(project);
|
|
451
|
+
return `Project "${project}": ${stats.goals} goals, ${stats.tasks} tasks (${stats.done} done, ${stats.pending} pending, ${stats.in_progress} in progress)`;
|
|
452
|
+
}
|
|
453
|
+
const rows = getProjectStats();
|
|
454
|
+
if (!rows.length) return "No active goals in any project.";
|
|
455
|
+
const lines = rows.map(r =>
|
|
456
|
+
` ${r.project}: ${r.goals} goals, ${r.tasks} tasks (${r.done} done, ${r.pending} pending)`
|
|
457
|
+
);
|
|
458
|
+
return `Active projects:\n${lines.join("\n")}`;
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
async today({ task_ids } = {}) {
|
|
462
|
+
const todayDate = new Date().toISOString().slice(0, 10);
|
|
463
|
+
if (task_ids && task_ids.length > 0) {
|
|
464
|
+
const invalid = task_ids.filter(tid => !getTask(tid));
|
|
465
|
+
if (invalid.length) return `Error: task(s) not found: ${invalid.join(", ")}`;
|
|
466
|
+
for (const tid of task_ids) {
|
|
467
|
+
setDailyFocus(tid, todayDate);
|
|
468
|
+
}
|
|
469
|
+
const tasks = getTodayTasks(todayDate);
|
|
470
|
+
const lines = tasks.map(t =>
|
|
471
|
+
` [#${t.id}] ${t.title} (${t.project || "default"}: ${t.goal_title || "standalone"})`
|
|
472
|
+
);
|
|
473
|
+
return `Marked ${task_ids.length} task(s) for today (${todayDate}).\nToday's tasks:\n${lines.join("\n")}`;
|
|
474
|
+
}
|
|
475
|
+
// View mode
|
|
476
|
+
const tasks = getTodayTasks(todayDate);
|
|
477
|
+
if (!tasks.length) return `No tasks planned for today (${todayDate}). Use today({task_ids: [...]}) to set tasks.`;
|
|
478
|
+
const lines = tasks.map(t => {
|
|
479
|
+
const icon = STATUS_ICONS[t.status] || "o";
|
|
480
|
+
return ` [#${t.id}] ${icon} ${t.title} (${t.project || "default"}: ${t.goal_title || "standalone"})`;
|
|
481
|
+
});
|
|
482
|
+
return `Today (${todayDate}):\n${lines.join("\n")}`;
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
async list_goals({ status } = {}) {
|
|
486
|
+
const goals = listGoals(status || null);
|
|
487
|
+
if (!goals.length) return status ? `No ${status} goals.` : "No goals.";
|
|
488
|
+
const sessionId = getStore().getState().sessionId;
|
|
489
|
+
const focused = getFocusedGoal(sessionId);
|
|
490
|
+
const focusedId = focused ? focused.id : null;
|
|
491
|
+
const lines = goals.map(g => {
|
|
492
|
+
const tag = g.id === focusedId ? " [FOCUSED]" : "";
|
|
493
|
+
const stats = g.status === "active" ? (() => {
|
|
494
|
+
const s = getTaskStats(g.id);
|
|
495
|
+
return ` — ${s.done}/${s.total} done`;
|
|
496
|
+
})() : "";
|
|
497
|
+
return ` [#${g.id}] ${g.title} (${g.project}) [${g.status}]${stats}${tag}`;
|
|
498
|
+
});
|
|
499
|
+
return `Goals:\n${lines.join("\n")}`;
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
return { tools, handlers };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const STATUS_ICONS = {
|
|
507
|
+
done: "+",
|
|
508
|
+
in_progress: ">",
|
|
509
|
+
pending: "o",
|
|
510
|
+
skipped: "-",
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
export function formatPlanForPrompt(plan) {
|
|
514
|
+
if (!plan) return "";
|
|
515
|
+
const lines = [`[PLAN #${plan.goalId || "?"}] Goal: ${plan.goal} (project: ${plan.project || "default"})`];
|
|
516
|
+
for (const t of plan.tasks) {
|
|
517
|
+
const icon = STATUS_ICONS[t.status] || "o";
|
|
518
|
+
let line = `[#${t.id}] ${icon} ${t.title}`;
|
|
519
|
+
if (t.result) line += ` - ${t.result}`;
|
|
520
|
+
lines.push(line);
|
|
521
|
+
// Subtasks indented
|
|
522
|
+
if (t.subtasks && t.subtasks.length) {
|
|
523
|
+
for (const s of t.subtasks) {
|
|
524
|
+
const sIcon = STATUS_ICONS[s.status] || "o";
|
|
525
|
+
let sLine = ` [#${s.id}] ${sIcon} ${s.title}`;
|
|
526
|
+
if (s.result) sLine += ` - ${s.result}`;
|
|
527
|
+
lines.push(sLine);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return lines.join("\n");
|
|
532
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// tool_search: a small core set every turn, everything else found by meaning.
|
|
2
|
+
//
|
|
3
|
+
// The classifier used to pick the tools for each turn, and on 2026-09-26 its
|
|
4
|
+
// picks cost three of Flint's five losses: no web on a repair task, every tool
|
|
5
|
+
// taken away from an explicit delete, write_file alone for "make a Word
|
|
6
|
+
// document", three operator tools for a browser task. A guess made before the
|
|
7
|
+
// model has read the request should not decide what the model can do.
|
|
8
|
+
//
|
|
9
|
+
// Here the model always has the core it needs for most work, and when it needs
|
|
10
|
+
// something else (a browser, a remote desktop, mail, the planner) it asks for
|
|
11
|
+
// it by describing the job. What it finds stays loaded for the rest of the
|
|
12
|
+
// session. Flint had a regex version of this,
|
|
13
|
+
// find_tools, until the classifier replaced it on 2026-04-04, unmeasured.
|
|
14
|
+
|
|
15
|
+
// Always in the payload. Files, a shell, processes and the web cover most of
|
|
16
|
+
// what a user asks for; the rest is one search away.
|
|
17
|
+
export const CORE_TOOLS = [
|
|
18
|
+
"read_file", "write_file", "edit_file", "list_directory", "search_in_files", "glob",
|
|
19
|
+
"create_directory", "delete_file", "move_file", "copy_file", "view_image",
|
|
20
|
+
"run_command", "run_background_command", "peek_process", "kill_process", "list_processes",
|
|
21
|
+
"web_search", "web_fetch", "think",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const TOOL_SEARCH_NAME = "tool_search";
|
|
25
|
+
|
|
26
|
+
export const toolSearchDef = {
|
|
27
|
+
type: "function",
|
|
28
|
+
function: {
|
|
29
|
+
name: TOOL_SEARCH_NAME,
|
|
30
|
+
description:
|
|
31
|
+
"Find more tools by describing what you need to do, e.g. 'open a page in a browser and click', " +
|
|
32
|
+
"'control a remote desktop', 'send an email', 'search my memory notes', 'create a task in the planner'. " +
|
|
33
|
+
"The tools it returns become available immediately and stay available for the rest of the session. " +
|
|
34
|
+
"Use it when none of your current tools can do the job.",
|
|
35
|
+
parameters: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
query: { type: "string", description: "What you need to do, in plain words" },
|
|
39
|
+
limit: { type: "number", description: "How many tools to return (default 6, max 15)" },
|
|
40
|
+
},
|
|
41
|
+
required: ["query"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Tool names shown per MCP server in the catalog before "+N more". */
|
|
47
|
+
export const CATALOG_NAMES_PER_SERVER = 4;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What tool_search can load, by MCP server: "screenbox: desktop_screenshot,
|
|
51
|
+
* desktop_click, desktop_chrome, desktop_type, +19 more". Tools already in
|
|
52
|
+
* hand are left out. Empty when every MCP tool is in hand.
|
|
53
|
+
*
|
|
54
|
+
* Owner, 2026-10-02: with the classifier off the model was handed the built-in
|
|
55
|
+
* tools only, and an operator who had connected Screenbox got an agent that
|
|
56
|
+
* said it had no Screenbox tools. Hiding them saves tokens; not saying they
|
|
57
|
+
* exist cost the task. The catalog is a line per server, not their schemas.
|
|
58
|
+
*/
|
|
59
|
+
export function mcpCatalog(allDefs, inHandNames = []) {
|
|
60
|
+
const inHand = new Set(inHandNames);
|
|
61
|
+
const servers = new Map();
|
|
62
|
+
for (const t of allDefs || []) {
|
|
63
|
+
const name = nameOf(t);
|
|
64
|
+
const m = name.match(/^mcp_([A-Za-z0-9-]+?)_(.+)$/);
|
|
65
|
+
if (!m || inHand.has(name)) continue;
|
|
66
|
+
if (!servers.has(m[1])) servers.set(m[1], []);
|
|
67
|
+
servers.get(m[1]).push(m[2]);
|
|
68
|
+
}
|
|
69
|
+
return [...servers].map(([server, names]) => {
|
|
70
|
+
const shown = names.slice(0, CATALOG_NAMES_PER_SERVER).join(", ");
|
|
71
|
+
const more = names.length > CATALOG_NAMES_PER_SERVER ? `, +${names.length - CATALOG_NAMES_PER_SERVER} more` : "";
|
|
72
|
+
return `- ${server}: ${shown}${more}`;
|
|
73
|
+
}).join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** tool_search's definition with the catalog of what it can load appended. */
|
|
77
|
+
export function toolSearchDefWith(catalog) {
|
|
78
|
+
if (!catalog) return toolSearchDef;
|
|
79
|
+
return {
|
|
80
|
+
...toolSearchDef,
|
|
81
|
+
function: {
|
|
82
|
+
...toolSearchDef.function,
|
|
83
|
+
description: toolSearchDef.function.description +
|
|
84
|
+
"\n\nConnected tool servers whose tools are not loaded yet (search by the tool or server name, " +
|
|
85
|
+
"e.g. 'screenbox desktop_chrome'):\n" + catalog,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Loaded by a search in this process. One Flint process is one session.
|
|
91
|
+
const loaded = new Set();
|
|
92
|
+
|
|
93
|
+
export function loadedToolNames() {
|
|
94
|
+
return [...loaded];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resetLoadedTools() {
|
|
98
|
+
loaded.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Tools that arrived by a plugin install or reload are in hand at once, the
|
|
102
|
+
// way a search's finds are: the model asked for them by installing.
|
|
103
|
+
export function markLoaded(names) {
|
|
104
|
+
for (const n of names || []) if (n) loaded.add(n);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function nameOf(t) {
|
|
108
|
+
return t.function?.name || t.name || "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Words, lowercased, with snake_case and camelCase split, so "desktop_click"
|
|
112
|
+
// matches "click on the desktop".
|
|
113
|
+
function tokens(text) {
|
|
114
|
+
return String(text || "")
|
|
115
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.split(/[^a-z0-9]+/)
|
|
118
|
+
.filter((w) => w.length > 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const STOP = new Set(["the", "and", "for", "with", "to", "of", "in", "on", "an", "or", "my", "me", "it", "is", "at", "by", "from", "this", "that", "use", "do", "need"]);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Rank tools against a plain-words query.
|
|
125
|
+
*
|
|
126
|
+
* Name words count three times description words: a tool's name is the
|
|
127
|
+
* shortest honest statement of what it does. A crude stem (drop a trailing s,
|
|
128
|
+
* ing, ed) lets "clicking" meet "click". Ties keep registry order.
|
|
129
|
+
*
|
|
130
|
+
* @returns {Array} tool definitions, best first
|
|
131
|
+
*/
|
|
132
|
+
export function rankTools(query, candidates, limit = 6) {
|
|
133
|
+
const stem = (w) => w.replace(/(ing|ed|es|s)$/, "") || w;
|
|
134
|
+
const q = tokens(query).filter((w) => !STOP.has(w)).map(stem);
|
|
135
|
+
if (!q.length) return [];
|
|
136
|
+
const scored = candidates.map((t, i) => {
|
|
137
|
+
const name = tokens(nameOf(t)).map(stem);
|
|
138
|
+
const desc = tokens(t.function?.description).map(stem);
|
|
139
|
+
let score = 0;
|
|
140
|
+
for (const w of q) {
|
|
141
|
+
if (name.includes(w)) score += 3;
|
|
142
|
+
if (desc.includes(w)) score += 1;
|
|
143
|
+
}
|
|
144
|
+
return { t, i, score };
|
|
145
|
+
});
|
|
146
|
+
return scored
|
|
147
|
+
.filter((s) => s.score > 0)
|
|
148
|
+
.sort((a, b) => b.score - a.score || a.i - b.i)
|
|
149
|
+
.slice(0, Math.max(1, Math.min(15, limit)))
|
|
150
|
+
.map((s) => s.t);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The handler. Searches everything registered that is not already in hand,
|
|
155
|
+
* loads the matches, and says what they are.
|
|
156
|
+
*
|
|
157
|
+
* @param {Function} getAll returns every registered tool definition
|
|
158
|
+
*/
|
|
159
|
+
export function createToolSearchHandler(getAll) {
|
|
160
|
+
return function toolSearch({ query, limit = 6 } = {}) {
|
|
161
|
+
const inHand = new Set([...CORE_TOOLS, ...loaded, TOOL_SEARCH_NAME]);
|
|
162
|
+
const pool = getAll().filter((t) => !inHand.has(nameOf(t)));
|
|
163
|
+
const found = rankTools(query, pool, limit);
|
|
164
|
+
if (!found.length) {
|
|
165
|
+
return `No tools matched "${query}". Try other words for the job, or do it with the tools you have.`;
|
|
166
|
+
}
|
|
167
|
+
for (const t of found) loaded.add(nameOf(t));
|
|
168
|
+
const lines = found.map((t) => `- ${nameOf(t)}: ${(t.function?.description || "").split("\n")[0].slice(0, 160)}`);
|
|
169
|
+
return `Loaded ${found.length} tool(s), available now:\n${lines.join("\n")}`;
|
|
170
|
+
};
|
|
171
|
+
}
|