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
package/src/bus/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// Unified Message Bus — SQLite-backed priority queue
|
|
2
|
+
// All input channels (TUI, API, child agents, scheduler) push here.
|
|
3
|
+
// Agent loop drains by priority. Crash-safe: pending messages survive restart.
|
|
4
|
+
|
|
5
|
+
import { getDb } from "../tasks/db.js";
|
|
6
|
+
import { createLogger } from "../logging/logger.js";
|
|
7
|
+
|
|
8
|
+
const log = createLogger("bus");
|
|
9
|
+
|
|
10
|
+
// Ring buffer for recent bus events (for /bus/log endpoint)
|
|
11
|
+
const _eventLog = [];
|
|
12
|
+
const MAX_EVENTS = 100;
|
|
13
|
+
function logEvent(type, msg) {
|
|
14
|
+
_eventLog.push({ type, ts: new Date().toISOString(), ...msg });
|
|
15
|
+
if (_eventLog.length > MAX_EVENTS) _eventLog.shift();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get recent bus events (last N).
|
|
20
|
+
* @param {number} [limit=50]
|
|
21
|
+
*/
|
|
22
|
+
export function recentEvents(limit = 50) {
|
|
23
|
+
return _eventLog.slice(-limit);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Priority levels (lower = higher priority)
|
|
27
|
+
export const PRIORITY = {
|
|
28
|
+
USER: 0, // TUI input, /commands
|
|
29
|
+
API: 1, // HTTP /message from paired client
|
|
30
|
+
AGENT: 2, // Child agent responses
|
|
31
|
+
TASK: 3, // Scheduled tasks firing
|
|
32
|
+
EVENT: 4, // Email, Telegram, external events
|
|
33
|
+
SYSTEM: 5, // BG process alerts, heartbeat
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Push a message onto the bus.
|
|
38
|
+
* @param {object} msg
|
|
39
|
+
* @param {string} msg.channel - 'user' | 'api' | 'agent' | 'scheduler' | 'telegram' | 'email' | 'voice'
|
|
40
|
+
* @param {string} msg.content - Message text
|
|
41
|
+
* @param {number} [msg.priority=5] - 0=highest, 10=lowest
|
|
42
|
+
* @param {string} [msg.source] - Agent port, email address, etc.
|
|
43
|
+
* @param {object} [msg.metadata] - Channel-specific data (stored as JSON)
|
|
44
|
+
* @param {string} [msg.sessionId] - Session context
|
|
45
|
+
* @returns {{ id: number }} Inserted message ID
|
|
46
|
+
*/
|
|
47
|
+
export function push({ channel, content, priority, source, metadata, sessionId }) {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
const result = db.prepare(
|
|
50
|
+
`INSERT INTO message_queue (channel, content, priority, source, metadata, session_id)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
52
|
+
).run(
|
|
53
|
+
channel,
|
|
54
|
+
content,
|
|
55
|
+
priority ?? PRIORITY.SYSTEM,
|
|
56
|
+
source || null,
|
|
57
|
+
metadata ? JSON.stringify(metadata) : null,
|
|
58
|
+
sessionId || null,
|
|
59
|
+
);
|
|
60
|
+
const id = result.lastInsertRowid;
|
|
61
|
+
log.debug("PUSH", { id, channel, priority: priority ?? PRIORITY.SYSTEM, source });
|
|
62
|
+
logEvent("push", { id: Number(id), channel, priority: priority ?? PRIORITY.SYSTEM, source, contentPreview: content.slice(0, 80) });
|
|
63
|
+
return { id: Number(id) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Drain the highest-priority pending message.
|
|
68
|
+
* Atomically sets status = 'processing'.
|
|
69
|
+
* @returns {object|null} Message row or null if queue empty
|
|
70
|
+
*/
|
|
71
|
+
export function drain() {
|
|
72
|
+
const db = getDb();
|
|
73
|
+
// Single atomic operation: select + update
|
|
74
|
+
const msg = db.prepare(
|
|
75
|
+
`SELECT * FROM message_queue
|
|
76
|
+
WHERE status = 'pending'
|
|
77
|
+
ORDER BY priority ASC, created_at ASC
|
|
78
|
+
LIMIT 1`
|
|
79
|
+
).get();
|
|
80
|
+
|
|
81
|
+
if (!msg) return null;
|
|
82
|
+
|
|
83
|
+
db.prepare(
|
|
84
|
+
`UPDATE message_queue SET status = 'processing', processed_at = datetime('now') WHERE id = ?`
|
|
85
|
+
).run(msg.id);
|
|
86
|
+
|
|
87
|
+
log.debug("DRAIN", { id: msg.id, channel: msg.channel, priority: msg.priority });
|
|
88
|
+
logEvent("drain", { id: msg.id, channel: msg.channel, priority: msg.priority });
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...msg,
|
|
92
|
+
status: "processing",
|
|
93
|
+
metadata: msg.metadata ? JSON.parse(msg.metadata) : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Mark a message as successfully processed.
|
|
99
|
+
* @param {number} id - Message ID
|
|
100
|
+
* @param {string} [result] - Agent response summary (for audit)
|
|
101
|
+
*/
|
|
102
|
+
export function complete(id, result) {
|
|
103
|
+
const db = getDb();
|
|
104
|
+
db.prepare(
|
|
105
|
+
`UPDATE message_queue SET status = 'done', result = ?, processed_at = datetime('now') WHERE id = ?`
|
|
106
|
+
).run(result || null, id);
|
|
107
|
+
log.debug("DONE", { id, resultLen: result?.length || 0 });
|
|
108
|
+
logEvent("done", { id, resultPreview: (result || "").slice(0, 80) });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Mark a message as failed.
|
|
113
|
+
* @param {number} id - Message ID
|
|
114
|
+
* @param {string} [error] - Error description
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* Take back a message that is still waiting, for the operator to edit it
|
|
118
|
+
* (Esc on an unread message). Only a pending message: once the agent has
|
|
119
|
+
* taken it, it is read and cannot be recalled.
|
|
120
|
+
* @returns {boolean} true if it was still pending and is now cancelled
|
|
121
|
+
*/
|
|
122
|
+
export function cancelPending(id) {
|
|
123
|
+
const db = getDb();
|
|
124
|
+
const r = db.prepare(
|
|
125
|
+
`UPDATE message_queue SET status = 'failed', result = 'recalled for editing', processed_at = datetime('now') WHERE id = ? AND status = 'pending'`
|
|
126
|
+
).run(id);
|
|
127
|
+
if (r.changes) logEvent("cancel", { id });
|
|
128
|
+
return r.changes > 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function fail(id, error) {
|
|
132
|
+
const db = getDb();
|
|
133
|
+
db.prepare(
|
|
134
|
+
`UPDATE message_queue SET status = 'failed', result = ?, processed_at = datetime('now') WHERE id = ?`
|
|
135
|
+
).run(error || "unknown error", id);
|
|
136
|
+
log.warn("FAIL", { id, error });
|
|
137
|
+
logEvent("fail", { id, error });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Crash recovery: reset stale 'processing' messages back to 'pending'.
|
|
142
|
+
* Called on startup to recover from mid-processing crashes.
|
|
143
|
+
* @param {number} [staleMinutes=5] - Messages processing longer than this are stale
|
|
144
|
+
* @returns {number} Number of recovered messages
|
|
145
|
+
*/
|
|
146
|
+
export function recover(staleMinutes = 5) {
|
|
147
|
+
const db = getDb();
|
|
148
|
+
const result = db.prepare(
|
|
149
|
+
`UPDATE message_queue
|
|
150
|
+
SET status = 'pending', processed_at = NULL
|
|
151
|
+
WHERE status = 'processing'
|
|
152
|
+
AND processed_at < datetime('now', ? || ' minutes')`
|
|
153
|
+
).run(`-${staleMinutes}`);
|
|
154
|
+
const count = result.changes;
|
|
155
|
+
if (count > 0) {
|
|
156
|
+
log.info("RECOVER", { count, staleMinutes });
|
|
157
|
+
logEvent("recover", { count, staleMinutes });
|
|
158
|
+
}
|
|
159
|
+
return count;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Older than this, nobody is still waiting for the answer: an API caller's
|
|
163
|
+
// sync wait is 180 s, a child agent's report is about work long gone.
|
|
164
|
+
const LEFTOVER_MAX_AGE_MINUTES = 10;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Start-up: put this session's half-done messages back in the queue, and
|
|
168
|
+
* expire what an earlier run left for someone else.
|
|
169
|
+
*
|
|
170
|
+
* Owner, 2026-10-02: a console started with --new drained a benchmark task an
|
|
171
|
+
* API client had sent five days earlier to another session, and ran it with
|
|
172
|
+
* the API's automatic approval. A message belongs to the session it was sent
|
|
173
|
+
* to and to the minutes after it was sent. Expired: any message of another
|
|
174
|
+
* session, and any message older than LEFTOVER_MAX_AGE_MINUTES. Reminders
|
|
175
|
+
* (channel 'scheduler') are kept: they are the operator's own schedule.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} opts
|
|
178
|
+
* @param {string} opts.sessionId - The session this start runs
|
|
179
|
+
* @returns {{ recovered: number, expired: number }}
|
|
180
|
+
*/
|
|
181
|
+
export function prepareQueueAtStart({ sessionId }) {
|
|
182
|
+
const recovered = recover();
|
|
183
|
+
const db = getDb();
|
|
184
|
+
const r = db.prepare(
|
|
185
|
+
`UPDATE message_queue
|
|
186
|
+
SET status = 'failed', result = 'expired: left in the queue by an earlier run', processed_at = datetime('now')
|
|
187
|
+
WHERE status IN ('pending', 'processing')
|
|
188
|
+
AND channel != 'scheduler'
|
|
189
|
+
AND ((session_id IS NOT NULL AND session_id != ?)
|
|
190
|
+
OR created_at < datetime('now', ? || ' minutes'))`
|
|
191
|
+
).run(sessionId || "", `-${LEFTOVER_MAX_AGE_MINUTES}`);
|
|
192
|
+
const expired = r.changes;
|
|
193
|
+
if (expired > 0) {
|
|
194
|
+
log.info("EXPIRE", { expired, sessionId });
|
|
195
|
+
logEvent("expire", { count: expired });
|
|
196
|
+
}
|
|
197
|
+
return { recovered, expired };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Cleanup old processed messages.
|
|
202
|
+
* @param {number} [doneHours=24] - Delete 'done' messages older than this
|
|
203
|
+
* @param {number} [failedDays=7] - Delete 'failed' messages older than this
|
|
204
|
+
* @returns {{ done: number, failed: number }} Counts of deleted messages
|
|
205
|
+
*/
|
|
206
|
+
export function cleanup(doneHours = 24, failedDays = 7) {
|
|
207
|
+
const db = getDb();
|
|
208
|
+
const doneResult = db.prepare(
|
|
209
|
+
`DELETE FROM message_queue
|
|
210
|
+
WHERE status = 'done'
|
|
211
|
+
AND processed_at < datetime('now', ? || ' hours')`
|
|
212
|
+
).run(`-${doneHours}`);
|
|
213
|
+
const failedResult = db.prepare(
|
|
214
|
+
`DELETE FROM message_queue
|
|
215
|
+
WHERE status = 'failed'
|
|
216
|
+
AND processed_at < datetime('now', ? || ' days')`
|
|
217
|
+
).run(`-${failedDays}`);
|
|
218
|
+
const counts = { done: doneResult.changes, failed: failedResult.changes };
|
|
219
|
+
if (counts.done + counts.failed > 0) {
|
|
220
|
+
log.debug("CLEANUP", counts);
|
|
221
|
+
}
|
|
222
|
+
return counts;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get queue statistics for StatusBar and monitoring.
|
|
227
|
+
* @returns {{ pending: number, processing: number, done: number, failed: number, total: number }}
|
|
228
|
+
*/
|
|
229
|
+
export function stats() {
|
|
230
|
+
const db = getDb();
|
|
231
|
+
const rows = db.prepare(
|
|
232
|
+
`SELECT status, COUNT(*) as count FROM message_queue GROUP BY status`
|
|
233
|
+
).all();
|
|
234
|
+
const s = { pending: 0, processing: 0, done: 0, failed: 0, total: 0 };
|
|
235
|
+
for (const row of rows) {
|
|
236
|
+
s[row.status] = row.count;
|
|
237
|
+
s.total += row.count;
|
|
238
|
+
}
|
|
239
|
+
return s;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get all pending messages (for inspection, not processing).
|
|
244
|
+
* @param {number} [limit=50]
|
|
245
|
+
* @returns {object[]}
|
|
246
|
+
*/
|
|
247
|
+
export function pending(limit = 50) {
|
|
248
|
+
const db = getDb();
|
|
249
|
+
return db.prepare(
|
|
250
|
+
`SELECT * FROM message_queue
|
|
251
|
+
WHERE status = 'pending'
|
|
252
|
+
ORDER BY priority ASC, created_at ASC
|
|
253
|
+
LIMIT ?`
|
|
254
|
+
).all(limit).map(row => ({
|
|
255
|
+
...row,
|
|
256
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get message by ID (for polling result).
|
|
262
|
+
* @param {number} id
|
|
263
|
+
* @returns {object|null}
|
|
264
|
+
*/
|
|
265
|
+
export function getMessage(id) {
|
|
266
|
+
const db = getDb();
|
|
267
|
+
const row = db.prepare("SELECT * FROM message_queue WHERE id = ?").get(id);
|
|
268
|
+
if (!row) return null;
|
|
269
|
+
return { ...row, metadata: row.metadata ? JSON.parse(row.metadata) : null };
|
|
270
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Channel plugin loader
|
|
2
|
+
// Scans plugins/ directory for channel plugins.
|
|
3
|
+
// Each plugin: { name, priority, start(bus), stop() }
|
|
4
|
+
|
|
5
|
+
import { createLogger } from "../logging/logger.js";
|
|
6
|
+
import { readdirSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { config } from "../config.js";
|
|
9
|
+
|
|
10
|
+
const log = createLogger("bus-plugins");
|
|
11
|
+
|
|
12
|
+
const _plugins = new Map(); // name → plugin instance
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load and start all channel plugins from plugins/ directory.
|
|
16
|
+
* @param {object} bus - bus module (push, drain, etc.)
|
|
17
|
+
*/
|
|
18
|
+
export async function loadPlugins(bus) {
|
|
19
|
+
const pluginsDir = path.join(config.projectRoot, "plugins");
|
|
20
|
+
let files;
|
|
21
|
+
try {
|
|
22
|
+
files = readdirSync(pluginsDir).filter(f => f.endsWith(".js"));
|
|
23
|
+
} catch {
|
|
24
|
+
// No plugins directory — that's fine
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
try {
|
|
30
|
+
const mod = await import(path.join(pluginsDir, file));
|
|
31
|
+
const plugin = mod.default || mod;
|
|
32
|
+
if (!plugin.name || !plugin.start) {
|
|
33
|
+
log.warn(`Plugin ${file} missing name or start(), skipping`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
await plugin.start(bus);
|
|
37
|
+
_plugins.set(plugin.name, plugin);
|
|
38
|
+
log.info(`Plugin loaded: ${plugin.name}`, { file, priority: plugin.priority });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
log.error(`Failed to load plugin ${file}: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stop all loaded plugins.
|
|
47
|
+
*/
|
|
48
|
+
export async function stopPlugins() {
|
|
49
|
+
for (const [name, plugin] of _plugins) {
|
|
50
|
+
try {
|
|
51
|
+
if (plugin.stop) await plugin.stop();
|
|
52
|
+
log.debug(`Plugin stopped: ${name}`);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log.error(`Failed to stop plugin ${name}: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
_plugins.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get list of loaded plugin names.
|
|
62
|
+
*/
|
|
63
|
+
export function listPlugins() {
|
|
64
|
+
return [..._plugins.keys()];
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// When a child agent counts as idle, for its idle timeout (index.js).
|
|
2
|
+
//
|
|
3
|
+
// The timeout was reset only when the store's message count changed, and a
|
|
4
|
+
// turn writes its messages back at the end. A child given a research task
|
|
5
|
+
// shut itself down one minute in, between two browser calls, with nothing
|
|
6
|
+
// written (2026-10-02, agent@3010). Working is not idle.
|
|
7
|
+
|
|
8
|
+
/** A turn, queued work, or a stoppable task in flight: not idle. */
|
|
9
|
+
export function childBusy({ processing, processingCount, agentStatus, abortController } = {}) {
|
|
10
|
+
return !!processing
|
|
11
|
+
|| (processingCount || 0) > 0
|
|
12
|
+
|| (!!agentStatus && agentStatus !== "idle")
|
|
13
|
+
|| !!abortController;
|
|
14
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { listSessions } from "./sessions.js";
|
|
3
|
+
import { migrateEnvKey, setKey, hasKey } from "./providers/keys.js";
|
|
4
|
+
import { listProviders, getProvider } from "./providers/registry.js";
|
|
5
|
+
import { setActiveProvider, setLastModel } from "./providers/state.js";
|
|
6
|
+
import { config, needsFirstRunSetup } from "./config.js";
|
|
7
|
+
|
|
8
|
+
// -- CLI argument parsing --
|
|
9
|
+
|
|
10
|
+
export function getArgValue(name) {
|
|
11
|
+
const idx = process.argv.indexOf(name);
|
|
12
|
+
return (idx !== -1 && process.argv[idx + 1]) ? process.argv[idx + 1] : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseCLI() {
|
|
16
|
+
const args = process.argv;
|
|
17
|
+
if (args.includes("--list")) return { action: "list" };
|
|
18
|
+
if (args.includes("--headless")) {
|
|
19
|
+
const taskIdx = args.indexOf("--task");
|
|
20
|
+
const task = (taskIdx !== -1 && args[taskIdx + 1]) ? args[taskIdx + 1] : null;
|
|
21
|
+
const cwdIdx = args.indexOf("--cwd");
|
|
22
|
+
const cwd = (cwdIdx !== -1 && args[cwdIdx + 1]) ? args[cwdIdx + 1] : null;
|
|
23
|
+
const budgetIdx = args.indexOf("--budget");
|
|
24
|
+
const budget = (budgetIdx !== -1 && args[budgetIdx + 1]) ? parseFloat(args[budgetIdx + 1]) : null;
|
|
25
|
+
return { action: "headless", task, cwd, budget };
|
|
26
|
+
}
|
|
27
|
+
if (args.includes("--new")) return { action: "new" };
|
|
28
|
+
if (args.includes("--last")) return { action: "last" };
|
|
29
|
+
const idx = args.indexOf("--session");
|
|
30
|
+
if (idx !== -1 && args[idx + 1])
|
|
31
|
+
return { action: "resume", id: args[idx + 1] };
|
|
32
|
+
return { action: "new" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runListSessions() {
|
|
36
|
+
const sessions = await listSessions();
|
|
37
|
+
if (!sessions.length) {
|
|
38
|
+
console.log(chalk.gray("No sessions found."));
|
|
39
|
+
} else {
|
|
40
|
+
console.log(chalk.cyan.bold("\n Sessions:\n"));
|
|
41
|
+
for (const s of sessions) {
|
|
42
|
+
console.log(` ${chalk.yellow(s.id)} ${chalk.gray(s.model)} ${s.preview}`);
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
45
|
+
}
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function migrateKeys() {
|
|
50
|
+
await migrateEnvKey("OPENROUTER_API_KEY", "openrouter");
|
|
51
|
+
await migrateEnvKey("OPENAI_API_KEY", "openai");
|
|
52
|
+
await migrateEnvKey("ANTHROPIC_API_KEY", "anthropic");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Real provider keys are long tokens; a few characters is a stray keypress. */
|
|
56
|
+
export function looksLikeApiKey(key) {
|
|
57
|
+
const k = String(key || "").trim();
|
|
58
|
+
return k.length >= 20 && !/\s/.test(k);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runFirstRunSetup(cli) {
|
|
62
|
+
if (!needsFirstRunSetup || cli.action === "list") return;
|
|
63
|
+
|
|
64
|
+
const readline = await import("node:readline");
|
|
65
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
66
|
+
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
67
|
+
|
|
68
|
+
console.log(chalk.cyan("\n +-------------------------------------+"));
|
|
69
|
+
console.log(chalk.cyan(" | Flint Agent -- First Run |"));
|
|
70
|
+
console.log(chalk.cyan(" +-------------------------------------+\n"));
|
|
71
|
+
console.log(chalk.gray(" No API keys found. Let's set one up.\n"));
|
|
72
|
+
|
|
73
|
+
const providers = listProviders().filter((p) => p.keyRequired);
|
|
74
|
+
for (let i = 0; i < providers.length; i++) {
|
|
75
|
+
console.log(chalk.white(` ${i + 1}. ${providers[i].name} (${providers[i].id})`));
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk.white(` ${providers.length + 1}. Ollama (local, no key needed)`));
|
|
78
|
+
console.log();
|
|
79
|
+
|
|
80
|
+
const choice = await ask(chalk.cyan(" Choose provider (number): "));
|
|
81
|
+
const idx = parseInt(choice.trim(), 10) - 1;
|
|
82
|
+
|
|
83
|
+
if (idx === providers.length) {
|
|
84
|
+
config.provider = "ollama";
|
|
85
|
+
setActiveProvider("ollama");
|
|
86
|
+
config.model = "llama3.2";
|
|
87
|
+
console.log(chalk.green("\n Selected Ollama. Make sure it's running on localhost:11434.\n"));
|
|
88
|
+
} else if (idx >= 0 && idx < providers.length) {
|
|
89
|
+
const selected = providers[idx];
|
|
90
|
+
// Owner, 2026-10-01: the spinner hid this prompt, a stray "1" was saved as
|
|
91
|
+
// the OpenRouter key, and the wizard said "Key saved". Anything this short
|
|
92
|
+
// is not a key, so ask again rather than store it.
|
|
93
|
+
let trimmedKey = "";
|
|
94
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
95
|
+
trimmedKey = (await ask(chalk.cyan(` Enter ${selected.name} API key: `))).trim();
|
|
96
|
+
if (!trimmedKey || looksLikeApiKey(trimmedKey)) break;
|
|
97
|
+
console.log(chalk.yellow(" That does not look like an API key (too short). Paste the full key."));
|
|
98
|
+
trimmedKey = "";
|
|
99
|
+
}
|
|
100
|
+
if (trimmedKey) {
|
|
101
|
+
await setKey(selected.id, trimmedKey);
|
|
102
|
+
config.provider = selected.id;
|
|
103
|
+
config.apiKey = trimmedKey;
|
|
104
|
+
setActiveProvider(selected.id);
|
|
105
|
+
config.model = selected.defaultModel;
|
|
106
|
+
console.log(chalk.green(`\n Key saved (encrypted). Provider: ${selected.name}, Model: ${selected.defaultModel}\n`));
|
|
107
|
+
} else {
|
|
108
|
+
console.log(chalk.yellow("\n No key entered. You can add one later with /key <provider>.\n"));
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
console.log(chalk.yellow("\n Invalid choice. You can set up later with /key <provider>.\n"));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Once, at the end of the first run: where Flint lives (owner, 2026-10-02:
|
|
115
|
+
// one line, no nagging).
|
|
116
|
+
console.log(chalk.gray(" Flint is free and open source: https://klymentiev.com/projects/flint\n"));
|
|
117
|
+
rl.close();
|
|
118
|
+
}
|