omegon 0.10.3 → 0.10.5
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/extensions/chronos/chronos.ts +324 -0
- package/extensions/chronos/index.ts +47 -68
- package/extensions/cleave/dispatcher.ts +48 -1
- package/extensions/cleave/index.ts +3 -3
- package/node_modules/@styrene-lab/pi-ai/dist/models.generated.d.ts +51 -0
- package/node_modules/@styrene-lab/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/node_modules/@styrene-lab/pi-ai/dist/models.generated.js +51 -0
- package/node_modules/@styrene-lab/pi-ai/dist/models.generated.js.map +1 -1
- package/package.json +1 -1
- package/extensions/chronos/chronos.sh +0 -487
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chronos — Pure TypeScript date/time context functions
|
|
3
|
+
*
|
|
4
|
+
* Replaces chronos.sh. All functions accept an injectable `now` for deterministic testing.
|
|
5
|
+
* Output format matches the original shell script exactly for backward compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] as const;
|
|
11
|
+
const MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
12
|
+
|
|
13
|
+
/** Format as YYYY-MM-DD */
|
|
14
|
+
function ymd(d: Date): string {
|
|
15
|
+
const y = d.getFullYear();
|
|
16
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
17
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
18
|
+
return `${y}-${m}-${day}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Day of week name */
|
|
22
|
+
function dowName(d: Date): string {
|
|
23
|
+
return DAYS[d.getDay()];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** ISO day of week: 1=Monday … 7=Sunday */
|
|
27
|
+
function isoDow(d: Date): number {
|
|
28
|
+
return d.getDay() === 0 ? 7 : d.getDay();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Format as "Mon D" (e.g. "Mar 18") */
|
|
32
|
+
function formatShort(d: Date): string {
|
|
33
|
+
return `${MONTHS_SHORT[d.getMonth()]} ${d.getDate()}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Add N days to a date (returns new Date) */
|
|
37
|
+
function addDays(d: Date, n: number): Date {
|
|
38
|
+
const r = new Date(d);
|
|
39
|
+
r.setDate(r.getDate() + n);
|
|
40
|
+
return r;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Add N months to a date */
|
|
44
|
+
function addMonths(d: Date, n: number): Date {
|
|
45
|
+
const r = new Date(d);
|
|
46
|
+
r.setMonth(r.getMonth() + n);
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** First day of month */
|
|
51
|
+
function monthStart(year: number, month: number): Date {
|
|
52
|
+
return new Date(year, month, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Last day of month */
|
|
56
|
+
function monthEnd(year: number, month: number): Date {
|
|
57
|
+
return new Date(year, month + 1, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Build range string like "Mar 16 - Mar 20, 2026" handling year boundaries */
|
|
61
|
+
function weekRange(mon: Date, fri: Date): string {
|
|
62
|
+
const monY = mon.getFullYear();
|
|
63
|
+
const friY = fri.getFullYear();
|
|
64
|
+
if (monY === friY) {
|
|
65
|
+
return `${formatShort(mon)} - ${formatShort(fri)}, ${friY}`;
|
|
66
|
+
}
|
|
67
|
+
return `${formatShort(mon)}, ${monY} - ${formatShort(fri)}, ${friY}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Subcommands ──────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export function computeWeek(now: Date = new Date()): string {
|
|
73
|
+
const dow = isoDow(now);
|
|
74
|
+
const daysSinceMon = dow - 1;
|
|
75
|
+
const currMon = addDays(now, -daysSinceMon);
|
|
76
|
+
const currFri = addDays(currMon, 4);
|
|
77
|
+
const prevMon = addDays(currMon, -7);
|
|
78
|
+
const prevFri = addDays(prevMon, 4);
|
|
79
|
+
|
|
80
|
+
return [
|
|
81
|
+
"DATE_CONTEXT:",
|
|
82
|
+
` TODAY: ${ymd(now)} (${dowName(now)})`,
|
|
83
|
+
` CURR_WEEK_START: ${ymd(currMon)} (Monday)`,
|
|
84
|
+
` CURR_WEEK_END: ${ymd(currFri)} (Friday)`,
|
|
85
|
+
` CURR_WEEK_RANGE: ${weekRange(currMon, currFri)}`,
|
|
86
|
+
` PREV_WEEK_START: ${ymd(prevMon)} (Monday)`,
|
|
87
|
+
` PREV_WEEK_END: ${ymd(prevFri)} (Friday)`,
|
|
88
|
+
` PREV_WEEK_RANGE: ${weekRange(prevMon, prevFri)}`,
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function computeMonth(now: Date = new Date()): string {
|
|
93
|
+
const year = now.getFullYear();
|
|
94
|
+
const month = now.getMonth();
|
|
95
|
+
|
|
96
|
+
const currStart = monthStart(year, month);
|
|
97
|
+
const currEnd = monthEnd(year, month);
|
|
98
|
+
|
|
99
|
+
const prevMonth = month === 0 ? 11 : month - 1;
|
|
100
|
+
const prevYear = month === 0 ? year - 1 : year;
|
|
101
|
+
const prevStart = monthStart(prevYear, prevMonth);
|
|
102
|
+
const prevEnd = monthEnd(prevYear, prevMonth);
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
"MONTH_CONTEXT:",
|
|
106
|
+
` TODAY: ${ymd(now)} (${dowName(now)})`,
|
|
107
|
+
` CURR_MONTH_START: ${ymd(currStart)}`,
|
|
108
|
+
` CURR_MONTH_END: ${ymd(currEnd)}`,
|
|
109
|
+
` CURR_MONTH_RANGE: ${formatShort(currStart)} - ${formatShort(currEnd)}, ${year}`,
|
|
110
|
+
` PREV_MONTH_START: ${ymd(prevStart)}`,
|
|
111
|
+
` PREV_MONTH_END: ${ymd(prevEnd)}`,
|
|
112
|
+
` PREV_MONTH_RANGE: ${formatShort(prevStart)}, ${prevYear} - ${formatShort(prevEnd)}, ${prevEnd.getFullYear()}`,
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function computeQuarter(now: Date = new Date()): string {
|
|
117
|
+
const year = now.getFullYear();
|
|
118
|
+
const month = now.getMonth() + 1; // 1-based
|
|
119
|
+
|
|
120
|
+
const quarter = Math.ceil(month / 3);
|
|
121
|
+
const qStartMonth = (quarter - 1) * 3; // 0-based
|
|
122
|
+
const qStart = monthStart(year, qStartMonth);
|
|
123
|
+
const qEnd = monthEnd(year, qStartMonth + 2);
|
|
124
|
+
|
|
125
|
+
let fyYear: number, fyStart: string, fyEnd: string;
|
|
126
|
+
if (month >= 10) {
|
|
127
|
+
fyYear = year + 1;
|
|
128
|
+
fyStart = `${year}-10-01`;
|
|
129
|
+
fyEnd = `${fyYear}-09-30`;
|
|
130
|
+
} else {
|
|
131
|
+
fyYear = year;
|
|
132
|
+
fyStart = `${year - 1}-10-01`;
|
|
133
|
+
fyEnd = `${year}-09-30`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fyMonthOffset = month >= 10 ? month - 10 + 1 : month + 3;
|
|
137
|
+
const fq = Math.ceil(fyMonthOffset / 3);
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
"QUARTER_CONTEXT:",
|
|
141
|
+
` TODAY: ${ymd(now)} (${dowName(now)})`,
|
|
142
|
+
` CALENDAR_QUARTER: Q${quarter} ${year}`,
|
|
143
|
+
` QUARTER_START: ${ymd(qStart)}`,
|
|
144
|
+
` QUARTER_END: ${ymd(qEnd)}`,
|
|
145
|
+
` FISCAL_YEAR: FY${fyYear} (Oct-Sep)`,
|
|
146
|
+
` FISCAL_QUARTER: FQ${fq}`,
|
|
147
|
+
` FY_START: ${fyStart}`,
|
|
148
|
+
` FY_END: ${fyEnd}`,
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Resolve a relative date expression. Throws on unrecognized expressions. */
|
|
153
|
+
export function resolveRelative(expression: string, now: Date = new Date()): Date {
|
|
154
|
+
const expr = expression.trim().toLowerCase();
|
|
155
|
+
|
|
156
|
+
if (expr === "yesterday") return addDays(now, -1);
|
|
157
|
+
if (expr === "tomorrow") return addDays(now, 1);
|
|
158
|
+
if (expr === "today") return now;
|
|
159
|
+
|
|
160
|
+
// N days/weeks/months ago
|
|
161
|
+
const agoMatch = expr.match(/^(\d+)\s+(day|days|week|weeks|month|months)\s+ago$/);
|
|
162
|
+
if (agoMatch) {
|
|
163
|
+
const n = parseInt(agoMatch[1], 10);
|
|
164
|
+
const unit = agoMatch[2];
|
|
165
|
+
if (unit.startsWith("day")) return addDays(now, -n);
|
|
166
|
+
if (unit.startsWith("week")) return addDays(now, -n * 7);
|
|
167
|
+
if (unit.startsWith("month")) return addMonths(now, -n);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// N days/weeks from now / ahead / from today
|
|
171
|
+
const aheadMatch = expr.match(/^(\d+)\s+(day|days|week|weeks)\s+(from now|ahead|from today)$/);
|
|
172
|
+
if (aheadMatch) {
|
|
173
|
+
const n = parseInt(aheadMatch[1], 10);
|
|
174
|
+
const unit = aheadMatch[2];
|
|
175
|
+
if (unit.startsWith("day")) return addDays(now, n);
|
|
176
|
+
if (unit.startsWith("week")) return addDays(now, n * 7);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// next/last {weekday}
|
|
180
|
+
const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
181
|
+
const dayMatch = expr.match(/^(next|last)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)$/);
|
|
182
|
+
if (dayMatch) {
|
|
183
|
+
const direction = dayMatch[1];
|
|
184
|
+
const targetDow = dayNames.indexOf(dayMatch[2]);
|
|
185
|
+
const currentDow = now.getDay();
|
|
186
|
+
|
|
187
|
+
if (direction === "next") {
|
|
188
|
+
let diff = targetDow - currentDow;
|
|
189
|
+
if (diff <= 0) diff += 7;
|
|
190
|
+
return addDays(now, diff);
|
|
191
|
+
} else {
|
|
192
|
+
let diff = currentDow - targetDow;
|
|
193
|
+
if (diff <= 0) diff += 7;
|
|
194
|
+
return addDays(now, -diff);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
throw new Error(`Cannot parse relative expression: '${expression}'. Supported: N days/weeks/months ago, N days/weeks from now, yesterday, tomorrow, next/last {weekday}.`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function computeRelative(expression: string, now: Date = new Date()): string {
|
|
202
|
+
const resolved = resolveRelative(expression, now);
|
|
203
|
+
return [
|
|
204
|
+
"RELATIVE_DATE:",
|
|
205
|
+
` EXPRESSION: ${expression}`,
|
|
206
|
+
` RESOLVED: ${ymd(resolved)} (${dowName(resolved)})`,
|
|
207
|
+
` TODAY: ${ymd(now)} (${dowName(now)})`,
|
|
208
|
+
].join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** ISO 8601 week number (Thursday-based) */
|
|
212
|
+
function isoWeekNumber(d: Date): { week: number; year: number } {
|
|
213
|
+
const tmp = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
214
|
+
const dayNum = tmp.getUTCDay() || 7;
|
|
215
|
+
tmp.setUTCDate(tmp.getUTCDate() + 4 - dayNum);
|
|
216
|
+
const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1));
|
|
217
|
+
const week = Math.ceil(((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
218
|
+
return { week, year: tmp.getUTCFullYear() };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Day of year (1-366) */
|
|
222
|
+
function dayOfYear(d: Date): number {
|
|
223
|
+
const start = new Date(d.getFullYear(), 0, 0);
|
|
224
|
+
const diff = d.getTime() - start.getTime();
|
|
225
|
+
return Math.floor(diff / 86400000);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function computeIso(now: Date = new Date()): string {
|
|
229
|
+
const { week, year } = isoWeekNumber(now);
|
|
230
|
+
const wStr = String(week).padStart(2, "0");
|
|
231
|
+
const doy = String(dayOfYear(now)).padStart(3, "0");
|
|
232
|
+
const dow = isoDow(now);
|
|
233
|
+
|
|
234
|
+
return [
|
|
235
|
+
"ISO_CONTEXT:",
|
|
236
|
+
` TODAY: ${ymd(now)} (${dowName(now)})`,
|
|
237
|
+
` ISO_WEEK: W${wStr}`,
|
|
238
|
+
` ISO_YEAR: ${year}`,
|
|
239
|
+
` ISO_WEEKDATE: ${year}-W${wStr}-${dow}`,
|
|
240
|
+
` DAY_OF_YEAR: ${doy}`,
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function computeEpoch(now: Date = new Date()): string {
|
|
245
|
+
const seconds = Math.floor(now.getTime() / 1000);
|
|
246
|
+
const millis = now.getTime();
|
|
247
|
+
|
|
248
|
+
return [
|
|
249
|
+
"EPOCH_CONTEXT:",
|
|
250
|
+
` TODAY: ${ymd(now)} (${dowName(now)})`,
|
|
251
|
+
` UNIX_SECONDS: ${seconds}`,
|
|
252
|
+
` UNIX_MILLIS: ${millis}`,
|
|
253
|
+
].join("\n");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function computeTz(now: Date = new Date()): string {
|
|
257
|
+
const tzParts = now.toTimeString().match(/\((.+)\)/);
|
|
258
|
+
const tzAbbrev = tzParts
|
|
259
|
+
? tzParts[1].replace(/[a-z ]/g, "") || tzParts[1]
|
|
260
|
+
: Intl.DateTimeFormat(undefined, { timeZoneName: "short" }).formatToParts(now).find(p => p.type === "timeZoneName")?.value || "Unknown";
|
|
261
|
+
|
|
262
|
+
const offsetMin = now.getTimezoneOffset();
|
|
263
|
+
const sign = offsetMin <= 0 ? "+" : "-";
|
|
264
|
+
const absMin = Math.abs(offsetMin);
|
|
265
|
+
const hh = String(Math.floor(absMin / 60)).padStart(2, "0");
|
|
266
|
+
const mm = String(absMin % 60).padStart(2, "0");
|
|
267
|
+
const utcOffset = `${sign}${hh}${mm}`;
|
|
268
|
+
|
|
269
|
+
return [
|
|
270
|
+
"TIMEZONE_CONTEXT:",
|
|
271
|
+
` TODAY: ${ymd(now)} (${dowName(now)})`,
|
|
272
|
+
` TIMEZONE: ${tzAbbrev}`,
|
|
273
|
+
` UTC_OFFSET: ${utcOffset}`,
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function computeRange(fromDate: string, toDate: string): string {
|
|
278
|
+
const dateRe = /^\d{4}-\d{2}-\d{2}$/;
|
|
279
|
+
if (!dateRe.test(fromDate)) throw new Error(`Invalid date format '${fromDate}'. Use YYYY-MM-DD.`);
|
|
280
|
+
if (!dateRe.test(toDate)) throw new Error(`Invalid date format '${toDate}'. Use YYYY-MM-DD.`);
|
|
281
|
+
|
|
282
|
+
const d1 = new Date(fromDate + "T00:00:00");
|
|
283
|
+
const d2 = new Date(toDate + "T00:00:00");
|
|
284
|
+
|
|
285
|
+
if (isNaN(d1.getTime())) throw new Error(`Could not parse date: ${fromDate}`);
|
|
286
|
+
if (isNaN(d2.getTime())) throw new Error(`Could not parse date: ${toDate}`);
|
|
287
|
+
|
|
288
|
+
const diffMs = d2.getTime() - d1.getTime();
|
|
289
|
+
const calendarDays = Math.round(diffMs / 86400000);
|
|
290
|
+
const absDays = Math.abs(calendarDays);
|
|
291
|
+
|
|
292
|
+
let businessDays = 0;
|
|
293
|
+
const step = calendarDays >= 0 ? 1 : -1;
|
|
294
|
+
let cursor = new Date(d1);
|
|
295
|
+
for (let i = 0; i < absDays; i++) {
|
|
296
|
+
const dow = cursor.getDay();
|
|
297
|
+
if (dow >= 1 && dow <= 5) businessDays++;
|
|
298
|
+
cursor = addDays(cursor, step);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return [
|
|
302
|
+
"RANGE_CONTEXT:",
|
|
303
|
+
` FROM: ${fromDate}`,
|
|
304
|
+
` TO: ${toDate}`,
|
|
305
|
+
` CALENDAR_DAYS: ${absDays}`,
|
|
306
|
+
` BUSINESS_DAYS: ${businessDays}`,
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function computeAll(now: Date = new Date()): string {
|
|
311
|
+
return [
|
|
312
|
+
computeWeek(now),
|
|
313
|
+
"",
|
|
314
|
+
computeMonth(now),
|
|
315
|
+
"",
|
|
316
|
+
computeQuarter(now),
|
|
317
|
+
"",
|
|
318
|
+
computeIso(now),
|
|
319
|
+
"",
|
|
320
|
+
computeEpoch(now),
|
|
321
|
+
"",
|
|
322
|
+
computeTz(now),
|
|
323
|
+
].join("\n");
|
|
324
|
+
}
|
|
@@ -1,35 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* chronos — Authoritative date and time context from system clock
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* an authoritative source of truth from the system clock.
|
|
7
|
-
*
|
|
8
|
-
* Also registers a `/chronos` command for interactive use.
|
|
4
|
+
* Pure TypeScript implementation — no shell dependencies.
|
|
5
|
+
* Registers a `chronos` tool and `/chronos` command.
|
|
9
6
|
*
|
|
10
7
|
* Subcommands: week (default), month, quarter, relative, iso, epoch, tz, range, all
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
|
-
import { existsSync } from "node:fs";
|
|
14
|
-
import { join } from "node:path";
|
|
15
10
|
import { StringEnum } from "../lib/typebox-helpers";
|
|
16
11
|
import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
|
|
17
12
|
import { Type } from "@sinclair/typebox";
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
import {
|
|
14
|
+
computeWeek,
|
|
15
|
+
computeMonth,
|
|
16
|
+
computeQuarter,
|
|
17
|
+
computeRelative,
|
|
18
|
+
computeIso,
|
|
19
|
+
computeEpoch,
|
|
20
|
+
computeTz,
|
|
21
|
+
computeRange,
|
|
22
|
+
computeAll,
|
|
23
|
+
} from "./chronos";
|
|
20
24
|
|
|
21
25
|
const SUBCOMMANDS = ["week", "month", "quarter", "relative", "iso", "epoch", "tz", "range", "all"] as const;
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
function executeChronos(params: { subcommand?: string; expression?: string; from_date?: string; to_date?: string }): string {
|
|
28
|
+
const sub = params.subcommand || "week";
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
switch (sub) {
|
|
31
|
+
case "week": return computeWeek();
|
|
32
|
+
case "month": return computeMonth();
|
|
33
|
+
case "quarter": return computeQuarter();
|
|
34
|
+
case "relative":
|
|
35
|
+
if (!params.expression) {
|
|
36
|
+
throw new Error("The 'relative' subcommand requires an 'expression' parameter (e.g. '3 days ago').");
|
|
37
|
+
}
|
|
38
|
+
return computeRelative(params.expression);
|
|
39
|
+
case "iso": return computeIso();
|
|
40
|
+
case "epoch": return computeEpoch();
|
|
41
|
+
case "tz": return computeTz();
|
|
42
|
+
case "range":
|
|
43
|
+
if (!params.from_date || !params.to_date) {
|
|
44
|
+
throw new Error("The 'range' subcommand requires both 'from_date' and 'to_date' (YYYY-MM-DD).");
|
|
45
|
+
}
|
|
46
|
+
return computeRange(params.from_date, params.to_date);
|
|
47
|
+
case "all": return computeAll();
|
|
48
|
+
default: throw new Error(`Unknown subcommand: ${sub}`);
|
|
28
49
|
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function chronosExtension(pi: ExtensionAPI) {
|
|
29
53
|
|
|
30
|
-
// ------------------------------------------------------------------
|
|
31
|
-
// chronos tool — callable by the LLM
|
|
32
|
-
// ------------------------------------------------------------------
|
|
33
54
|
pi.registerTool({
|
|
34
55
|
name: "chronos",
|
|
35
56
|
label: "Chronos",
|
|
@@ -68,45 +89,15 @@ export default function chronosExtension(pi: ExtensionAPI) {
|
|
|
68
89
|
),
|
|
69
90
|
}),
|
|
70
91
|
|
|
71
|
-
async execute(_toolCallId, params,
|
|
72
|
-
|
|
73
|
-
throw new Error(
|
|
74
|
-
`chronos.sh not found at ${CHRONOS_SH}. ` +
|
|
75
|
-
`Expected alongside the chronos skill.`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const sub = params.subcommand || "week";
|
|
80
|
-
const args = [CHRONOS_SH, sub];
|
|
81
|
-
|
|
82
|
-
if (sub === "relative") {
|
|
83
|
-
if (!params.expression) {
|
|
84
|
-
throw new Error("The 'relative' subcommand requires an 'expression' parameter (e.g. '3 days ago').");
|
|
85
|
-
}
|
|
86
|
-
args.push(params.expression);
|
|
87
|
-
} else if (sub === "range") {
|
|
88
|
-
if (!params.from_date || !params.to_date) {
|
|
89
|
-
throw new Error("The 'range' subcommand requires both 'from_date' and 'to_date' (YYYY-MM-DD).");
|
|
90
|
-
}
|
|
91
|
-
args.push(params.from_date, params.to_date);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const result = await pi.exec("bash", args, { signal, timeout: 10_000 });
|
|
95
|
-
|
|
96
|
-
if (result.code !== 0) {
|
|
97
|
-
throw new Error(`chronos.sh failed (exit ${result.code}):\n${result.stderr || result.stdout}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
92
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
93
|
+
const result = executeChronos(params);
|
|
100
94
|
return {
|
|
101
|
-
content: [{ type: "text", text: result
|
|
102
|
-
details: { subcommand:
|
|
95
|
+
content: [{ type: "text", text: result }],
|
|
96
|
+
details: { subcommand: params.subcommand || "week" },
|
|
103
97
|
};
|
|
104
98
|
},
|
|
105
99
|
});
|
|
106
100
|
|
|
107
|
-
// ------------------------------------------------------------------
|
|
108
|
-
// /chronos command — interactive shortcut
|
|
109
|
-
// ------------------------------------------------------------------
|
|
110
101
|
pi.registerCommand("chronos", {
|
|
111
102
|
description: "Show date/time context (usage: /chronos [week|month|quarter|iso|epoch|tz|all])",
|
|
112
103
|
getArgumentCompletions: (prefix: string) => {
|
|
@@ -116,33 +107,21 @@ export default function chronosExtension(pi: ExtensionAPI) {
|
|
|
116
107
|
},
|
|
117
108
|
handler: async (args, _ctx) => {
|
|
118
109
|
const sub = (args || "").trim() || "week";
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
try {
|
|
111
|
+
const result = executeChronos({ subcommand: sub });
|
|
121
112
|
pi.sendMessage({
|
|
122
113
|
customType: "view",
|
|
123
|
-
content:
|
|
114
|
+
content: `**Chronos**\n\n\`\`\`\n${result}\n\`\`\``,
|
|
124
115
|
display: true,
|
|
125
116
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const cliArgs = [CHRONOS_SH, sub];
|
|
130
|
-
const result = await pi.exec("bash", cliArgs, { timeout: 10_000 });
|
|
131
|
-
|
|
132
|
-
if (result.code !== 0) {
|
|
117
|
+
} catch (err: unknown) {
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
133
119
|
pi.sendMessage({
|
|
134
120
|
customType: "view",
|
|
135
|
-
content: `❌
|
|
121
|
+
content: `❌ ${msg}`,
|
|
136
122
|
display: true,
|
|
137
123
|
});
|
|
138
|
-
return;
|
|
139
124
|
}
|
|
140
|
-
|
|
141
|
-
pi.sendMessage({
|
|
142
|
-
customType: "view",
|
|
143
|
-
content: `**Chronos**\n\n\`\`\`\n${result.stdout.trim()}\n\`\`\``,
|
|
144
|
-
display: true,
|
|
145
|
-
});
|
|
146
125
|
},
|
|
147
126
|
});
|
|
148
127
|
}
|
|
@@ -41,6 +41,20 @@ import { registerCleaveProc, deregisterCleaveProc, killCleaveProc } from "./subp
|
|
|
41
41
|
*/
|
|
42
42
|
export const LARGE_RUN_THRESHOLD = 4;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Default per-child wall-clock timeout (15 minutes).
|
|
46
|
+
* Hard backstop — children that exceed this are killed regardless of activity.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_CHILD_TIMEOUT_MS = 15 * 60 * 1000;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default RPC idle timeout (3 minutes).
|
|
52
|
+
* If no RPC event arrives within this window, the child is considered stalled
|
|
53
|
+
* and is killed. Resets on every event (tool_start, tool_end, assistant_message,
|
|
54
|
+
* etc.). Only applies to RPC mode — pipe mode children use wall-clock only.
|
|
55
|
+
*/
|
|
56
|
+
export const IDLE_TIMEOUT_MS = 3 * 60 * 1000;
|
|
57
|
+
|
|
44
58
|
// ─── Explicit model resolution ──────────────────────────────────────────────
|
|
45
59
|
|
|
46
60
|
/**
|
|
@@ -505,6 +519,7 @@ async function spawnChildRpc(
|
|
|
505
519
|
signal?: AbortSignal,
|
|
506
520
|
localModel?: string,
|
|
507
521
|
onEvent?: (event: RpcChildEvent) => void,
|
|
522
|
+
idleTimeoutMs: number = IDLE_TIMEOUT_MS,
|
|
508
523
|
): Promise<RpcChildResult> {
|
|
509
524
|
const omegon = resolveOmegonSubprocess();
|
|
510
525
|
const args = [...omegon.argvPrefix, "--mode", "rpc", "--no-session"];
|
|
@@ -540,6 +555,25 @@ async function spawnChildRpc(
|
|
|
540
555
|
// Collect stderr
|
|
541
556
|
proc.stderr?.on("data", (data) => { stderr += data.toString(); });
|
|
542
557
|
|
|
558
|
+
// ── Idle timeout ─────────────────────────────────────────────────
|
|
559
|
+
// Reset on every RPC event. If no event arrives within the idle
|
|
560
|
+
// window, the child is stalled — kill it.
|
|
561
|
+
let idleKilled = false;
|
|
562
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
563
|
+
const resetIdleTimer = () => {
|
|
564
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
565
|
+
if (idleTimeoutMs > 0) {
|
|
566
|
+
idleTimer = setTimeout(() => {
|
|
567
|
+
if (!killed && !proc.killed) {
|
|
568
|
+
idleKilled = true;
|
|
569
|
+
killed = true;
|
|
570
|
+
killCleaveProc(proc);
|
|
571
|
+
scheduleEscalation();
|
|
572
|
+
}
|
|
573
|
+
}, idleTimeoutMs);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
543
577
|
// Parse stdout exclusively via RPC event stream (no competing data listener)
|
|
544
578
|
let eventsFinished: Promise<void> = Promise.resolve();
|
|
545
579
|
if (proc.stdout) {
|
|
@@ -547,6 +581,7 @@ async function spawnChildRpc(
|
|
|
547
581
|
try {
|
|
548
582
|
for await (const event of parseRpcEventStream(proc.stdout!)) {
|
|
549
583
|
events.push(event);
|
|
584
|
+
resetIdleTimer(); // activity — push back the idle deadline
|
|
550
585
|
if (event.type === "pipe_closed") {
|
|
551
586
|
pipeBroken = true;
|
|
552
587
|
}
|
|
@@ -572,6 +607,10 @@ async function spawnChildRpc(
|
|
|
572
607
|
}, 5_000);
|
|
573
608
|
};
|
|
574
609
|
|
|
610
|
+
// Start the idle timer now — if the child never emits an event, it's
|
|
611
|
+
// caught within the idle window rather than waiting for the full wall clock.
|
|
612
|
+
resetIdleTimer();
|
|
613
|
+
|
|
575
614
|
const timer = setTimeout(() => {
|
|
576
615
|
killed = true;
|
|
577
616
|
killCleaveProc(proc);
|
|
@@ -592,6 +631,7 @@ async function spawnChildRpc(
|
|
|
592
631
|
deregisterCleaveProc(proc);
|
|
593
632
|
clearTimeout(timer);
|
|
594
633
|
clearTimeout(escalationTimer);
|
|
634
|
+
clearTimeout(idleTimer);
|
|
595
635
|
signal?.removeEventListener("abort", onAbort);
|
|
596
636
|
|
|
597
637
|
// Close stdin if still open (child has exited)
|
|
@@ -600,10 +640,16 @@ async function spawnChildRpc(
|
|
|
600
640
|
// Wait for all RPC events to be consumed before resolving
|
|
601
641
|
await eventsFinished;
|
|
602
642
|
|
|
643
|
+
const killReason = idleKilled
|
|
644
|
+
? `Killed (idle — no RPC events for ${Math.round(idleTimeoutMs / 1000)}s)\n${stderr}`
|
|
645
|
+
: killed
|
|
646
|
+
? `Killed (timeout or abort)\n${stderr}`
|
|
647
|
+
: stderr;
|
|
648
|
+
|
|
603
649
|
resolve({
|
|
604
650
|
exitCode: killed ? -1 : (code ?? 1),
|
|
605
651
|
stdout: "",
|
|
606
|
-
stderr:
|
|
652
|
+
stderr: killReason,
|
|
607
653
|
events,
|
|
608
654
|
pipeBroken,
|
|
609
655
|
});
|
|
@@ -615,6 +661,7 @@ async function spawnChildRpc(
|
|
|
615
661
|
deregisterCleaveProc(proc);
|
|
616
662
|
clearTimeout(timer);
|
|
617
663
|
clearTimeout(escalationTimer);
|
|
664
|
+
clearTimeout(idleTimer);
|
|
618
665
|
signal?.removeEventListener("abort", onAbort);
|
|
619
666
|
resolve({
|
|
620
667
|
exitCode: 1,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
} from "./assessment.ts";
|
|
52
52
|
import { detectConflicts, parseTaskResult } from "./conflicts.ts";
|
|
53
53
|
import { emitResolvedBugCandidate } from "./lifecycle-emitter.ts";
|
|
54
|
-
import { dispatchChildren, resolveExecuteModel } from "./dispatcher.ts";
|
|
54
|
+
import { DEFAULT_CHILD_TIMEOUT_MS, dispatchChildren, resolveExecuteModel } from "./dispatcher.ts";
|
|
55
55
|
import { DEFAULT_REVIEW_CONFIG, type ReviewConfig } from "./review.ts";
|
|
56
56
|
import {
|
|
57
57
|
detectOpenSpec,
|
|
@@ -2144,7 +2144,7 @@ export default function cleaveExtension(pi: ExtensionAPI) {
|
|
|
2144
2144
|
pi,
|
|
2145
2145
|
state,
|
|
2146
2146
|
4, // maxParallel
|
|
2147
|
-
|
|
2147
|
+
DEFAULT_CHILD_TIMEOUT_MS,
|
|
2148
2148
|
undefined,
|
|
2149
2149
|
signal,
|
|
2150
2150
|
(msg) => emit(msg),
|
|
@@ -2581,7 +2581,7 @@ export default function cleaveExtension(pi: ExtensionAPI) {
|
|
|
2581
2581
|
pi,
|
|
2582
2582
|
state,
|
|
2583
2583
|
maxParallel,
|
|
2584
|
-
|
|
2584
|
+
DEFAULT_CHILD_TIMEOUT_MS,
|
|
2585
2585
|
localModel,
|
|
2586
2586
|
signal ?? undefined,
|
|
2587
2587
|
(msg) => {
|
|
@@ -9071,6 +9071,40 @@ export declare const MODELS: {
|
|
|
9071
9071
|
contextWindow: number;
|
|
9072
9072
|
maxTokens: number;
|
|
9073
9073
|
};
|
|
9074
|
+
readonly "openai/gpt-5.4-mini": {
|
|
9075
|
+
id: string;
|
|
9076
|
+
name: string;
|
|
9077
|
+
api: "openai-completions";
|
|
9078
|
+
provider: string;
|
|
9079
|
+
baseUrl: string;
|
|
9080
|
+
reasoning: true;
|
|
9081
|
+
input: ("image" | "text")[];
|
|
9082
|
+
cost: {
|
|
9083
|
+
input: number;
|
|
9084
|
+
output: number;
|
|
9085
|
+
cacheRead: number;
|
|
9086
|
+
cacheWrite: number;
|
|
9087
|
+
};
|
|
9088
|
+
contextWindow: number;
|
|
9089
|
+
maxTokens: number;
|
|
9090
|
+
};
|
|
9091
|
+
readonly "openai/gpt-5.4-nano": {
|
|
9092
|
+
id: string;
|
|
9093
|
+
name: string;
|
|
9094
|
+
api: "openai-completions";
|
|
9095
|
+
provider: string;
|
|
9096
|
+
baseUrl: string;
|
|
9097
|
+
reasoning: true;
|
|
9098
|
+
input: ("image" | "text")[];
|
|
9099
|
+
cost: {
|
|
9100
|
+
input: number;
|
|
9101
|
+
output: number;
|
|
9102
|
+
cacheRead: number;
|
|
9103
|
+
cacheWrite: number;
|
|
9104
|
+
};
|
|
9105
|
+
contextWindow: number;
|
|
9106
|
+
maxTokens: number;
|
|
9107
|
+
};
|
|
9074
9108
|
readonly "openai/gpt-5.4-pro": {
|
|
9075
9109
|
id: string;
|
|
9076
9110
|
name: string;
|
|
@@ -12439,6 +12473,23 @@ export declare const MODELS: {
|
|
|
12439
12473
|
contextWindow: number;
|
|
12440
12474
|
maxTokens: number;
|
|
12441
12475
|
};
|
|
12476
|
+
readonly "openai/gpt-5.4-mini": {
|
|
12477
|
+
id: string;
|
|
12478
|
+
name: string;
|
|
12479
|
+
api: "anthropic-messages";
|
|
12480
|
+
provider: string;
|
|
12481
|
+
baseUrl: string;
|
|
12482
|
+
reasoning: true;
|
|
12483
|
+
input: ("image" | "text")[];
|
|
12484
|
+
cost: {
|
|
12485
|
+
input: number;
|
|
12486
|
+
output: number;
|
|
12487
|
+
cacheRead: number;
|
|
12488
|
+
cacheWrite: number;
|
|
12489
|
+
};
|
|
12490
|
+
contextWindow: number;
|
|
12491
|
+
maxTokens: number;
|
|
12492
|
+
};
|
|
12442
12493
|
readonly "openai/gpt-5.4-pro": {
|
|
12443
12494
|
id: string;
|
|
12444
12495
|
name: string;
|