@mulmoclaude/core 0.1.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/assets/helps/billing-clients-worklog.md +215 -0
- package/assets/helps/billing-invoice.md +458 -0
- package/assets/helps/business.md +104 -0
- package/assets/helps/collection-skills.md +810 -0
- package/assets/helps/custom-view.md +433 -0
- package/assets/helps/feeds.md +114 -0
- package/assets/helps/gemini.md +57 -0
- package/assets/helps/github.md +23 -0
- package/assets/helps/guide.md +61 -0
- package/assets/helps/index.md +89 -0
- package/assets/helps/lessons-collection.md +400 -0
- package/assets/helps/mulmoscript.md +249 -0
- package/assets/helps/portfolio-tracker.md +211 -0
- package/assets/helps/presentation-deck.md +828 -0
- package/assets/helps/presenthtml.md +89 -0
- package/assets/helps/sandbox.md +97 -0
- package/assets/helps/spreadsheet.md +43 -0
- package/assets/helps/storyteller.md +101 -0
- package/assets/helps/telegram.md +136 -0
- package/assets/helps/todo-collection.md +140 -0
- package/assets/helps/vocabulary.md +109 -0
- package/assets/helps/wiki.md +168 -0
- package/assets/skills-preset/mc-cooking-coach/SKILL.md +217 -0
- package/assets/skills-preset/mc-library/SKILL.md +188 -0
- package/assets/skills-preset/mc-manage-automations/SKILL.md +119 -0
- package/assets/skills-preset/mc-manage-skills/SKILL.md +141 -0
- package/assets/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
- package/assets/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
- package/assets/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
- package/assets/skills-preset/mc-wiki-promote/SKILL.md +175 -0
- package/assets/skills-preset/mc-zenn/SKILL.md +136 -0
- package/dist/chunk-CKQMccvm.cjs +28 -0
- package/dist/collection/core/actionVisible.d.ts +34 -0
- package/dist/collection/core/calendarGrid.d.ts +120 -0
- package/dist/collection/core/deriveAll.d.ts +38 -0
- package/dist/collection/core/derivedFormula.d.ts +18 -0
- package/dist/collection/core/draft.d.ts +18 -0
- package/dist/collection/core/enumColors.d.ts +33 -0
- package/dist/collection/core/errorMessage.d.ts +4 -0
- package/dist/collection/core/itemLabel.d.ts +12 -0
- package/dist/collection/core/presentCollection.d.ts +13 -0
- package/dist/collection/core/promptSafety.d.ts +1 -0
- package/dist/collection/core/schema.d.ts +355 -0
- package/dist/collection/core/shortHexId.d.ts +8 -0
- package/dist/collection/core/sortItems.d.ts +29 -0
- package/dist/collection/core/uiTypes.d.ts +106 -0
- package/dist/collection/index.cjs +793 -0
- package/dist/collection/index.cjs.map +1 -0
- package/dist/collection/index.d.ts +14 -0
- package/dist/collection/index.js +740 -0
- package/dist/collection/index.js.map +1 -0
- package/dist/collection/paths.cjs +44 -0
- package/dist/collection/paths.cjs.map +1 -0
- package/dist/collection/paths.js +41 -0
- package/dist/collection/paths.js.map +1 -0
- package/dist/collection/server/atomic.d.ts +1 -0
- package/dist/collection/server/delete.d.ts +38 -0
- package/dist/collection/server/derive.d.ts +8 -0
- package/dist/collection/server/discoveredCollection.d.ts +18 -0
- package/dist/collection/server/discovery.d.ts +227 -0
- package/dist/collection/server/host.d.ts +77 -0
- package/dist/collection/server/index.cjs +1721 -0
- package/dist/collection/server/index.cjs.map +1 -0
- package/dist/collection/server/index.d.ts +11 -0
- package/dist/collection/server/index.js +1671 -0
- package/dist/collection/server/index.js.map +1 -0
- package/dist/collection/server/io.d.ts +114 -0
- package/dist/collection/server/paths.d.ts +52 -0
- package/dist/collection/server/spawn.d.ts +55 -0
- package/dist/collection/server/templatePath.d.ts +25 -0
- package/dist/collection/server/util.d.ts +3 -0
- package/dist/collection/server/validate.d.ts +19 -0
- package/dist/collection/server/views.d.ts +20 -0
- package/dist/deriveAll-C15OpM3K.cjs +399 -0
- package/dist/deriveAll-C15OpM3K.cjs.map +1 -0
- package/dist/deriveAll-C6BYnpBL.js +364 -0
- package/dist/deriveAll-C6BYnpBL.js.map +1 -0
- package/dist/file-change/index.cjs +72 -0
- package/dist/file-change/index.cjs.map +1 -0
- package/dist/file-change/index.d.ts +43 -0
- package/dist/file-change/index.js +66 -0
- package/dist/file-change/index.js.map +1 -0
- package/dist/notifier/engine.d.ts +72 -0
- package/dist/notifier/index.cjs +484 -0
- package/dist/notifier/index.cjs.map +1 -0
- package/dist/notifier/index.d.ts +3 -0
- package/dist/notifier/index.js +464 -0
- package/dist/notifier/index.js.map +1 -0
- package/dist/notifier/store.d.ts +18 -0
- package/dist/notifier/types.d.ts +118 -0
- package/dist/notifier/validate.d.ts +17 -0
- package/dist/scheduler/adapter.d.ts +48 -0
- package/dist/scheduler/index.cjs +352 -0
- package/dist/scheduler/index.cjs.map +1 -0
- package/dist/scheduler/index.d.ts +2 -0
- package/dist/scheduler/index.js +343 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/task-manager.d.ts +51 -0
- package/dist/whisper/client.cjs +241 -0
- package/dist/whisper/client.cjs.map +1 -0
- package/dist/whisper/client.d.ts +35 -0
- package/dist/whisper/client.js +239 -0
- package/dist/whisper/client.js.map +1 -0
- package/dist/whisper/ffmpeg.d.ts +6 -0
- package/dist/whisper/index.cjs +433 -0
- package/dist/whisper/index.cjs.map +1 -0
- package/dist/whisper/index.d.ts +5 -0
- package/dist/whisper/index.js +425 -0
- package/dist/whisper/index.js.map +1 -0
- package/dist/whisper/internal.d.ts +11 -0
- package/dist/whisper/models.d.ts +49 -0
- package/dist/whisper/sidecar.d.ts +8 -0
- package/dist/whisper/whisper.d.ts +28 -0
- package/dist/workspace-setup/assets.d.ts +10 -0
- package/dist/workspace-setup/index.d.ts +3 -0
- package/dist/workspace-setup/index.js +556 -0
- package/dist/workspace-setup/index.js.map +1 -0
- package/dist/workspace-setup/slug.d.ts +6 -0
- package/dist/workspace-setup/slug.js +13 -0
- package/dist/workspace-setup/slug.js.map +1 -0
- package/dist/workspace-setup/sync.d.ts +94 -0
- package/package.json +95 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { SCHEDULE_TYPES, TASK_RESULTS, TASK_TRIGGERS, appendLogEntry, computeCatchUpPlan, emptyState, loadState, nextWindowAfter, queryLog, updateAndSave } from "@receptron/task-scheduler";
|
|
5
|
+
//#region src/scheduler/task-manager.ts
|
|
6
|
+
var ONE_SECOND_MS$1 = 1e3;
|
|
7
|
+
var ONE_MINUTE_MS = 60 * ONE_SECOND_MS$1;
|
|
8
|
+
var ONE_HOUR_MS = 60 * ONE_MINUTE_MS;
|
|
9
|
+
var NOOP_LOG$1 = {
|
|
10
|
+
info: () => {},
|
|
11
|
+
warn: () => {},
|
|
12
|
+
error: () => {}
|
|
13
|
+
};
|
|
14
|
+
function isDue(now, schedule, tickMs) {
|
|
15
|
+
if (schedule.type === SCHEDULE_TYPES.interval) {
|
|
16
|
+
const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS$1;
|
|
17
|
+
return Math.floor(msSinceMidnight / tickMs) * tickMs % schedule.intervalMs === 0;
|
|
18
|
+
}
|
|
19
|
+
if (schedule.type === SCHEDULE_TYPES.daily) {
|
|
20
|
+
const [hours, minutes] = schedule.time.split(":").map(Number);
|
|
21
|
+
const targetMs = hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;
|
|
22
|
+
const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS$1;
|
|
23
|
+
return Math.floor(msSinceMidnight / tickMs) * tickMs === targetMs;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function createTaskManager(options) {
|
|
28
|
+
const tickMs = options?.tickMs ?? ONE_MINUTE_MS;
|
|
29
|
+
const now = options?.now ?? (() => /* @__PURE__ */ new Date());
|
|
30
|
+
const log = options?.log ?? NOOP_LOG$1;
|
|
31
|
+
const registry = /* @__PURE__ */ new Map();
|
|
32
|
+
let timer = null;
|
|
33
|
+
function collectDueTasks(currentTime) {
|
|
34
|
+
const independent = [];
|
|
35
|
+
const dependent = [];
|
|
36
|
+
for (const def of registry.values()) {
|
|
37
|
+
if (def.enabled === false) continue;
|
|
38
|
+
if (!isDue(currentTime, def.schedule, tickMs)) continue;
|
|
39
|
+
if (def.dependsOn) dependent.push(def);
|
|
40
|
+
else independent.push(def);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
independent,
|
|
44
|
+
dependent
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function runAndTrack(def, currentTime, succeeded) {
|
|
48
|
+
try {
|
|
49
|
+
await def.run({
|
|
50
|
+
taskId: def.id,
|
|
51
|
+
now: currentTime
|
|
52
|
+
});
|
|
53
|
+
succeeded.add(def.id);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
log.error("task failed", {
|
|
56
|
+
id: def.id,
|
|
57
|
+
error: String(err)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function runDependentChain(dependent, currentTime, succeeded) {
|
|
62
|
+
let remaining = [...dependent];
|
|
63
|
+
let progress = true;
|
|
64
|
+
while (remaining.length > 0 && progress) {
|
|
65
|
+
progress = false;
|
|
66
|
+
const next = [];
|
|
67
|
+
for (const def of remaining) {
|
|
68
|
+
const dep = def.dependsOn;
|
|
69
|
+
if (!dep || !succeeded.has(dep)) {
|
|
70
|
+
next.push(def);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
await runAndTrack(def, currentTime, succeeded);
|
|
74
|
+
progress = true;
|
|
75
|
+
}
|
|
76
|
+
remaining = next;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function onTick() {
|
|
80
|
+
const currentTime = now();
|
|
81
|
+
const { independent, dependent } = collectDueTasks(currentTime);
|
|
82
|
+
const succeeded = /* @__PURE__ */ new Set();
|
|
83
|
+
await Promise.all(independent.map((def) => runAndTrack(def, currentTime, succeeded)));
|
|
84
|
+
await runDependentChain(dependent, currentTime, succeeded);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
async tick() {
|
|
88
|
+
await onTick();
|
|
89
|
+
},
|
|
90
|
+
registerTask(def) {
|
|
91
|
+
if (registry.has(def.id)) throw new Error(`[task-manager] Task "${def.id}" is already registered`);
|
|
92
|
+
registry.set(def.id, def);
|
|
93
|
+
log.info("registered", { id: def.id });
|
|
94
|
+
},
|
|
95
|
+
updateSchedule(taskId, schedule) {
|
|
96
|
+
const def = registry.get(taskId);
|
|
97
|
+
if (!def) return false;
|
|
98
|
+
def.schedule = schedule;
|
|
99
|
+
log.info("schedule updated", { id: taskId });
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
102
|
+
removeTask(taskId) {
|
|
103
|
+
if (registry.delete(taskId)) log.info("removed", { id: taskId });
|
|
104
|
+
},
|
|
105
|
+
start() {
|
|
106
|
+
if (timer) return;
|
|
107
|
+
timer = setInterval(onTick, tickMs);
|
|
108
|
+
log.info("started", { tickMs });
|
|
109
|
+
},
|
|
110
|
+
stop() {
|
|
111
|
+
if (timer) {
|
|
112
|
+
clearInterval(timer);
|
|
113
|
+
timer = null;
|
|
114
|
+
log.info("stopped");
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
listTasks() {
|
|
118
|
+
return [...registry.values()].map((taskDef) => ({
|
|
119
|
+
id: taskDef.id,
|
|
120
|
+
description: taskDef.description,
|
|
121
|
+
schedule: taskDef.schedule,
|
|
122
|
+
dependsOn: taskDef.dependsOn
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/scheduler/adapter.ts
|
|
129
|
+
var ONE_SECOND_MS = 1e3;
|
|
130
|
+
var SCHEDULER_CONFIG_DIR = "config/scheduler";
|
|
131
|
+
var SCHEDULER_DATA_DIR = "data/scheduler/logs";
|
|
132
|
+
var NOOP_LOG = {
|
|
133
|
+
info: () => {},
|
|
134
|
+
warn: () => {},
|
|
135
|
+
error: () => {}
|
|
136
|
+
};
|
|
137
|
+
var config = null;
|
|
138
|
+
/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */
|
|
139
|
+
function configureScheduler(injected) {
|
|
140
|
+
config = injected;
|
|
141
|
+
}
|
|
142
|
+
function requireConfig() {
|
|
143
|
+
if (!config) throw new Error("scheduler: configureScheduler() not called");
|
|
144
|
+
return config;
|
|
145
|
+
}
|
|
146
|
+
function logger() {
|
|
147
|
+
return config?.log ?? NOOP_LOG;
|
|
148
|
+
}
|
|
149
|
+
function errorMessage(err) {
|
|
150
|
+
return err instanceof Error ? err.message : String(err);
|
|
151
|
+
}
|
|
152
|
+
function stateFilePath() {
|
|
153
|
+
return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, "state.json");
|
|
154
|
+
}
|
|
155
|
+
function logsDir() {
|
|
156
|
+
return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);
|
|
157
|
+
}
|
|
158
|
+
function stateDeps() {
|
|
159
|
+
return {
|
|
160
|
+
readFile: (filePath) => readFile(filePath, "utf-8"),
|
|
161
|
+
writeFileAtomic: (filePath, content) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),
|
|
162
|
+
exists: existsSync
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
var logDeps = {
|
|
166
|
+
appendFile: (filePath, content) => appendFile(filePath, content),
|
|
167
|
+
readFile: (filePath) => readFile(filePath, "utf-8"),
|
|
168
|
+
exists: existsSync,
|
|
169
|
+
ensureDir: (directoryPath) => mkdir(directoryPath, { recursive: true }).then(() => {})
|
|
170
|
+
};
|
|
171
|
+
var stateMap = /* @__PURE__ */ new Map();
|
|
172
|
+
var systemTasks = [];
|
|
173
|
+
var taskManagerRef = null;
|
|
174
|
+
/**
|
|
175
|
+
* Initialize the scheduler adapter. Call once at server startup AFTER the
|
|
176
|
+
* task-manager is created but BEFORE `taskManager.start()`.
|
|
177
|
+
*/
|
|
178
|
+
async function initScheduler(taskManager, tasks) {
|
|
179
|
+
await mkdir(path.dirname(stateFilePath()), { recursive: true });
|
|
180
|
+
await mkdir(logsDir(), { recursive: true });
|
|
181
|
+
stateMap = await loadState(stateFilePath(), stateDeps());
|
|
182
|
+
systemTasks.length = 0;
|
|
183
|
+
systemTasks.push(...tasks);
|
|
184
|
+
taskManagerRef = taskManager;
|
|
185
|
+
const plan = computeCatchUpPlan(tasks.map((taskDef) => ({
|
|
186
|
+
id: taskDef.id,
|
|
187
|
+
name: taskDef.name,
|
|
188
|
+
schedule: toCoreSchedule(taskDef.schedule),
|
|
189
|
+
missedRunPolicy: taskDef.missedRunPolicy,
|
|
190
|
+
enabled: true
|
|
191
|
+
})), stateMap, Date.now());
|
|
192
|
+
for (const skip of plan.skipped) {
|
|
193
|
+
logger().info("catch-up skipped", {
|
|
194
|
+
taskId: skip.taskId,
|
|
195
|
+
windows: skip.windowCount
|
|
196
|
+
});
|
|
197
|
+
await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });
|
|
198
|
+
}
|
|
199
|
+
if (plan.runs.length > 0) {
|
|
200
|
+
logger().info("catch-up enqueued", { runs: plan.runs.length });
|
|
201
|
+
for (const run of plan.runs) {
|
|
202
|
+
const task = tasks.find((taskDef) => taskDef.id === run.taskId);
|
|
203
|
+
if (!task) continue;
|
|
204
|
+
await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const task of tasks) taskManager.registerTask({
|
|
208
|
+
id: task.id,
|
|
209
|
+
description: task.description,
|
|
210
|
+
schedule: task.schedule,
|
|
211
|
+
run: async () => {
|
|
212
|
+
await executeAndLog(task, computeCurrentWindow(task), TASK_TRIGGERS.scheduled);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
logger().info("initialized", {
|
|
216
|
+
tasks: tasks.map((taskDef) => taskDef.id),
|
|
217
|
+
stateEntries: stateMap.size
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Apply a schedule override to a running system task. Updates the
|
|
221
|
+
* in-memory task definition, the task-manager, and recalculates
|
|
222
|
+
* nextScheduledAt in persisted state. */
|
|
223
|
+
async function applyScheduleOverride(taskId, schedule) {
|
|
224
|
+
const task = systemTasks.find((taskDef) => taskDef.id === taskId);
|
|
225
|
+
if (!task || !taskManagerRef) return false;
|
|
226
|
+
if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;
|
|
227
|
+
task.schedule = schedule;
|
|
228
|
+
await safeUpdateState(taskId, { nextScheduledAt: computeNextScheduled(task) });
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
/** Query execution logs — used by API routes. */
|
|
232
|
+
async function getSchedulerLogs(opts) {
|
|
233
|
+
return queryLog(logsDir(), opts, logDeps);
|
|
234
|
+
}
|
|
235
|
+
/** Get all task states — used by API routes. */
|
|
236
|
+
function getSchedulerTasks() {
|
|
237
|
+
return systemTasks.map((taskDef) => ({
|
|
238
|
+
id: taskDef.id,
|
|
239
|
+
name: taskDef.name,
|
|
240
|
+
description: taskDef.description,
|
|
241
|
+
schedule: taskDef.schedule,
|
|
242
|
+
missedRunPolicy: taskDef.missedRunPolicy,
|
|
243
|
+
state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id)
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
/** Test-only: clear config + in-memory state. */
|
|
247
|
+
function resetSchedulerForTesting() {
|
|
248
|
+
config = null;
|
|
249
|
+
stateMap = /* @__PURE__ */ new Map();
|
|
250
|
+
systemTasks.length = 0;
|
|
251
|
+
taskManagerRef = null;
|
|
252
|
+
}
|
|
253
|
+
async function executeAndLog(task, scheduledFor, trigger) {
|
|
254
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
255
|
+
const startMs = Date.now();
|
|
256
|
+
let errMsg = null;
|
|
257
|
+
try {
|
|
258
|
+
await task.run();
|
|
259
|
+
} catch (err) {
|
|
260
|
+
errMsg = errorMessage(err);
|
|
261
|
+
logger().error("task failed", {
|
|
262
|
+
taskId: task.id,
|
|
263
|
+
error: errMsg
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
await safePersist(task, scheduledFor, startedAt, Date.now() - startMs, trigger, errMsg);
|
|
267
|
+
}
|
|
268
|
+
/** Best-effort persistence — state and log are independent. A failure in
|
|
269
|
+
* one does not block the other, and neither propagates upward. */
|
|
270
|
+
async function safePersist(task, scheduledFor, startedAt, durationMs, trigger, errMsg) {
|
|
271
|
+
const isSuccess = errMsg === null;
|
|
272
|
+
const currentState = stateMap.get(task.id);
|
|
273
|
+
try {
|
|
274
|
+
await updateAndSave(stateFilePath(), stateMap, task.id, {
|
|
275
|
+
lastRunAt: scheduledFor,
|
|
276
|
+
lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,
|
|
277
|
+
lastRunDurationMs: durationMs,
|
|
278
|
+
lastErrorMessage: errMsg,
|
|
279
|
+
consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,
|
|
280
|
+
totalRuns: (currentState?.totalRuns ?? 0) + 1,
|
|
281
|
+
nextScheduledAt: computeNextScheduled(task)
|
|
282
|
+
}, stateDeps());
|
|
283
|
+
} catch (err) {
|
|
284
|
+
logger().warn("state persistence failed", {
|
|
285
|
+
taskId: task.id,
|
|
286
|
+
error: String(err)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
await appendLogEntry(logsDir(), {
|
|
291
|
+
taskId: task.id,
|
|
292
|
+
taskName: task.name,
|
|
293
|
+
scheduledFor,
|
|
294
|
+
startedAt,
|
|
295
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
296
|
+
result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,
|
|
297
|
+
durationMs,
|
|
298
|
+
trigger,
|
|
299
|
+
...errMsg !== null && { errorMessage: errMsg }
|
|
300
|
+
}, logDeps);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
logger().warn("log persistence failed", {
|
|
303
|
+
taskId: task.id,
|
|
304
|
+
error: String(err)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** Safe state update — swallows errors. */
|
|
309
|
+
async function safeUpdateState(taskId, patch) {
|
|
310
|
+
try {
|
|
311
|
+
await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());
|
|
312
|
+
} catch (err) {
|
|
313
|
+
logger().warn("state update failed", {
|
|
314
|
+
taskId,
|
|
315
|
+
error: String(err)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Compute the window boundary that the current tick belongs to. For
|
|
320
|
+
* scheduled runs, this is the epoch-aligned window — not the wall-clock
|
|
321
|
+
* time of execution. This keeps lastRunAt consistent with catch-up's
|
|
322
|
+
* window-based accounting. */
|
|
323
|
+
function computeCurrentWindow(task) {
|
|
324
|
+
const coreSchedule = toCoreSchedule(task.schedule);
|
|
325
|
+
const nowMs = Date.now();
|
|
326
|
+
const windowMs = nextWindowAfter(coreSchedule, nowMs - (coreSchedule.type === SCHEDULE_TYPES.interval ? coreSchedule.intervalSec * ONE_SECOND_MS : 0));
|
|
327
|
+
return windowMs !== null && windowMs <= nowMs ? new Date(windowMs).toISOString() : new Date(nowMs).toISOString();
|
|
328
|
+
}
|
|
329
|
+
function computeNextScheduled(task) {
|
|
330
|
+
const next = nextWindowAfter(toCoreSchedule(task.schedule), Date.now() + 1);
|
|
331
|
+
return next !== null ? new Date(next).toISOString() : null;
|
|
332
|
+
}
|
|
333
|
+
function toCoreSchedule(schedule) {
|
|
334
|
+
if (schedule.type === SCHEDULE_TYPES.interval) return {
|
|
335
|
+
type: SCHEDULE_TYPES.interval,
|
|
336
|
+
intervalSec: Math.round(schedule.intervalMs / ONE_SECOND_MS)
|
|
337
|
+
};
|
|
338
|
+
return schedule;
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
export { applyScheduleOverride, configureScheduler, createTaskManager, getSchedulerLogs, getSchedulerTasks, initScheduler, resetSchedulerForTesting };
|
|
342
|
+
|
|
343
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/scheduler/task-manager.ts","../../src/scheduler/adapter.ts"],"sourcesContent":["// Generic dependency-ordered cron tick engine. Host-agnostic: the only\n// host coupling (a logger) is injected via options. Schedules are either\n// fixed intervals or a daily UTC time; tasks may declare a `dependsOn`\n// edge so an ordering like \"news fetch → journal → memory extraction\"\n// runs in sequence within one tick.\n\nimport { SCHEDULE_TYPES } from \"@receptron/task-scheduler\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\nconst ONE_HOUR_MS = 60 * ONE_MINUTE_MS;\n\n/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport interface SchedulerLogger {\n info: (message: string, data?: Record<string, unknown>) => void;\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nexport type TaskSchedule = { type: typeof SCHEDULE_TYPES.interval; intervalMs: number } | { type: typeof SCHEDULE_TYPES.daily; time: string }; // time: \"HH:MM\" in UTC\n\nexport interface TaskRunContext {\n taskId: string;\n now: Date;\n}\n\nexport interface TaskDefinition {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n enabled?: boolean; // default: true\n /** If set, this task only fires after the named task has completed\n * successfully in the current tick cycle. Enforces ordering like\n * \"news fetch → journal → memory extraction\". */\n dependsOn?: string;\n run: (ctx: TaskRunContext) => Promise<void>;\n}\n\nexport interface ITaskManager {\n registerTask: (def: TaskDefinition) => void;\n removeTask: (taskId: string) => void;\n /** Update the schedule of an existing task. Returns false if not found. */\n updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;\n start: () => void;\n stop: () => void;\n /** Run one tick manually (for testing). */\n tick: () => Promise<void>;\n listTasks: () => {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n dependsOn?: string;\n }[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\n}\n\nfunction isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS;\n // Round down to tick boundary, then check if it aligns with the interval\n const rounded = Math.floor(msSinceMidnight / tickMs) * tickMs;\n return rounded % schedule.intervalMs === 0;\n }\n\n if (schedule.type === SCHEDULE_TYPES.daily) {\n const [hours, minutes] = schedule.time.split(\":\").map(Number);\n const targetMs = hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;\n const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS;\n const rounded = Math.floor(msSinceMidnight / tickMs) * tickMs;\n return rounded === targetMs;\n }\n\n return false;\n}\n\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n const now = options?.now ?? (() => new Date());\n const log = options?.log ?? NOOP_LOG;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function collectDueTasks(currentTime: Date): {\n independent: TaskDefinition[];\n dependent: TaskDefinition[];\n } {\n const independent: TaskDefinition[] = [];\n const dependent: TaskDefinition[] = [];\n for (const def of registry.values()) {\n if (def.enabled === false) continue;\n if (!isDue(currentTime, def.schedule, tickMs)) continue;\n if (def.dependsOn) {\n dependent.push(def);\n } else {\n independent.push(def);\n }\n }\n return { independent, dependent };\n }\n\n async function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>): Promise<void> {\n try {\n await def.run({ taskId: def.id, now: currentTime });\n succeeded.add(def.id);\n } catch (err) {\n log.error(\"task failed\", {\n id: def.id,\n error: String(err),\n });\n }\n }\n\n async function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>): Promise<void> {\n let remaining = [...dependent];\n let progress = true;\n while (remaining.length > 0 && progress) {\n progress = false;\n const next: TaskDefinition[] = [];\n for (const def of remaining) {\n const dep = def.dependsOn;\n if (!dep || !succeeded.has(dep)) {\n next.push(def);\n continue;\n }\n await runAndTrack(def, currentTime, succeeded);\n progress = true;\n }\n remaining = next;\n }\n }\n\n async function onTick(): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n await Promise.all(independent.map((def) => runAndTrack(def, currentTime, succeeded)));\n\n await runDependentChain(dependent, currentTime, succeeded);\n }\n\n return {\n async tick() {\n await onTick();\n },\n\n registerTask(def: TaskDefinition) {\n if (registry.has(def.id)) {\n throw new Error(`[task-manager] Task \"${def.id}\" is already registered`);\n }\n registry.set(def.id, def);\n log.info(\"registered\", { id: def.id });\n },\n\n updateSchedule(taskId: string, schedule: TaskSchedule): boolean {\n const def = registry.get(taskId);\n if (!def) return false;\n def.schedule = schedule;\n log.info(\"schedule updated\", { id: taskId });\n return true;\n },\n\n removeTask(taskId: string) {\n if (registry.delete(taskId)) {\n log.info(\"removed\", { id: taskId });\n }\n },\n\n start() {\n if (timer) return;\n timer = setInterval(onTick, tickMs);\n log.info(\"started\", { tickMs });\n },\n\n stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n log.info(\"stopped\");\n }\n },\n\n listTasks() {\n return [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\n },\n };\n}\n","// Adapter that wires the pure scheduler library (@receptron/task-scheduler)\n// to a host's task-manager + workspace. Registers system tasks, runs\n// catch-up on startup, and persists execution state + logs.\n//\n// Host-agnostic: the workspace root, the atomic file writer, and the\n// logger are injected via `configureScheduler`. The host supplies its OWN\n// system tasks (journal / feeds / user-cron in MulmoClaude) to\n// `initScheduler` — the package owns no task definitions. Deliberately\n// thin: all complex scheduling logic lives in @receptron/task-scheduler.\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, appendFile, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n type TaskSchedule,\n type TaskExecutionState,\n type TaskLogEntry,\n type CatchUpTask,\n type TaskTrigger,\n emptyState,\n computeCatchUpPlan,\n nextWindowAfter,\n loadState,\n updateAndSave,\n appendLogEntry,\n queryLog,\n SCHEDULE_TYPES,\n TASK_RESULTS,\n TASK_TRIGGERS,\n type MISSED_RUN_POLICIES,\n type StateMap,\n type StateDeps,\n type LogDeps,\n} from \"@receptron/task-scheduler\";\nimport type { ITaskManager, TaskDefinition, SchedulerLogger } from \"./task-manager.js\";\n\nconst ONE_SECOND_MS = 1000;\nconst SCHEDULER_CONFIG_DIR = \"config/scheduler\";\nconst SCHEDULER_DATA_DIR = \"data/scheduler/logs\";\n\n// ── Host injection ────────────────────────────────────────────────\n\nexport interface SchedulerConfig {\n /** Absolute workspace root — state.json + logs hang off it. */\n workspaceRoot: string;\n /** Host atomic file writer (used with `uniqueTmp` for the state file). */\n writeFileAtomic: (filePath: string, content: string, opts: { uniqueTmp: boolean }) => Promise<void>;\n /** Optional logger. */\n log?: SchedulerLogger;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nlet config: SchedulerConfig | null = null;\n\n/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */\nexport function configureScheduler(injected: SchedulerConfig): void {\n config = injected;\n}\n\nfunction requireConfig(): SchedulerConfig {\n if (!config) throw new Error(\"scheduler: configureScheduler() not called\");\n return config;\n}\n\nfunction logger(): SchedulerLogger {\n return config?.log ?? NOOP_LOG;\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n// ── Paths ─────────────────────────────────────────────────────────\n\nfunction stateFilePath(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, \"state.json\");\n}\n\nfunction logsDir(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);\n}\n\n// ── I/O deps (real filesystem) ────────────────────────────────────\n\nfunction stateDeps(): StateDeps {\n return {\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n writeFileAtomic: (filePath: string, content: string) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),\n exists: existsSync,\n };\n}\n\nconst logDeps: LogDeps = {\n appendFile: (filePath: string, content: string) => appendFile(filePath, content),\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n exists: existsSync,\n ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),\n};\n\n// ── System task registry ──────────────────────────────────────────\n\nexport interface SystemTaskDef {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;\n run: () => Promise<void>;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nlet stateMap: StateMap = new Map();\nconst systemTasks: SystemTaskDef[] = [];\nlet taskManagerRef: ITaskManager | null = null;\n\n/**\n * Initialize the scheduler adapter. Call once at server startup AFTER the\n * task-manager is created but BEFORE `taskManager.start()`.\n */\nexport async function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void> {\n await mkdir(path.dirname(stateFilePath()), { recursive: true });\n await mkdir(logsDir(), { recursive: true });\n\n stateMap = await loadState(stateFilePath(), stateDeps());\n systemTasks.length = 0;\n systemTasks.push(...tasks);\n taskManagerRef = taskManager;\n\n // Run catch-up\n const catchUpTasks: CatchUpTask[] = tasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n schedule: toCoreSchedule(taskDef.schedule),\n missedRunPolicy: taskDef.missedRunPolicy,\n enabled: true,\n }));\n const plan = computeCatchUpPlan(catchUpTasks, stateMap, Date.now());\n\n for (const skip of plan.skipped) {\n logger().info(\"catch-up skipped\", { taskId: skip.taskId, windows: skip.windowCount });\n await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });\n }\n\n if (plan.runs.length > 0) {\n logger().info(\"catch-up enqueued\", { runs: plan.runs.length });\n for (const run of plan.runs) {\n const task = tasks.find((taskDef) => taskDef.id === run.taskId);\n if (!task) continue;\n await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);\n }\n }\n\n // Register with task-manager for ongoing ticks\n for (const task of tasks) {\n taskManager.registerTask({\n id: task.id,\n description: task.description,\n schedule: task.schedule,\n run: async () => {\n const windowIso = computeCurrentWindow(task);\n await executeAndLog(task, windowIso, TASK_TRIGGERS.scheduled);\n },\n });\n }\n\n logger().info(\"initialized\", { tasks: tasks.map((taskDef) => taskDef.id), stateEntries: stateMap.size });\n}\n\n/** Apply a schedule override to a running system task. Updates the\n * in-memory task definition, the task-manager, and recalculates\n * nextScheduledAt in persisted state. */\nexport async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef[\"schedule\"]): Promise<boolean> {\n const task = systemTasks.find((taskDef) => taskDef.id === taskId);\n if (!task || !taskManagerRef) return false;\n if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;\n task.schedule = schedule;\n\n // Recalculate next window so the UI reflects the new schedule\n const nextScheduledAt = computeNextScheduled(task);\n await safeUpdateState(taskId, { nextScheduledAt });\n\n return true;\n}\n\n/** Query execution logs — used by API routes. */\nexport async function getSchedulerLogs(opts: { since?: string; taskId?: string; limit?: number }): Promise<TaskLogEntry[]> {\n return queryLog(logsDir(), opts, logDeps);\n}\n\n/** Get all task states — used by API routes. */\nexport function getSchedulerTasks(): {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: string;\n state: TaskExecutionState;\n}[] {\n return systemTasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n description: taskDef.description,\n schedule: taskDef.schedule,\n missedRunPolicy: taskDef.missedRunPolicy,\n state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),\n }));\n}\n\n/** Test-only: clear config + in-memory state. */\nexport function resetSchedulerForTesting(): void {\n config = null;\n stateMap = new Map();\n systemTasks.length = 0;\n taskManagerRef = null;\n}\n\n// ── Internal ──────────────────────────────────────────────────────\n\nasync function executeAndLog(task: SystemTaskDef, scheduledFor: string, trigger: TaskTrigger): Promise<void> {\n const startedAt = new Date().toISOString();\n const startMs = Date.now();\n let errMsg: string | null = null;\n try {\n await task.run();\n } catch (err) {\n errMsg = errorMessage(err);\n logger().error(\"task failed\", { taskId: task.id, error: errMsg });\n }\n const durationMs = Date.now() - startMs;\n // Persistence is best-effort — never let disk failures propagate to the\n // tick loop or abort startup catch-up.\n await safePersist(task, scheduledFor, startedAt, durationMs, trigger, errMsg);\n}\n\n/** Best-effort persistence — state and log are independent. A failure in\n * one does not block the other, and neither propagates upward. */\nasync function safePersist(\n task: SystemTaskDef,\n scheduledFor: string,\n startedAt: string,\n durationMs: number,\n trigger: TaskTrigger,\n errMsg: string | null,\n): Promise<void> {\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(task.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n task.id,\n {\n lastRunAt: scheduledFor,\n lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n lastRunDurationMs: durationMs,\n lastErrorMessage: errMsg,\n consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,\n totalRuns: (currentState?.totalRuns ?? 0) + 1,\n nextScheduledAt: computeNextScheduled(task),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: task.id, error: String(err) });\n }\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: task.id,\n taskName: task.name,\n scheduledFor,\n startedAt,\n completedAt: new Date().toISOString(),\n result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n durationMs,\n trigger,\n ...(errMsg !== null && { errorMessage: errMsg }),\n },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: task.id, error: String(err) });\n }\n}\n\n/** Safe state update — swallows errors. */\nasync function safeUpdateState(taskId: string, patch: Partial<TaskExecutionState>): Promise<void> {\n try {\n await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());\n } catch (err) {\n logger().warn(\"state update failed\", { taskId, error: String(err) });\n }\n}\n\n/** Compute the window boundary that the current tick belongs to. For\n * scheduled runs, this is the epoch-aligned window — not the wall-clock\n * time of execution. This keeps lastRunAt consistent with catch-up's\n * window-based accounting. */\nfunction computeCurrentWindow(task: SystemTaskDef): string {\n const coreSchedule = toCoreSchedule(task.schedule);\n // The window that just fired is the latest one at or before now.\n const nowMs = Date.now();\n const windowMs = nextWindowAfter(coreSchedule, nowMs - (coreSchedule.type === SCHEDULE_TYPES.interval ? coreSchedule.intervalSec * ONE_SECOND_MS : 0));\n return windowMs !== null && windowMs <= nowMs ? new Date(windowMs).toISOString() : new Date(nowMs).toISOString();\n}\n\nfunction computeNextScheduled(task: SystemTaskDef): string | null {\n const coreSchedule = toCoreSchedule(task.schedule);\n const next = nextWindowAfter(coreSchedule, Date.now() + 1);\n return next !== null ? new Date(next).toISOString() : null;\n}\n\nfunction toCoreSchedule(schedule: TaskDefinition[\"schedule\"]): TaskSchedule {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return {\n type: SCHEDULE_TYPES.interval,\n intervalSec: Math.round(schedule.intervalMs / ONE_SECOND_MS),\n };\n }\n return schedule;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SCHEDULE_TYPES } from '@receptron/task-scheduler';
|
|
2
|
+
/** Minimal logger the engine logs through. Absent one, runs silent. */
|
|
3
|
+
export interface SchedulerLogger {
|
|
4
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
5
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
6
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
7
|
+
}
|
|
8
|
+
export type TaskSchedule = {
|
|
9
|
+
type: typeof SCHEDULE_TYPES.interval;
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: typeof SCHEDULE_TYPES.daily;
|
|
13
|
+
time: string;
|
|
14
|
+
};
|
|
15
|
+
export interface TaskRunContext {
|
|
16
|
+
taskId: string;
|
|
17
|
+
now: Date;
|
|
18
|
+
}
|
|
19
|
+
export interface TaskDefinition {
|
|
20
|
+
id: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
schedule: TaskSchedule;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** If set, this task only fires after the named task has completed
|
|
25
|
+
* successfully in the current tick cycle. Enforces ordering like
|
|
26
|
+
* "news fetch → journal → memory extraction". */
|
|
27
|
+
dependsOn?: string;
|
|
28
|
+
run: (ctx: TaskRunContext) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface ITaskManager {
|
|
31
|
+
registerTask: (def: TaskDefinition) => void;
|
|
32
|
+
removeTask: (taskId: string) => void;
|
|
33
|
+
/** Update the schedule of an existing task. Returns false if not found. */
|
|
34
|
+
updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;
|
|
35
|
+
start: () => void;
|
|
36
|
+
stop: () => void;
|
|
37
|
+
/** Run one tick manually (for testing). */
|
|
38
|
+
tick: () => Promise<void>;
|
|
39
|
+
listTasks: () => {
|
|
40
|
+
id: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
schedule: TaskSchedule;
|
|
43
|
+
dependsOn?: string;
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
export interface TaskManagerOptions {
|
|
47
|
+
tickMs?: number;
|
|
48
|
+
now?: () => Date;
|
|
49
|
+
log?: SchedulerLogger;
|
|
50
|
+
}
|
|
51
|
+
export declare function createTaskManager(options?: TaskManagerOptions): ITaskManager;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/whisper/client.ts
|
|
3
|
+
var LOCALE_TO_WHISPER = {
|
|
4
|
+
en: "en",
|
|
5
|
+
ja: "ja",
|
|
6
|
+
zh: "zh",
|
|
7
|
+
ko: "ko",
|
|
8
|
+
es: "es",
|
|
9
|
+
"pt-BR": "pt",
|
|
10
|
+
fr: "fr",
|
|
11
|
+
de: "de"
|
|
12
|
+
};
|
|
13
|
+
function localeToWhisperLanguage(locale) {
|
|
14
|
+
return LOCALE_TO_WHISPER[locale] ?? "auto";
|
|
15
|
+
}
|
|
16
|
+
var SPEECH_RMS = .015;
|
|
17
|
+
var SILENCE_MS = 800;
|
|
18
|
+
var MAX_SEGMENT_MS = 2e4;
|
|
19
|
+
var MONITOR_INTERVAL_MS = 100;
|
|
20
|
+
var AVAILABILITY_POLL_MS = 2e3;
|
|
21
|
+
function pickRecorderMime() {
|
|
22
|
+
const candidates = [
|
|
23
|
+
"audio/webm;codecs=opus",
|
|
24
|
+
"audio/webm",
|
|
25
|
+
"audio/mp4"
|
|
26
|
+
];
|
|
27
|
+
if (typeof MediaRecorder === "undefined") return void 0;
|
|
28
|
+
return candidates.find((type) => MediaRecorder.isTypeSupported(type));
|
|
29
|
+
}
|
|
30
|
+
function blobToDataUrl(blob) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const reader = new FileReader();
|
|
33
|
+
reader.onload = () => resolve(String(reader.result));
|
|
34
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("FileReader failed"));
|
|
35
|
+
reader.readAsDataURL(blob);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function computeRms(buffer) {
|
|
39
|
+
let sum = 0;
|
|
40
|
+
for (const sample of buffer) sum += sample * sample;
|
|
41
|
+
return Math.sqrt(sum / buffer.length);
|
|
42
|
+
}
|
|
43
|
+
function toMessage(err) {
|
|
44
|
+
return err instanceof Error ? err.message : String(err);
|
|
45
|
+
}
|
|
46
|
+
function createVoiceCapture(transport, language, callbacks) {
|
|
47
|
+
let available = false;
|
|
48
|
+
let listening = false;
|
|
49
|
+
let transcribing = false;
|
|
50
|
+
function emit() {
|
|
51
|
+
callbacks.onState?.({
|
|
52
|
+
available,
|
|
53
|
+
listening,
|
|
54
|
+
transcribing
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function setAvailable(value) {
|
|
58
|
+
if (available !== value) {
|
|
59
|
+
available = value;
|
|
60
|
+
emit();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function setListening(value) {
|
|
64
|
+
if (listening !== value) {
|
|
65
|
+
listening = value;
|
|
66
|
+
emit();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let stream = null;
|
|
70
|
+
let audioCtx = null;
|
|
71
|
+
let analyser = null;
|
|
72
|
+
let vadBuffer = new Float32Array(0);
|
|
73
|
+
let monitorHandle = null;
|
|
74
|
+
let recorder = null;
|
|
75
|
+
let chunks = [];
|
|
76
|
+
let mimeType = "";
|
|
77
|
+
let segmentHasSpeech = false;
|
|
78
|
+
let silenceStart = null;
|
|
79
|
+
let segmentStart = 0;
|
|
80
|
+
let pending = 0;
|
|
81
|
+
let queue = Promise.resolve();
|
|
82
|
+
let availabilityPollHandle = null;
|
|
83
|
+
let generation = 0;
|
|
84
|
+
let segmentGeneration = 0;
|
|
85
|
+
let startInFlight = false;
|
|
86
|
+
function setPending(delta) {
|
|
87
|
+
pending += delta;
|
|
88
|
+
const next = pending > 0;
|
|
89
|
+
if (transcribing !== next) {
|
|
90
|
+
transcribing = next;
|
|
91
|
+
emit();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function stopAvailabilityPoll() {
|
|
95
|
+
if (availabilityPollHandle !== null) {
|
|
96
|
+
window.clearInterval(availabilityPollHandle);
|
|
97
|
+
availabilityPollHandle = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function refreshAvailability() {
|
|
101
|
+
let status;
|
|
102
|
+
try {
|
|
103
|
+
status = await transport.getStatus();
|
|
104
|
+
} catch {
|
|
105
|
+
setAvailable(false);
|
|
106
|
+
stopAvailabilityPoll();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
setAvailable(status.ready);
|
|
110
|
+
if (status.downloading) {
|
|
111
|
+
if (availabilityPollHandle === null) availabilityPollHandle = window.setInterval(() => {
|
|
112
|
+
refreshAvailability();
|
|
113
|
+
}, AVAILABILITY_POLL_MS);
|
|
114
|
+
} else stopAvailabilityPoll();
|
|
115
|
+
}
|
|
116
|
+
async function sendSegment(blob, gen) {
|
|
117
|
+
if (gen !== generation) return;
|
|
118
|
+
try {
|
|
119
|
+
const dataUrl = await blobToDataUrl(blob);
|
|
120
|
+
const result = await transport.transcribe(dataUrl, language());
|
|
121
|
+
if (gen !== generation) return;
|
|
122
|
+
const text = result.text.trim();
|
|
123
|
+
if (text.length === 0) callbacks.onEmpty?.();
|
|
124
|
+
else callbacks.onTranscript(text);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (gen === generation) callbacks.onError?.(toMessage(err));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function enqueue(blob, gen) {
|
|
130
|
+
setPending(1);
|
|
131
|
+
queue = queue.then(() => sendSegment(blob, gen)).catch(() => void 0).finally(() => setPending(-1));
|
|
132
|
+
}
|
|
133
|
+
function containerType() {
|
|
134
|
+
return mimeType.split(";")[0] || "audio/webm";
|
|
135
|
+
}
|
|
136
|
+
function onSegmentStop() {
|
|
137
|
+
const hadSpeech = segmentHasSpeech;
|
|
138
|
+
const gen = segmentGeneration;
|
|
139
|
+
const blob = new Blob(chunks, { type: containerType() });
|
|
140
|
+
if (listening) startRecorder();
|
|
141
|
+
if (hadSpeech && blob.size > 0 && gen === generation) enqueue(blob, gen);
|
|
142
|
+
}
|
|
143
|
+
function startRecorder() {
|
|
144
|
+
if (!stream) return;
|
|
145
|
+
chunks = [];
|
|
146
|
+
segmentHasSpeech = false;
|
|
147
|
+
silenceStart = null;
|
|
148
|
+
segmentStart = Date.now();
|
|
149
|
+
segmentGeneration = generation;
|
|
150
|
+
recorder = new MediaRecorder(stream, { mimeType });
|
|
151
|
+
recorder.ondataavailable = (event) => {
|
|
152
|
+
if (event.data.size > 0) chunks.push(event.data);
|
|
153
|
+
};
|
|
154
|
+
recorder.onstop = onSegmentStop;
|
|
155
|
+
recorder.start();
|
|
156
|
+
}
|
|
157
|
+
function cutSegment() {
|
|
158
|
+
if (recorder && recorder.state === "recording") recorder.stop();
|
|
159
|
+
}
|
|
160
|
+
function monitorTick() {
|
|
161
|
+
if (!analyser) return;
|
|
162
|
+
analyser.getFloatTimeDomainData(vadBuffer);
|
|
163
|
+
const rms = computeRms(vadBuffer);
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
if (rms > SPEECH_RMS) {
|
|
166
|
+
segmentHasSpeech = true;
|
|
167
|
+
silenceStart = null;
|
|
168
|
+
} else if (segmentHasSpeech) {
|
|
169
|
+
if (silenceStart === null) silenceStart = now;
|
|
170
|
+
else if (now - silenceStart > SILENCE_MS) cutSegment();
|
|
171
|
+
}
|
|
172
|
+
if (segmentHasSpeech && now - segmentStart > MAX_SEGMENT_MS) cutSegment();
|
|
173
|
+
}
|
|
174
|
+
async function start() {
|
|
175
|
+
if (startInFlight || listening) return true;
|
|
176
|
+
startInFlight = true;
|
|
177
|
+
const startGen = generation;
|
|
178
|
+
try {
|
|
179
|
+
mimeType = pickRecorderMime() ?? "";
|
|
180
|
+
if (!mimeType || !navigator.mediaDevices?.getUserMedia) {
|
|
181
|
+
callbacks.onError?.("unsupported");
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
let acquired;
|
|
185
|
+
try {
|
|
186
|
+
acquired = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
187
|
+
} catch {
|
|
188
|
+
callbacks.onError?.("permission-denied");
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (startGen !== generation) {
|
|
192
|
+
acquired.getTracks().forEach((track) => track.stop());
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
stream = acquired;
|
|
196
|
+
audioCtx = new AudioContext();
|
|
197
|
+
analyser = audioCtx.createAnalyser();
|
|
198
|
+
analyser.fftSize = 2048;
|
|
199
|
+
vadBuffer = new Float32Array(analyser.fftSize);
|
|
200
|
+
audioCtx.createMediaStreamSource(stream).connect(analyser);
|
|
201
|
+
setListening(true);
|
|
202
|
+
startRecorder();
|
|
203
|
+
monitorHandle = window.setInterval(monitorTick, MONITOR_INTERVAL_MS);
|
|
204
|
+
return true;
|
|
205
|
+
} finally {
|
|
206
|
+
startInFlight = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function stop() {
|
|
210
|
+
generation += 1;
|
|
211
|
+
setListening(false);
|
|
212
|
+
if (monitorHandle !== null) {
|
|
213
|
+
window.clearInterval(monitorHandle);
|
|
214
|
+
monitorHandle = null;
|
|
215
|
+
}
|
|
216
|
+
if (recorder && recorder.state === "recording") recorder.stop();
|
|
217
|
+
recorder = null;
|
|
218
|
+
if (audioCtx) {
|
|
219
|
+
audioCtx.close().catch(() => void 0);
|
|
220
|
+
audioCtx = null;
|
|
221
|
+
}
|
|
222
|
+
analyser = null;
|
|
223
|
+
stream?.getTracks().forEach((track) => track.stop());
|
|
224
|
+
stream = null;
|
|
225
|
+
}
|
|
226
|
+
function dispose() {
|
|
227
|
+
stopAvailabilityPoll();
|
|
228
|
+
stop();
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
refreshAvailability,
|
|
232
|
+
start,
|
|
233
|
+
stop,
|
|
234
|
+
dispose
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
exports.createVoiceCapture = createVoiceCapture;
|
|
239
|
+
exports.localeToWhisperLanguage = localeToWhisperLanguage;
|
|
240
|
+
|
|
241
|
+
//# sourceMappingURL=client.cjs.map
|