@saadnvd1/agent-os 0.3.479 → 0.3.481
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/README.md +17 -1
- package/app/api/schedules/route.ts +1 -0
- package/components/Orchestrator/OrchestratorBar.tsx +12 -0
- package/components/Schedules/ScheduleForm.tsx +94 -12
- package/components/Schedules/SchedulesPanel.tsx +60 -14
- package/components/SessionList/SidebarHeader.tsx +1 -1
- package/components/SessionList/useSchedulesGlance.ts +6 -2
- package/lib/chat/drivers/demo.ts +2 -1
- package/lib/chat/test-touch.ts +58 -12
- package/lib/db/migrations.ts +14 -0
- package/lib/orchestrator/brief.ts +3 -0
- package/lib/orchestrator/reminders.ts +91 -0
- package/lib/orchestrator/serve.ts +9 -0
- package/lib/orchestrator/tool-names.ts +3 -0
- package/lib/orchestrator/tool-schemas.ts +17 -0
- package/lib/orchestrator/tools.ts +6 -0
- package/lib/schedules/badge.ts +11 -1
- package/lib/schedules/cron.ts +1 -1
- package/lib/schedules/index.ts +67 -6
- package/lib/schedules/run.ts +23 -6
- package/lib/schedules/scheduler.ts +12 -2
- package/lib/schedules/start.ts +11 -5
- package/lib/schedules/store.ts +133 -16
- package/lib/schedules/when.ts +166 -0
- package/package.json +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// One parser for "when" so the API, the CLI and the orchestrator's remind_me
|
|
2
|
+
// all read a time the same way: an ISO timestamp, "tomorrow 10am", "in 2h".
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_TIMEZONE, wall, wallToInstant } from "./cron";
|
|
5
|
+
|
|
6
|
+
const DAYS = [
|
|
7
|
+
"sunday",
|
|
8
|
+
"monday",
|
|
9
|
+
"tuesday",
|
|
10
|
+
"wednesday",
|
|
11
|
+
"thursday",
|
|
12
|
+
"friday",
|
|
13
|
+
"saturday",
|
|
14
|
+
];
|
|
15
|
+
const dayIndex = (word: string): number =>
|
|
16
|
+
DAYS.findIndex((d) => d === word || d.slice(0, 3) === word);
|
|
17
|
+
|
|
18
|
+
const bad = (text: string) =>
|
|
19
|
+
new Error(
|
|
20
|
+
`Can't read "${text}" as a time: try "tomorrow 10am", "in 2h" or 2026-10-12T10:00`
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const UNIT_MS: Record<string, number> = {
|
|
24
|
+
m: 60_000,
|
|
25
|
+
h: 3_600_000,
|
|
26
|
+
d: 86_400_000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function duration(text: string): number | null {
|
|
30
|
+
let total = 0;
|
|
31
|
+
const rest = text.replace(
|
|
32
|
+
/(\d+(?:\.\d+)?|an?)\s*(minutes?|mins?|m|hours?|hrs?|h|days?|d)\b/g,
|
|
33
|
+
(_, n: string, unit: string) => {
|
|
34
|
+
total += (/^an?$/.test(n) ? 1 : Number(n)) * UNIT_MS[unit[0]];
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
return total > 0 && /^(\s|,|and)*$/.test(rest) ? total : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clock(text: string): { h: number; mi: number } | null {
|
|
42
|
+
if (text === "noon") return { h: 12, mi: 0 };
|
|
43
|
+
if (text === "midnight") return { h: 0, mi: 0 };
|
|
44
|
+
const m = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/.exec(text);
|
|
45
|
+
if (!m) return null;
|
|
46
|
+
const mi = m[2] ? Number(m[2]) : 0;
|
|
47
|
+
let h = Number(m[1]);
|
|
48
|
+
if (mi > 59) return null;
|
|
49
|
+
if (m[3]) {
|
|
50
|
+
if (h < 1 || h > 12) return null;
|
|
51
|
+
h = (h % 12) + (m[3] === "pm" ? 12 : 0);
|
|
52
|
+
} else if (!m[2] || h > 23) {
|
|
53
|
+
// "tomorrow 10" is ambiguous; 24-hour times need their minutes.
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return { h, mi };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CLOCK = String.raw`noon|midnight|\d{1,2}(?::\d{2})?\s*(?:am|pm)?`;
|
|
60
|
+
const DAY = String.raw`(?:next\s+)?(?:today|tomorrow|tmrw|[a-z]{3,9})`;
|
|
61
|
+
const DAY_FIRST = new RegExp(`^(${DAY})\\s+(?:at\\s+)?(${CLOCK})$`);
|
|
62
|
+
const CLOCK_FIRST = new RegExp(`^(?:at\\s+)?(${CLOCK})(?:\\s+(${DAY}))?$`);
|
|
63
|
+
|
|
64
|
+
function onDay(
|
|
65
|
+
word: string | undefined,
|
|
66
|
+
at: { h: number; mi: number },
|
|
67
|
+
now: number,
|
|
68
|
+
tz: string
|
|
69
|
+
): number | null {
|
|
70
|
+
const today = wall(now, tz);
|
|
71
|
+
const instantIn = (days: number) => {
|
|
72
|
+
const d = new Date(Date.UTC(today.y, today.m - 1, today.d + days));
|
|
73
|
+
return wallToInstant(
|
|
74
|
+
d.getUTCFullYear(),
|
|
75
|
+
d.getUTCMonth() + 1,
|
|
76
|
+
d.getUTCDate(),
|
|
77
|
+
at.h,
|
|
78
|
+
at.mi,
|
|
79
|
+
tz
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
const w = word?.replace(/^next\s+/, "");
|
|
83
|
+
if (!w) return instantIn(0) > now ? instantIn(0) : instantIn(1);
|
|
84
|
+
if (w === "today") return instantIn(0);
|
|
85
|
+
if (w === "tomorrow" || w === "tmrw") return instantIn(1);
|
|
86
|
+
const dow = dayIndex(w);
|
|
87
|
+
if (dow < 0) return null;
|
|
88
|
+
const todayDow = new Date(
|
|
89
|
+
Date.UTC(today.y, today.m - 1, today.d)
|
|
90
|
+
).getUTCDay();
|
|
91
|
+
const delta = (dow - todayDow + 7) % 7;
|
|
92
|
+
return delta === 0 && instantIn(0) <= now ? instantIn(7) : instantIn(delta);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The instant (epoch ms) the text names. An offset or Z is absolute; any other
|
|
96
|
+
// wall time reads in `tz`; "in 2h" is an exact duration from `now`.
|
|
97
|
+
export function parseWhen(
|
|
98
|
+
text: string,
|
|
99
|
+
tz = DEFAULT_TIMEZONE,
|
|
100
|
+
now = Date.now()
|
|
101
|
+
): number {
|
|
102
|
+
const t = text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
103
|
+
if (!t) throw bad(text);
|
|
104
|
+
|
|
105
|
+
const abs =
|
|
106
|
+
/^(\d{4}-\d{2}-\d{2})[t ](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)(z|[+-]\d{2}:?\d{2})$/.exec(
|
|
107
|
+
t
|
|
108
|
+
);
|
|
109
|
+
if (abs) {
|
|
110
|
+
const zone = abs[3] === "z" ? "Z" : abs[3].replace(/^([+-]\d{2}):?/, "$1:");
|
|
111
|
+
const ms = Date.parse(`${abs[1]}T${abs[2]}${zone}`);
|
|
112
|
+
if (Number.isNaN(ms)) throw bad(text);
|
|
113
|
+
return ms;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const iso =
|
|
117
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:[t ](\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?)?$/.exec(
|
|
118
|
+
t
|
|
119
|
+
);
|
|
120
|
+
if (iso) {
|
|
121
|
+
const [y, m, d] = [Number(iso[1]), Number(iso[2]), Number(iso[3])];
|
|
122
|
+
const h = iso[4] === undefined ? 9 : Number(iso[4]);
|
|
123
|
+
const mi = iso[5] === undefined ? 0 : Number(iso[5]);
|
|
124
|
+
const check = new Date(Date.UTC(y, m - 1, d));
|
|
125
|
+
if (
|
|
126
|
+
check.getUTCMonth() !== m - 1 ||
|
|
127
|
+
check.getUTCDate() !== d ||
|
|
128
|
+
h > 23 ||
|
|
129
|
+
mi > 59
|
|
130
|
+
)
|
|
131
|
+
throw bad(text);
|
|
132
|
+
return wallToInstant(y, m, d, h, mi, tz);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const inside = /^in (.+)$/.exec(t);
|
|
136
|
+
if (inside) {
|
|
137
|
+
const ms = duration(inside[1]);
|
|
138
|
+
if (ms === null) throw bad(text);
|
|
139
|
+
return now + ms;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const first = DAY_FIRST.exec(t);
|
|
143
|
+
const second = first ? null : CLOCK_FIRST.exec(t);
|
|
144
|
+
const at = clock(first?.[2] ?? second?.[1] ?? "");
|
|
145
|
+
if (at) {
|
|
146
|
+
const instant = onDay(first?.[1] ?? second?.[2], at, now, tz);
|
|
147
|
+
if (instant !== null) return instant;
|
|
148
|
+
}
|
|
149
|
+
throw bad(text);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// "in 17h", "in 5m", "3h ago".
|
|
153
|
+
export function relativeTime(ms: number, now = Date.now()): string {
|
|
154
|
+
const diff = ms - now;
|
|
155
|
+
const mins = Math.floor(Math.abs(diff) / 60_000);
|
|
156
|
+
const span =
|
|
157
|
+
mins < 1
|
|
158
|
+
? null
|
|
159
|
+
: mins < 60
|
|
160
|
+
? `${mins}m`
|
|
161
|
+
: mins < 24 * 60
|
|
162
|
+
? `${Math.floor(mins / 60)}h`
|
|
163
|
+
: `${Math.floor(mins / (24 * 60))}d`;
|
|
164
|
+
if (!span) return diff >= 0 ? "in under a minute" : "just now";
|
|
165
|
+
return diff >= 0 ? `in ${span}` : `${span} ago`;
|
|
166
|
+
}
|