zames_pro 2.32.0 → 2.35.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/dist/agent-loop.js +4 -1
- package/dist/browser.js +32 -1
- package/dist/commands.js +114 -4
- package/dist/i18n.js +96 -5
- package/dist/index.js +348 -30
- package/dist/input.js +98 -41
- package/dist/scheduler.js +185 -0
- package/dist/theme.js +1 -0
- package/package.json +1 -1
package/dist/input.js
CHANGED
|
@@ -205,26 +205,32 @@ export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
|
|
|
205
205
|
return '';
|
|
206
206
|
}
|
|
207
207
|
const n = Math.round(tokens);
|
|
208
|
-
|
|
209
|
-
if (n >= 1_000_000) {
|
|
210
|
-
const m = n / 1_000_000;
|
|
211
|
-
compact = (Number.isInteger(m) ? String(m) : m.toFixed(1)) + 'M';
|
|
212
|
-
}
|
|
213
|
-
else if (n >= 1_000) {
|
|
214
|
-
const k = n / 1_000;
|
|
215
|
-
compact =
|
|
216
|
-
(k >= 100 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '')) +
|
|
217
|
-
'k';
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
compact = String(n);
|
|
221
|
-
}
|
|
208
|
+
const compact = formatCompactTokens(n);
|
|
222
209
|
const pct = Math.max(0, (n / limit) * 100);
|
|
223
210
|
const pctStr = pct >= 10 ? String(Math.round(pct)) : pct.toFixed(1);
|
|
224
211
|
// "ctx:" marks this as the USED context size, so the operator does not have
|
|
225
212
|
// to guess what "302k · 30%" means.
|
|
226
213
|
return 'ctx: ' + compact + ' · ' + pctStr + '%';
|
|
227
214
|
}
|
|
215
|
+
// Compact token count (10k, 125k, 1.5M). Exported so the per-task summary
|
|
216
|
+
// reuses the EXACT same "k/M" formatting as the context counter. Invalid
|
|
217
|
+
// input (null/NaN/negative) renders an empty string.
|
|
218
|
+
export function formatCompactTokens(tokens) {
|
|
219
|
+
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
const n = Math.round(tokens);
|
|
223
|
+
if (n >= 1_000_000) {
|
|
224
|
+
const m = n / 1_000_000;
|
|
225
|
+
return (Number.isInteger(m) ? String(m) : m.toFixed(1)) + 'M';
|
|
226
|
+
}
|
|
227
|
+
if (n >= 1_000) {
|
|
228
|
+
const k = n / 1_000;
|
|
229
|
+
return ((k >= 100 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '')) +
|
|
230
|
+
'k');
|
|
231
|
+
}
|
|
232
|
+
return String(n);
|
|
233
|
+
}
|
|
228
234
|
export class LineEditor {
|
|
229
235
|
promptStr;
|
|
230
236
|
buf;
|
|
@@ -251,6 +257,12 @@ export class LineEditor {
|
|
|
251
257
|
_showHint;
|
|
252
258
|
_dotPhase;
|
|
253
259
|
_thinkBase;
|
|
260
|
+
// Explicit lifecycle state of the send ('generating' | 'paused' |
|
|
261
|
+
// 'settled' | ''). The thinking spinner already shows ACTIVITY, but it does
|
|
262
|
+
// not tell a throttle pause from a real generation — a long reasoning turn
|
|
263
|
+
// and a wait for the rate-limit slot look identical. Only 'generating' shows
|
|
264
|
+
// a prefix (see _stateLabel); the field also gates the clean-up in stop().
|
|
265
|
+
_sendState;
|
|
254
266
|
// The last rendered status row(s) (status + right-aligned context). Cached so
|
|
255
267
|
// a re-render driven by TYPING (the buffer changed, the status did not) can
|
|
256
268
|
// skip erasing and re-printing the status line entirely. On terminals like
|
|
@@ -298,6 +310,13 @@ export class LineEditor {
|
|
|
298
310
|
contextStatus;
|
|
299
311
|
// The raw token count behind contextStatus (for the color level).
|
|
300
312
|
contextTokens;
|
|
313
|
+
// Number of messages waiting in the queue, shown as a small "⧗N" badge in
|
|
314
|
+
// the status line so the operator sees that typed-ahead messages are still
|
|
315
|
+
// pending. 0 hides it. Updated by the caller.
|
|
316
|
+
queueLength;
|
|
317
|
+
// A callback the editor calls before every render to read the CURRENT queue
|
|
318
|
+
// length, so the badge stays live without the caller polling.
|
|
319
|
+
onQueueQuery;
|
|
301
320
|
// Whether the DeepSeek "Deep thinking" / "Smart search" toggles are ON.
|
|
302
321
|
// Shown as two colored icons before the context counter: a dim gray icon
|
|
303
322
|
// means off, a teal-green icon means on.
|
|
@@ -333,6 +352,7 @@ export class LineEditor {
|
|
|
333
352
|
this._showHint = true;
|
|
334
353
|
this._dotPhase = 0;
|
|
335
354
|
this._thinkBase = '';
|
|
355
|
+
this._sendState = '';
|
|
336
356
|
this._lastStatusBlock = '';
|
|
337
357
|
this._wasRaw = false;
|
|
338
358
|
this._onData = (b) => this._handle(b);
|
|
@@ -375,6 +395,8 @@ export class LineEditor {
|
|
|
375
395
|
this.contextLimit = CONTEXT_LIMIT;
|
|
376
396
|
this.onToggleQuery = null;
|
|
377
397
|
this.onContextQuery = null;
|
|
398
|
+
this.queueLength = 0;
|
|
399
|
+
this.onQueueQuery = null;
|
|
378
400
|
}
|
|
379
401
|
// Refresh the toggle icon states from onToggleQuery (if wired) before a
|
|
380
402
|
// render. A broken callback must never break the render.
|
|
@@ -423,6 +445,23 @@ export class LineEditor {
|
|
|
423
445
|
return theme.warn(ctx);
|
|
424
446
|
return theme.success(ctx);
|
|
425
447
|
}
|
|
448
|
+
// The queue badge ("⧗N") shown before the context counter when messages are
|
|
449
|
+
// waiting. Returns '' when the queue is empty, so it never clutters an idle
|
|
450
|
+
// status line. Refreshed from onQueueQuery before every render when wired.
|
|
451
|
+
_queueBadge() {
|
|
452
|
+
if (this.onQueueQuery) {
|
|
453
|
+
try {
|
|
454
|
+
const n = this.onQueueQuery();
|
|
455
|
+
this.queueLength = typeof n === 'number' && n > 0 ? n : 0;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// A broken callback must never break the render.
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (this.queueLength <= 0)
|
|
462
|
+
return '';
|
|
463
|
+
return theme.warn('⧗' + this.queueLength) + ' ';
|
|
464
|
+
}
|
|
426
465
|
// Icons for the DeepSeek toggles: 🧠 "Deep thinking" and 🌐 "Smart search",
|
|
427
466
|
// shown before the context counter. Only the ENABLED toggles are shown, so
|
|
428
467
|
// the state is unambiguous WITHOUT relying on the terminal coloring the
|
|
@@ -658,7 +697,7 @@ export class LineEditor {
|
|
|
658
697
|
// but the icons are still meaningful.
|
|
659
698
|
const ctxRaw = this._contextText();
|
|
660
699
|
this._refreshToggles();
|
|
661
|
-
const ctxText = this._toggleIcons() + ctxRaw;
|
|
700
|
+
const ctxText = this._queueBadge() + this._toggleIcons() + ctxRaw;
|
|
662
701
|
let statusOut = '';
|
|
663
702
|
let top = 0;
|
|
664
703
|
// Never fill a row to the FULL terminal width: on terminals with
|
|
@@ -668,31 +707,38 @@ export class LineEditor {
|
|
|
668
707
|
// free (effective width = cols - 1) for the right-aligned context and for
|
|
669
708
|
// the padded status row.
|
|
670
709
|
const usable = Math.max(1, cols - 1);
|
|
671
|
-
|
|
710
|
+
// An explicit lifecycle phase (generating/settled) is prefixed to the
|
|
711
|
+
// animated status, so the operator can tell a real generation from a
|
|
712
|
+
// quiet spinner. Empty when no phase is active (tool running, idle).
|
|
713
|
+
const stateLabel = this._stateLabel();
|
|
714
|
+
const statusText = stateLabel
|
|
715
|
+
? stateLabel + ' ' + this.statusText
|
|
716
|
+
: this.statusText;
|
|
717
|
+
if (statusText) {
|
|
672
718
|
if (ctxText) {
|
|
673
719
|
// Right-align the context on the SAME row as the status. When the
|
|
674
720
|
// status is too long to leave room, do NOT cram them together (that
|
|
675
721
|
// pushed the trailing `%` past the right edge and it got truncated):
|
|
676
722
|
// put the context on its own line instead.
|
|
677
|
-
const space = usable - visLen(
|
|
723
|
+
const space = usable - visLen(statusText) - visLen(ctxText);
|
|
678
724
|
if (space >= 2) {
|
|
679
|
-
const row =
|
|
725
|
+
const row = statusText + ' '.repeat(space) + ctxText;
|
|
680
726
|
statusOut = row + NL;
|
|
681
727
|
top = visRows(row, cols);
|
|
682
728
|
}
|
|
683
729
|
else {
|
|
684
|
-
statusOut =
|
|
685
|
-
top = visRows(
|
|
730
|
+
statusOut = statusText + NL;
|
|
731
|
+
top = visRows(statusText, cols);
|
|
686
732
|
const ctxRow = ' '.repeat(Math.max(0, usable - visLen(ctxText))) + ctxText;
|
|
687
733
|
statusOut += ctxRow + NL;
|
|
688
734
|
top += visRows(ctxRow, cols);
|
|
689
735
|
}
|
|
690
736
|
}
|
|
691
737
|
else {
|
|
692
|
-
statusOut =
|
|
738
|
+
statusOut = statusText + NL;
|
|
693
739
|
// The status may wrap onto several lines — we account for this,
|
|
694
740
|
// otherwise the block erase misses and statuses pile up.
|
|
695
|
-
top = visRows(
|
|
741
|
+
top = visRows(statusText, cols);
|
|
696
742
|
}
|
|
697
743
|
}
|
|
698
744
|
else if (ctxText) {
|
|
@@ -885,6 +931,28 @@ export class LineEditor {
|
|
|
885
931
|
return '';
|
|
886
932
|
return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
|
|
887
933
|
}
|
|
934
|
+
// The lifecycle phase prefix for the status line, colored by meaning:
|
|
935
|
+
// generating (brown). There is NO prefix for 'settled' (the answer block
|
|
936
|
+
// already marks completion) and none for 'paused' (the base status itself
|
|
937
|
+
// reads "пауза Ns"). Returns '' when no phase is active, so an idle status
|
|
938
|
+
// is not prefixed.
|
|
939
|
+
_stateLabel() {
|
|
940
|
+
if (this._sendState === 'generating')
|
|
941
|
+
return (theme.brown(translate(this.locale)('state.generating')) + theme.dim(':'));
|
|
942
|
+
return '';
|
|
943
|
+
}
|
|
944
|
+
// Called by the browser layer at real lifecycle points: right before a
|
|
945
|
+
// send (generating), during a throttle/Continue pause (paused), and after
|
|
946
|
+
// the answer settled (settled). Any other value clears the phase.
|
|
947
|
+
setSendState(state) {
|
|
948
|
+
const next = state === 'generating' || state === 'paused' || state === 'settled'
|
|
949
|
+
? state
|
|
950
|
+
: '';
|
|
951
|
+
if (next === this._sendState)
|
|
952
|
+
return;
|
|
953
|
+
this._sendState = next;
|
|
954
|
+
this._render();
|
|
955
|
+
}
|
|
888
956
|
_startThinking() {
|
|
889
957
|
if (this.pendingText) {
|
|
890
958
|
this.setStatus(theme.prompt('✎ ') + this.pendingText + this._hint());
|
|
@@ -945,6 +1013,9 @@ export class LineEditor {
|
|
|
945
1013
|
}
|
|
946
1014
|
stop() {
|
|
947
1015
|
this._stopDots();
|
|
1016
|
+
// A finished send must not keep prefixing the NEXT status (e.g. a tool
|
|
1017
|
+
// that starts right after the answer) with a stale "settled"/"generating".
|
|
1018
|
+
this._sendState = '';
|
|
948
1019
|
this.setStatus('');
|
|
949
1020
|
}
|
|
950
1021
|
toolCall(name, args) {
|
|
@@ -968,30 +1039,16 @@ export class LineEditor {
|
|
|
968
1039
|
assistant(msg) {
|
|
969
1040
|
this.stop();
|
|
970
1041
|
const rendered = renderMarkdown(msg);
|
|
971
|
-
//
|
|
972
|
-
//
|
|
973
|
-
//
|
|
974
|
-
//
|
|
975
|
-
// under the answer divider.
|
|
976
|
-
let ctxLine = '';
|
|
977
|
-
if (this.onContextQuery) {
|
|
978
|
-
try {
|
|
979
|
-
const n = this.onContextQuery();
|
|
980
|
-
const txt = formatTokenStatus(n, this.contextLimit);
|
|
981
|
-
if (txt)
|
|
982
|
-
ctxLine = NL + theme.dim(txt);
|
|
983
|
-
}
|
|
984
|
-
catch {
|
|
985
|
-
// never break the render
|
|
986
|
-
}
|
|
987
|
-
}
|
|
1042
|
+
// The used context is NOT repeated under the answer: `_writeBlock` keeps
|
|
1043
|
+
// showing it right-aligned in the status line even after `stop()` clears
|
|
1044
|
+
// the spinner text (the `else if (ctxText)` branch), so printing it here
|
|
1045
|
+
// again was a duplicate of the same `accumulated_token_usage` number.
|
|
988
1046
|
this.printAbove(NL +
|
|
989
1047
|
theme.assistant(translate(this.locale)('editor.answer')) +
|
|
990
1048
|
NL +
|
|
991
1049
|
rendered +
|
|
992
1050
|
NL +
|
|
993
|
-
theme.dim('─'.repeat(60))
|
|
994
|
-
ctxLine);
|
|
1051
|
+
theme.dim('─'.repeat(60)));
|
|
995
1052
|
}
|
|
996
1053
|
// Warning to the operator (e.g. the agent stopped suspiciously).
|
|
997
1054
|
// Printed above the input line without overwriting it.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Pure scheduling logic for the /loop and /cron commands. Kept free of any
|
|
2
|
+
// browser/terminal dependency so it can be unit-tested; index.ts owns the
|
|
3
|
+
// 1-second ticker and the actual enqueue into the message queue.
|
|
4
|
+
// Parse a human interval like "30s", "5m", "2h" or a bare number of seconds.
|
|
5
|
+
// Returns the period in ms, or null when the text is not a valid interval.
|
|
6
|
+
export function parseInterval(text) {
|
|
7
|
+
const m = /^(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hour|hours)?$/i.exec(String(text ?? '').trim());
|
|
8
|
+
if (!m)
|
|
9
|
+
return null;
|
|
10
|
+
const n = Number(m[1]);
|
|
11
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
12
|
+
return null;
|
|
13
|
+
const unit = (m[2] || 's').toLowerCase();
|
|
14
|
+
const mult = unit.startsWith('h')
|
|
15
|
+
? 3_600_000
|
|
16
|
+
: unit.startsWith('m')
|
|
17
|
+
? 60_000
|
|
18
|
+
: 1000;
|
|
19
|
+
return n * mult;
|
|
20
|
+
}
|
|
21
|
+
// Render an interval back to the shortest human form (used in listings).
|
|
22
|
+
export function formatInterval(ms) {
|
|
23
|
+
if (ms % 3_600_000 === 0)
|
|
24
|
+
return ms / 3_600_000 + 'h';
|
|
25
|
+
if (ms % 60_000 === 0)
|
|
26
|
+
return ms / 60_000 + 'm';
|
|
27
|
+
if (ms % 1000 === 0)
|
|
28
|
+
return ms / 1000 + 's';
|
|
29
|
+
return ms + 'ms';
|
|
30
|
+
}
|
|
31
|
+
// Parse one cron field ("*", "a-b", "*/n", "a,b", "a-b/n") into a value set.
|
|
32
|
+
function parseCronField(field, min, max) {
|
|
33
|
+
const out = new Set();
|
|
34
|
+
for (const part of field.split(',')) {
|
|
35
|
+
const [range, stepStr] = part.split('/');
|
|
36
|
+
const step = stepStr === undefined ? 1 : Number(stepStr);
|
|
37
|
+
if (!Number.isInteger(step) || step <= 0)
|
|
38
|
+
return null;
|
|
39
|
+
let lo;
|
|
40
|
+
let hi;
|
|
41
|
+
if (range === '*') {
|
|
42
|
+
lo = min;
|
|
43
|
+
hi = max;
|
|
44
|
+
}
|
|
45
|
+
else if (range.includes('-')) {
|
|
46
|
+
const [a, b] = range.split('-');
|
|
47
|
+
lo = Number(a);
|
|
48
|
+
hi = Number(b);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
lo = Number(range);
|
|
52
|
+
hi = lo;
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isInteger(lo) || !Number.isInteger(hi))
|
|
55
|
+
return null;
|
|
56
|
+
if (lo < min || hi > max || lo > hi)
|
|
57
|
+
return null;
|
|
58
|
+
for (let v = lo; v <= hi; v += step)
|
|
59
|
+
out.add(v);
|
|
60
|
+
}
|
|
61
|
+
return out.size ? out : null;
|
|
62
|
+
}
|
|
63
|
+
// Parse a 5-field cron expression: minute hour day-of-month month day-of-week.
|
|
64
|
+
// Day-of-week accepts 0-7 (both 0 and 7 mean Sunday). Returns null on a bad
|
|
65
|
+
// expression so the command can report it instead of scheduling silently.
|
|
66
|
+
export function parseCron(expr) {
|
|
67
|
+
const parts = String(expr ?? '')
|
|
68
|
+
.trim()
|
|
69
|
+
.split(/\s+/);
|
|
70
|
+
if (parts.length !== 5)
|
|
71
|
+
return null;
|
|
72
|
+
const minute = parseCronField(parts[0], 0, 59);
|
|
73
|
+
const hour = parseCronField(parts[1], 0, 23);
|
|
74
|
+
const dom = parseCronField(parts[2], 1, 31);
|
|
75
|
+
const month = parseCronField(parts[3], 1, 12);
|
|
76
|
+
const dowRaw = parseCronField(parts[4], 0, 7);
|
|
77
|
+
if (!minute || !hour || !dom || !month || !dowRaw)
|
|
78
|
+
return null;
|
|
79
|
+
const dow = new Set();
|
|
80
|
+
for (const d of dowRaw)
|
|
81
|
+
dow.add(d === 7 ? 0 : d);
|
|
82
|
+
return { minute, hour, dom, month, dow };
|
|
83
|
+
}
|
|
84
|
+
// Does the given Date match the cron expression? dom and dow are ANDed (the
|
|
85
|
+
// common, predictable behavior); to schedule "every day" use `*` for one.
|
|
86
|
+
export function cronMatches(expr, date) {
|
|
87
|
+
const c = parseCron(expr);
|
|
88
|
+
if (!c)
|
|
89
|
+
return false;
|
|
90
|
+
return (c.minute.has(date.getMinutes()) &&
|
|
91
|
+
c.hour.has(date.getHours()) &&
|
|
92
|
+
c.dom.has(date.getDate()) &&
|
|
93
|
+
c.month.has(date.getMonth() + 1) &&
|
|
94
|
+
c.dow.has(date.getDay()));
|
|
95
|
+
}
|
|
96
|
+
// Next epoch ms at which the expression fires, strictly after `fromMs`. Scans
|
|
97
|
+
// minute by minute (bounded to ~4 years); a one-time cost when a job is
|
|
98
|
+
// created/rescheduled, and the field checks reject almost every minute fast.
|
|
99
|
+
export function nextCronTime(expr, fromMs) {
|
|
100
|
+
const c = parseCron(expr);
|
|
101
|
+
if (!c)
|
|
102
|
+
return null;
|
|
103
|
+
const d = new Date(fromMs);
|
|
104
|
+
d.setSeconds(0, 0);
|
|
105
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
106
|
+
const limit = fromMs + 4 * 366 * 24 * 60 * 60 * 1000;
|
|
107
|
+
while (d.getTime() <= limit) {
|
|
108
|
+
if (cronMatches(expr, d))
|
|
109
|
+
return d.getTime();
|
|
110
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// Holds the scheduled jobs. Pure: no timers here. index.ts drives it with a
|
|
115
|
+
// 1-second tick and enqueues the tasks that `due()` returns.
|
|
116
|
+
export class Scheduler {
|
|
117
|
+
jobs = [];
|
|
118
|
+
nextId = 1;
|
|
119
|
+
addLoop(intervalMs, task, now) {
|
|
120
|
+
const job = {
|
|
121
|
+
id: this.nextId++,
|
|
122
|
+
kind: 'loop',
|
|
123
|
+
task,
|
|
124
|
+
intervalMs,
|
|
125
|
+
nextAt: now + intervalMs,
|
|
126
|
+
createdAt: now,
|
|
127
|
+
};
|
|
128
|
+
this.jobs.push(job);
|
|
129
|
+
return job;
|
|
130
|
+
}
|
|
131
|
+
addCron(cron, task, now) {
|
|
132
|
+
const nextAt = nextCronTime(cron, now);
|
|
133
|
+
if (nextAt === null)
|
|
134
|
+
return null;
|
|
135
|
+
const job = {
|
|
136
|
+
id: this.nextId++,
|
|
137
|
+
kind: 'cron',
|
|
138
|
+
task,
|
|
139
|
+
cron,
|
|
140
|
+
nextAt,
|
|
141
|
+
createdAt: now,
|
|
142
|
+
};
|
|
143
|
+
this.jobs.push(job);
|
|
144
|
+
return job;
|
|
145
|
+
}
|
|
146
|
+
remove(id) {
|
|
147
|
+
const i = this.jobs.findIndex((j) => j.id === id);
|
|
148
|
+
if (i === -1)
|
|
149
|
+
return false;
|
|
150
|
+
this.jobs.splice(i, 1);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
clear() {
|
|
154
|
+
const n = this.jobs.length;
|
|
155
|
+
this.jobs = [];
|
|
156
|
+
return n;
|
|
157
|
+
}
|
|
158
|
+
list() {
|
|
159
|
+
return this.jobs.slice();
|
|
160
|
+
}
|
|
161
|
+
count() {
|
|
162
|
+
return this.jobs.length;
|
|
163
|
+
}
|
|
164
|
+
// Jobs whose nextAt is due at `now`, rescheduled for their NEXT fire so a
|
|
165
|
+
// single tick never fires the same job twice. A loop that fell behind is
|
|
166
|
+
// advanced past `now` in one step (missed fires are coalesced, not queued
|
|
167
|
+
// many times).
|
|
168
|
+
due(now) {
|
|
169
|
+
const fired = [];
|
|
170
|
+
for (const job of this.jobs) {
|
|
171
|
+
if (job.nextAt > now)
|
|
172
|
+
continue;
|
|
173
|
+
fired.push({ ...job });
|
|
174
|
+
if (job.kind === 'loop' && job.intervalMs && job.intervalMs > 0) {
|
|
175
|
+
const missed = Math.floor((now - job.nextAt) / job.intervalMs);
|
|
176
|
+
job.nextAt = job.nextAt + (missed + 1) * job.intervalMs;
|
|
177
|
+
}
|
|
178
|
+
else if (job.cron) {
|
|
179
|
+
const nxt = nextCronTime(job.cron, now);
|
|
180
|
+
job.nextAt = nxt === null ? Number.POSITIVE_INFINITY : nxt;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return fired;
|
|
184
|
+
}
|
|
185
|
+
}
|
package/dist/theme.js
CHANGED
|
@@ -23,6 +23,7 @@ export const theme = {
|
|
|
23
23
|
toolResult: chalk.hex('#8a9bb5'), // gray-blue
|
|
24
24
|
system: chalk.hex('#808896'), // neutral gray
|
|
25
25
|
dim: chalk.hex('#5b616e'), // dark gray
|
|
26
|
+
taskSummary: chalk.hex('#8fa3c8'), // per-task summary — noticeable but calm, distinct from spinner brown and warn ochre
|
|
26
27
|
warn: chalk.hex('#c9a86a'), // calm ochre
|
|
27
28
|
error: chalk.hex('#c98a80'), // muted terracotta
|
|
28
29
|
success: chalk.hex('#a9c08c'), // soft sage
|