@harborppm/cli 0.75.0 → 0.77.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/README.md +3 -1
- package/dist/app.mjs +527 -93
- package/dist/words.mjs +301 -0
- package/lib/cli.mjs +116 -26
- package/lib/words.mjs +20 -0
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -128,10 +128,10 @@ function timeAgo(date, now = clockNow()) {
|
|
|
128
128
|
const words = `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
129
129
|
return diff > 0 ? `in ${words}` : `${words} ago`;
|
|
130
130
|
}
|
|
131
|
-
var MONTHS, DAYS$
|
|
131
|
+
var MONTHS$1, DAYS$2, pad$1, TOKENS, TOKEN, ISO, MINUTE;
|
|
132
132
|
var init_date_text = __esmMin((() => {
|
|
133
133
|
init_clock();
|
|
134
|
-
MONTHS = [
|
|
134
|
+
MONTHS$1 = [
|
|
135
135
|
"January",
|
|
136
136
|
"February",
|
|
137
137
|
"March",
|
|
@@ -145,7 +145,7 @@ var init_date_text = __esmMin((() => {
|
|
|
145
145
|
"November",
|
|
146
146
|
"December"
|
|
147
147
|
];
|
|
148
|
-
DAYS$
|
|
148
|
+
DAYS$2 = [
|
|
149
149
|
"Sunday",
|
|
150
150
|
"Monday",
|
|
151
151
|
"Tuesday",
|
|
@@ -157,13 +157,13 @@ var init_date_text = __esmMin((() => {
|
|
|
157
157
|
pad$1 = (n, width = 2) => String(n).padStart(width, "0");
|
|
158
158
|
TOKENS = {
|
|
159
159
|
yyyy: (d) => pad$1(d.getFullYear(), 4),
|
|
160
|
-
MMMM: (d) => MONTHS[d.getMonth()],
|
|
161
|
-
MMM: (d) => MONTHS[d.getMonth()].slice(0, 3),
|
|
160
|
+
MMMM: (d) => MONTHS$1[d.getMonth()],
|
|
161
|
+
MMM: (d) => MONTHS$1[d.getMonth()].slice(0, 3),
|
|
162
162
|
MM: (d) => pad$1(d.getMonth() + 1),
|
|
163
163
|
dd: (d) => pad$1(d.getDate()),
|
|
164
164
|
d: (d) => String(d.getDate()),
|
|
165
|
-
EEEE: (d) => DAYS$
|
|
166
|
-
EEE: (d) => DAYS$
|
|
165
|
+
EEEE: (d) => DAYS$2[d.getDay()],
|
|
166
|
+
EEE: (d) => DAYS$2[d.getDay()].slice(0, 3)
|
|
167
167
|
};
|
|
168
168
|
TOKEN = /yyyy|MMMM|MMM|MM|dd|d|EEEE|EEE/g;
|
|
169
169
|
ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?(Z|[+-]\d{2}(?::?\d{2})?)?)?$/;
|
|
@@ -2270,11 +2270,11 @@ function easter(year) {
|
|
|
2270
2270
|
function nth(year, month, dow, n) {
|
|
2271
2271
|
if (n > 0) {
|
|
2272
2272
|
const first = iso$4(year, month, 1);
|
|
2273
|
-
const shift = (dow - weekday(first) + 7) % 7;
|
|
2273
|
+
const shift = (dow - weekday$1(first) + 7) % 7;
|
|
2274
2274
|
return plus$1(first, shift + (n - 1) * 7);
|
|
2275
2275
|
}
|
|
2276
2276
|
const last = iso$4(year, month + 1, 0);
|
|
2277
|
-
return plus$1(last, -((weekday(last) - dow + 7) % 7));
|
|
2277
|
+
return plus$1(last, -((weekday$1(last) - dow + 7) % 7));
|
|
2278
2278
|
}
|
|
2279
2279
|
/** A country's public holidays in a year, by the day they're taken, oldest first. */
|
|
2280
2280
|
function holidaysIn(country, year) {
|
|
@@ -2290,7 +2290,7 @@ function holidaysIn(country, year) {
|
|
|
2290
2290
|
const out = [];
|
|
2291
2291
|
for (const h of days) {
|
|
2292
2292
|
let date = h.date;
|
|
2293
|
-
const wd = weekday(date);
|
|
2293
|
+
const wd = weekday$1(date);
|
|
2294
2294
|
if (h.move === "us" && (wd === 0 || wd === 6)) date = plus$1(date, wd === 6 ? -1 : 1);
|
|
2295
2295
|
else if (h.move === "next" && (wd === 0 || wd === 6)) {
|
|
2296
2296
|
date = plus$1(date, 1);
|
|
@@ -2389,19 +2389,19 @@ function harborHolidays(data, from, to) {
|
|
|
2389
2389
|
};
|
|
2390
2390
|
});
|
|
2391
2391
|
}
|
|
2392
|
-
var iso$4, weekday, plus$1, isWeekend, onOrBefore, fixed, fromEaster, MATARIKI, RULES, cache, PLAIN_CALENDAR;
|
|
2392
|
+
var iso$4, weekday$1, plus$1, isWeekend, onOrBefore, fixed, fromEaster, MATARIKI, RULES, cache, PLAIN_CALENDAR;
|
|
2393
2393
|
var init_holidays = __esmMin((() => {
|
|
2394
2394
|
init_date_fns();
|
|
2395
2395
|
init_format();
|
|
2396
2396
|
init_countries();
|
|
2397
2397
|
iso$4 = (y, m, d) => todayISO(new Date(y, m - 1, d, 12));
|
|
2398
|
-
weekday = (day) => (/* @__PURE__ */ new Date(`${day}T12:00:00`)).getDay();
|
|
2398
|
+
weekday$1 = (day) => (/* @__PURE__ */ new Date(`${day}T12:00:00`)).getDay();
|
|
2399
2399
|
plus$1 = (day, n) => todayISO(addDays(/* @__PURE__ */ new Date(`${day}T12:00:00`), n));
|
|
2400
2400
|
isWeekend = (day) => {
|
|
2401
|
-
const wd = weekday(day);
|
|
2401
|
+
const wd = weekday$1(day);
|
|
2402
2402
|
return wd === 0 || wd === 6;
|
|
2403
2403
|
};
|
|
2404
|
-
onOrBefore = (day, dow) => plus$1(day, -((weekday(day) - dow + 7) % 7));
|
|
2404
|
+
onOrBefore = (day, dow) => plus$1(day, -((weekday$1(day) - dow + 7) % 7));
|
|
2405
2405
|
fixed = (m, d) => (y) => iso$4(y, m, d);
|
|
2406
2406
|
fromEaster = (n) => (y) => plus$1(easter(y), n);
|
|
2407
2407
|
MATARIKI = {
|
|
@@ -2499,7 +2499,7 @@ var init_holidays = __esmMin((() => {
|
|
|
2499
2499
|
fixed(1, 1),
|
|
2500
2500
|
"next"
|
|
2501
2501
|
],
|
|
2502
|
-
["St Brigid's Day", (y) => weekday(iso$4(y, 2, 1)) === 5 ? iso$4(y, 2, 1) : nth(y, 2, 1, 1)],
|
|
2502
|
+
["St Brigid's Day", (y) => weekday$1(iso$4(y, 2, 1)) === 5 ? iso$4(y, 2, 1) : nth(y, 2, 1, 1)],
|
|
2503
2503
|
[
|
|
2504
2504
|
"St Patrick's Day",
|
|
2505
2505
|
fixed(3, 17),
|
|
@@ -2611,7 +2611,7 @@ var init_holidays = __esmMin((() => {
|
|
|
2611
2611
|
NL: [
|
|
2612
2612
|
["Nieuwjaarsdag", fixed(1, 1)],
|
|
2613
2613
|
["Tweede paasdag", fromEaster(1)],
|
|
2614
|
-
["Koningsdag", (y) => weekday(iso$4(y, 4, 27)) === 0 ? iso$4(y, 4, 26) : iso$4(y, 4, 27)],
|
|
2614
|
+
["Koningsdag", (y) => weekday$1(iso$4(y, 4, 27)) === 0 ? iso$4(y, 4, 26) : iso$4(y, 4, 27)],
|
|
2615
2615
|
["Hemelvaartsdag", fromEaster(39)],
|
|
2616
2616
|
["Tweede pinksterdag", fromEaster(50)],
|
|
2617
2617
|
["Eerste kerstdag", fixed(12, 25)],
|
|
@@ -3753,6 +3753,359 @@ function applyReckoning(actions, projectId, r, startDate) {
|
|
|
3753
3753
|
};
|
|
3754
3754
|
}
|
|
3755
3755
|
|
|
3756
|
+
// src/lib/plain-words.ts
|
|
3757
|
+
const DAYS$1 = [
|
|
3758
|
+
"sunday",
|
|
3759
|
+
"monday",
|
|
3760
|
+
"tuesday",
|
|
3761
|
+
"wednesday",
|
|
3762
|
+
"thursday",
|
|
3763
|
+
"friday",
|
|
3764
|
+
"saturday"
|
|
3765
|
+
];
|
|
3766
|
+
const MONTHS = [
|
|
3767
|
+
"january",
|
|
3768
|
+
"february",
|
|
3769
|
+
"march",
|
|
3770
|
+
"april",
|
|
3771
|
+
"may",
|
|
3772
|
+
"june",
|
|
3773
|
+
"july",
|
|
3774
|
+
"august",
|
|
3775
|
+
"september",
|
|
3776
|
+
"october",
|
|
3777
|
+
"november",
|
|
3778
|
+
"december"
|
|
3779
|
+
];
|
|
3780
|
+
const NUMBER_WORDS = {
|
|
3781
|
+
a: 1,
|
|
3782
|
+
an: 1,
|
|
3783
|
+
one: 1,
|
|
3784
|
+
two: 2,
|
|
3785
|
+
three: 3,
|
|
3786
|
+
four: 4,
|
|
3787
|
+
five: 5,
|
|
3788
|
+
six: 6,
|
|
3789
|
+
seven: 7,
|
|
3790
|
+
eight: 8,
|
|
3791
|
+
nine: 9,
|
|
3792
|
+
ten: 10
|
|
3793
|
+
};
|
|
3794
|
+
const loose = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
3795
|
+
function shift(iso, days) {
|
|
3796
|
+
const d = /* @__PURE__ */ new Date(`${iso}T12:00:00Z`);
|
|
3797
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
3798
|
+
return d.toISOString().slice(0, 10);
|
|
3799
|
+
}
|
|
3800
|
+
function weekday(iso) {
|
|
3801
|
+
return (/* @__PURE__ */ new Date(`${iso}T12:00:00Z`)).getUTCDay();
|
|
3802
|
+
}
|
|
3803
|
+
function dayOf$1(y, m, d) {
|
|
3804
|
+
const date = new Date(Date.UTC(y, m - 1, d, 12));
|
|
3805
|
+
if (date.getUTCMonth() !== m - 1 || date.getUTCDate() !== d) return null;
|
|
3806
|
+
return date.toISOString().slice(0, 10);
|
|
3807
|
+
}
|
|
3808
|
+
/** A month and day with no year: this year's, or next year's once it has passed. */
|
|
3809
|
+
function nextMonthDay(today, m, d) {
|
|
3810
|
+
const year = Number(today.slice(0, 4));
|
|
3811
|
+
const thisYear = dayOf$1(year, m, d);
|
|
3812
|
+
if (!thisYear) return null;
|
|
3813
|
+
return thisYear >= today ? thisYear : dayOf$1(year + 1, m, d);
|
|
3814
|
+
}
|
|
3815
|
+
const monthIndex = (word) => {
|
|
3816
|
+
const w = word.toLowerCase().replace(/\.$/, "");
|
|
3817
|
+
if (w.length < 3) return -1;
|
|
3818
|
+
return MONTHS.findIndex((m) => m.startsWith(w) && (w.length >= 3 || m === w));
|
|
3819
|
+
};
|
|
3820
|
+
const DAY_WORD = "(?:sun|mon|tues?|wed(?:nes)?|thu(?:rs?)?|fri|sat(?:ur)?)(?:day)?";
|
|
3821
|
+
const MONTH_WORD = "(?:jan|feb|mar|apr|may|jun|jul|aug|sept?|oct|nov|dec)[a-z]*\\.?";
|
|
3822
|
+
/** Every way of saying a day, longest first so "next friday" wins over "friday". */
|
|
3823
|
+
const WHEN = [
|
|
3824
|
+
"today",
|
|
3825
|
+
"tonight",
|
|
3826
|
+
"tomorrow",
|
|
3827
|
+
"tmrw",
|
|
3828
|
+
"the day after tomorrow",
|
|
3829
|
+
"end of (?:the )?week",
|
|
3830
|
+
"this week",
|
|
3831
|
+
"next week",
|
|
3832
|
+
"end of (?:the )?month",
|
|
3833
|
+
"next month",
|
|
3834
|
+
`in (?:\\d{1,2}|a|an|one|two|three|four|five|six|seven|eight|nine|ten) (?:days?|weeks?)`,
|
|
3835
|
+
`(?:this |next |on )?${DAY_WORD}`,
|
|
3836
|
+
`${MONTH_WORD} \\d{1,2}(?:st|nd|rd|th)?(?:,? \\d{4})?`,
|
|
3837
|
+
`\\d{1,2}(?:st|nd|rd|th)? (?:of )?${MONTH_WORD}(?:,? \\d{4})?`,
|
|
3838
|
+
"\\d{4}-\\d{1,2}-\\d{1,2}",
|
|
3839
|
+
"\\d{1,2}/\\d{1,2}(?:/\\d{2,4})?"
|
|
3840
|
+
].join("|");
|
|
3841
|
+
const LEAD$1 = "(?:by|due|due on|due by|on|before|until|till)";
|
|
3842
|
+
/** The day a phrase names, or null when it names none. */
|
|
3843
|
+
function dayFromWords(phrase, today) {
|
|
3844
|
+
const p = phrase.toLowerCase().replace(/\s+/g, " ").replace(/^on /, "").trim();
|
|
3845
|
+
if (p === "today" || p === "tonight") return today;
|
|
3846
|
+
if (p === "tomorrow" || p === "tmrw") return shift(today, 1);
|
|
3847
|
+
if (p === "the day after tomorrow") return shift(today, 2);
|
|
3848
|
+
const dow = weekday(today);
|
|
3849
|
+
if (/^end of (the )?week$/.test(p) || p === "this week") return shift(today, (5 - dow + 7) % 7);
|
|
3850
|
+
if (p === "next week") return shift(today, (1 - dow + 7) % 7 || 7);
|
|
3851
|
+
if (p === "next month") {
|
|
3852
|
+
const [y, m] = [Number(today.slice(0, 4)), Number(today.slice(5, 7))];
|
|
3853
|
+
return m === 12 ? dayOf$1(y + 1, 1, 1) : dayOf$1(y, m + 1, 1);
|
|
3854
|
+
}
|
|
3855
|
+
if (/^end of (the )?month$/.test(p)) {
|
|
3856
|
+
const [y, m] = [Number(today.slice(0, 4)), Number(today.slice(5, 7))];
|
|
3857
|
+
return shift(m === 12 ? dayOf$1(y + 1, 1, 1) : dayOf$1(y, m + 1, 1), -1);
|
|
3858
|
+
}
|
|
3859
|
+
let m = p.match(/^in (\w+) (day|week)s?$/);
|
|
3860
|
+
if (m) {
|
|
3861
|
+
const n = /^\d+$/.test(m[1]) ? Number(m[1]) : NUMBER_WORDS[m[1]];
|
|
3862
|
+
if (n == null || n > 366) return null;
|
|
3863
|
+
return shift(today, m[2] === "week" ? n * 7 : n);
|
|
3864
|
+
}
|
|
3865
|
+
m = p.match(/^(this |next )?([a-z]+)$/);
|
|
3866
|
+
if (m) {
|
|
3867
|
+
const target = DAYS$1.findIndex((d) => d.startsWith(m[2].slice(0, 3)) && m[2].length >= 3);
|
|
3868
|
+
if (target >= 0 && new RegExp(`^${DAY_WORD}$`).test(m[2])) {
|
|
3869
|
+
const ahead = (target - dow + 7) % 7 || 7;
|
|
3870
|
+
return shift(today, m[1] === "next " ? ahead + 7 : ahead);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
m = p.match(/^([a-z]+\.?) (\d{1,2})(?:st|nd|rd|th)?(?:,? (\d{4}))?$/);
|
|
3874
|
+
if (m && monthIndex(m[1]) >= 0) {
|
|
3875
|
+
const month = monthIndex(m[1]) + 1;
|
|
3876
|
+
return m[3] ? dayOf$1(Number(m[3]), month, Number(m[2])) : nextMonthDay(today, month, Number(m[2]));
|
|
3877
|
+
}
|
|
3878
|
+
m = p.match(/^(\d{1,2})(?:st|nd|rd|th)? (?:of )?([a-z]+\.?)(?:,? (\d{4}))?$/);
|
|
3879
|
+
if (m && monthIndex(m[2]) >= 0) {
|
|
3880
|
+
const month = monthIndex(m[2]) + 1;
|
|
3881
|
+
return m[3] ? dayOf$1(Number(m[3]), month, Number(m[1])) : nextMonthDay(today, month, Number(m[1]));
|
|
3882
|
+
}
|
|
3883
|
+
m = p.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
3884
|
+
if (m) return dayOf$1(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
3885
|
+
m = p.match(/^(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?$/);
|
|
3886
|
+
if (m) {
|
|
3887
|
+
if (!m[3]) return nextMonthDay(today, Number(m[1]), Number(m[2]));
|
|
3888
|
+
return dayOf$1(Number(m[3]) < 100 ? 2e3 + Number(m[3]) : Number(m[3]), Number(m[1]), Number(m[2]));
|
|
3889
|
+
}
|
|
3890
|
+
return null;
|
|
3891
|
+
}
|
|
3892
|
+
const PRIORITY_WORDS = {
|
|
3893
|
+
urgent: "urgent",
|
|
3894
|
+
asap: "urgent",
|
|
3895
|
+
top: "urgent",
|
|
3896
|
+
p0: "urgent",
|
|
3897
|
+
high: "high",
|
|
3898
|
+
p1: "high",
|
|
3899
|
+
medium: "medium",
|
|
3900
|
+
normal: "medium",
|
|
3901
|
+
p2: "medium",
|
|
3902
|
+
low: "low",
|
|
3903
|
+
p3: "low"
|
|
3904
|
+
};
|
|
3905
|
+
/** Removes found spans from the text, tidying the spaces and commas they leave. */
|
|
3906
|
+
function without(text, spans) {
|
|
3907
|
+
let out = text;
|
|
3908
|
+
for (const s of [...spans].sort((a, b) => b.start - a.start)) out = `${out.slice(0, s.start)} ${out.slice(s.end)}`;
|
|
3909
|
+
return out.replace(/\s+([,.;:!?])/g, "$1").replace(/[,;:]+([,.;:!?])/g, "$1").replace(/\s{2,}/g, " ").replace(/(?:[\s,;:–—-]+)$/u, "").replace(/^[\s,;:]+/, "").trim();
|
|
3910
|
+
}
|
|
3911
|
+
/** What a new task's words say, and the title they leave. */
|
|
3912
|
+
function readTaskWords(input, ctx) {
|
|
3913
|
+
const skip = ctx.skip ?? /* @__PURE__ */ new Set();
|
|
3914
|
+
const text = input;
|
|
3915
|
+
const taken = [];
|
|
3916
|
+
const free = (start, end) => !taken.some((t) => start < t.end && end > t.start);
|
|
3917
|
+
let priority = null;
|
|
3918
|
+
if (!skip.has("priority")) for (const re of [
|
|
3919
|
+
/(?:^|\s)!(urgent|asap|high|medium|normal|low|p[0-3])\b/gi,
|
|
3920
|
+
/(?:^|[\s,])(urgent|top|high|medium|normal|low) priority\b/gi,
|
|
3921
|
+
/(?:^|[\s,])priority (urgent|high|medium|normal|low|p[0-3])\b/gi,
|
|
3922
|
+
/(?:^|[\s,])(urgent|asap)$/gi
|
|
3923
|
+
]) {
|
|
3924
|
+
const m = re.exec(text);
|
|
3925
|
+
if (m) {
|
|
3926
|
+
const lead = m[0].length - m[0].trimStart().length;
|
|
3927
|
+
const start = m.index + lead + (m[0].trimStart().startsWith(",") ? 1 : 0);
|
|
3928
|
+
priority = {
|
|
3929
|
+
value: PRIORITY_WORDS[m[1].toLowerCase()],
|
|
3930
|
+
start,
|
|
3931
|
+
end: m.index + m[0].length
|
|
3932
|
+
};
|
|
3933
|
+
taken.push(priority);
|
|
3934
|
+
break;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
let project = null;
|
|
3938
|
+
if (!skip.has("project")) for (const m of text.matchAll(/(?:^|\s)#([\p{L}\p{N}_-]+)/gu)) {
|
|
3939
|
+
const key = loose(m[1]);
|
|
3940
|
+
if (!key) continue;
|
|
3941
|
+
const exact = ctx.projects.filter((p) => loose(p.name) === key);
|
|
3942
|
+
const begins = ctx.projects.filter((p) => loose(p.name).startsWith(key));
|
|
3943
|
+
const hit = exact.length === 1 ? exact[0] : begins.length === 1 ? begins[0] : null;
|
|
3944
|
+
if (!hit) continue;
|
|
3945
|
+
const start = m.index + (m[0].length - m[0].trimStart().length);
|
|
3946
|
+
project = {
|
|
3947
|
+
value: hit.id,
|
|
3948
|
+
start,
|
|
3949
|
+
end: m.index + m[0].length
|
|
3950
|
+
};
|
|
3951
|
+
taken.push(project);
|
|
3952
|
+
break;
|
|
3953
|
+
}
|
|
3954
|
+
let owner = null;
|
|
3955
|
+
if (!skip.has("owner")) {
|
|
3956
|
+
const byKey = (key) => {
|
|
3957
|
+
const full = ctx.people.filter((p) => loose(p.name) === key);
|
|
3958
|
+
if (full.length === 1) return full[0];
|
|
3959
|
+
const first = ctx.people.filter((p) => loose(p.name.split(/\s+/)[0] ?? "") === key);
|
|
3960
|
+
if (first.length === 1) return first[0];
|
|
3961
|
+
const begins = ctx.people.filter((p) => loose(p.name).startsWith(key));
|
|
3962
|
+
return key.length >= 3 && begins.length === 1 ? begins[0] : null;
|
|
3963
|
+
};
|
|
3964
|
+
for (const m of text.matchAll(/(?:^|\s)@([\p{L}\p{N}._-]+)/gu)) {
|
|
3965
|
+
const hit = byKey(loose(m[1]));
|
|
3966
|
+
if (!hit) continue;
|
|
3967
|
+
const start = m.index + (m[0].length - m[0].trimStart().length);
|
|
3968
|
+
if (!free(start, m.index + m[0].length)) continue;
|
|
3969
|
+
owner = {
|
|
3970
|
+
value: hit.id,
|
|
3971
|
+
start,
|
|
3972
|
+
end: m.index + m[0].length
|
|
3973
|
+
};
|
|
3974
|
+
break;
|
|
3975
|
+
}
|
|
3976
|
+
if (!owner) for (const m of text.matchAll(/(?:^|[\s,])(?:assign(?:ed)? (?:it )?to|owner(?: is)?:?) (\p{L}+(?: \p{L}+)?)/giu)) {
|
|
3977
|
+
const words = m[1].split(" ");
|
|
3978
|
+
const two = words.length === 2 ? byKey(loose(m[1])) : null;
|
|
3979
|
+
const one = byKey(loose(words[0]));
|
|
3980
|
+
const hit = two ?? one;
|
|
3981
|
+
if (!hit) continue;
|
|
3982
|
+
const lead = m[0].length - m[0].replace(/^[\s,]+/, "").length;
|
|
3983
|
+
const start = m.index + lead;
|
|
3984
|
+
const end = two ? m.index + m[0].length : m.index + m[0].length - (words.length === 2 ? words[1].length + 1 : 0);
|
|
3985
|
+
if (!free(start, end)) continue;
|
|
3986
|
+
owner = {
|
|
3987
|
+
value: hit.id,
|
|
3988
|
+
start,
|
|
3989
|
+
end
|
|
3990
|
+
};
|
|
3991
|
+
break;
|
|
3992
|
+
}
|
|
3993
|
+
if (owner) taken.push(owner);
|
|
3994
|
+
}
|
|
3995
|
+
let due = null;
|
|
3996
|
+
if (!skip.has("due")) {
|
|
3997
|
+
const led = new RegExp(`(?:^|[\\s,])(${LEAD$1} (${WHEN}))(?=$|[\\s,.;:!?])`, "gi");
|
|
3998
|
+
for (const m of text.matchAll(led)) {
|
|
3999
|
+
const day = dayFromWords(m[2], ctx.today);
|
|
4000
|
+
if (!day) continue;
|
|
4001
|
+
const start = m.index + m[0].indexOf(m[1]);
|
|
4002
|
+
const end = start + m[1].length;
|
|
4003
|
+
if (!free(start, end)) continue;
|
|
4004
|
+
due = {
|
|
4005
|
+
value: day,
|
|
4006
|
+
start,
|
|
4007
|
+
end
|
|
4008
|
+
};
|
|
4009
|
+
break;
|
|
4010
|
+
}
|
|
4011
|
+
if (!due) {
|
|
4012
|
+
const rest = without(text, taken);
|
|
4013
|
+
const tail = new RegExp(`(?:^|[\\s,])(${WHEN})[.!]?$`, "i").exec(rest);
|
|
4014
|
+
const short = tail && /^(?:sun|mon|tue|wed|thu|fri|sat)$/i.test(tail[1]);
|
|
4015
|
+
const said = tail && /\b(?:to|for|from|after|with|since)$/i.test(rest.slice(0, tail.index).trimEnd());
|
|
4016
|
+
if (tail && !short && !said && rest.trim().length > tail[1].length) {
|
|
4017
|
+
const day = dayFromWords(tail[1], ctx.today);
|
|
4018
|
+
if (day) {
|
|
4019
|
+
const at = text.toLowerCase().lastIndexOf(tail[1].toLowerCase());
|
|
4020
|
+
if (at >= 0 && free(at, at + tail[1].length)) due = {
|
|
4021
|
+
value: day,
|
|
4022
|
+
start: at,
|
|
4023
|
+
end: at + tail[1].length
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (due) taken.push(due);
|
|
4029
|
+
}
|
|
4030
|
+
const span = (f) => f ? text.slice(f.start, f.end).trim() : "";
|
|
4031
|
+
let title = without(text, taken);
|
|
4032
|
+
const first = text.search(/\S/);
|
|
4033
|
+
if (taken.some((t) => t.start === first)) title = title.charAt(0).toUpperCase() + title.slice(1);
|
|
4034
|
+
return {
|
|
4035
|
+
title,
|
|
4036
|
+
due: due ? {
|
|
4037
|
+
value: due.value,
|
|
4038
|
+
text: span(due)
|
|
4039
|
+
} : null,
|
|
4040
|
+
owner: owner ? {
|
|
4041
|
+
value: owner.value,
|
|
4042
|
+
text: span(owner)
|
|
4043
|
+
} : null,
|
|
4044
|
+
priority: priority ? {
|
|
4045
|
+
value: priority.value,
|
|
4046
|
+
text: span(priority)
|
|
4047
|
+
} : null,
|
|
4048
|
+
project: project ? {
|
|
4049
|
+
value: project.value,
|
|
4050
|
+
text: span(project)
|
|
4051
|
+
} : null
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
// src/lib/swatches.ts
|
|
4056
|
+
var SWATCHES, HEALTH_LABEL$1, TASK_COLUMNS, TASK_STATUS_LABEL, PRIORITY_LABEL, CONSTRAINT_KIND_LABEL, CONSTRAINT_KIND_HINT;
|
|
4057
|
+
var init_swatches = __esmMin((() => {
|
|
4058
|
+
SWATCHES = [
|
|
4059
|
+
"pine",
|
|
4060
|
+
"clay",
|
|
4061
|
+
"sage",
|
|
4062
|
+
"sand",
|
|
4063
|
+
"slate",
|
|
4064
|
+
"sea",
|
|
4065
|
+
"rust",
|
|
4066
|
+
"moss"
|
|
4067
|
+
];
|
|
4068
|
+
HEALTH_LABEL$1 = {
|
|
4069
|
+
"on-track": "On track",
|
|
4070
|
+
"at-risk": "At risk",
|
|
4071
|
+
"off-track": "Off track",
|
|
4072
|
+
paused: "Paused",
|
|
4073
|
+
done: "Done"
|
|
4074
|
+
};
|
|
4075
|
+
TASK_COLUMNS = [
|
|
4076
|
+
"backlog",
|
|
4077
|
+
"todo",
|
|
4078
|
+
"doing",
|
|
4079
|
+
"review",
|
|
4080
|
+
"done"
|
|
4081
|
+
];
|
|
4082
|
+
TASK_STATUS_LABEL = {
|
|
4083
|
+
backlog: "Backlog",
|
|
4084
|
+
todo: "To do",
|
|
4085
|
+
doing: "In play",
|
|
4086
|
+
review: "Review",
|
|
4087
|
+
done: "Done"
|
|
4088
|
+
};
|
|
4089
|
+
PRIORITY_LABEL = {
|
|
4090
|
+
low: "Low",
|
|
4091
|
+
medium: "Medium",
|
|
4092
|
+
high: "High",
|
|
4093
|
+
urgent: "Urgent"
|
|
4094
|
+
};
|
|
4095
|
+
CONSTRAINT_KIND_LABEL = {
|
|
4096
|
+
resource: "A person",
|
|
4097
|
+
policy: "A policy",
|
|
4098
|
+
external: "Outside the building",
|
|
4099
|
+
collision: "A collision"
|
|
4100
|
+
};
|
|
4101
|
+
CONSTRAINT_KIND_HINT = {
|
|
4102
|
+
resource: "Someone whose calendar sets the date.",
|
|
4103
|
+
policy: "A rule that creates the wait.",
|
|
4104
|
+
external: "A vendor, a city, a review board.",
|
|
4105
|
+
collision: "Two things sharing one window."
|
|
4106
|
+
};
|
|
4107
|
+
}));
|
|
4108
|
+
|
|
3756
4109
|
// src/lib/raid.ts
|
|
3757
4110
|
/** Still needs someone's attention: not closed, resolved, decided or superseded. */
|
|
3758
4111
|
function isActive(item) {
|
|
@@ -3963,67 +4316,13 @@ var init_raid = __esmMin((() => {
|
|
|
3963
4316
|
ids = (v) => Array.isArray(v) ? [...new Set(v.filter((x) => typeof x === "string" && !!x))] : [];
|
|
3964
4317
|
}));
|
|
3965
4318
|
|
|
3966
|
-
// src/lib/swatches.ts
|
|
3967
|
-
var SWATCHES, HEALTH_LABEL$1, TASK_COLUMNS, TASK_STATUS_LABEL, PRIORITY_LABEL, CONSTRAINT_KIND_LABEL, CONSTRAINT_KIND_HINT;
|
|
3968
|
-
var init_swatches = __esmMin((() => {
|
|
3969
|
-
SWATCHES = [
|
|
3970
|
-
"pine",
|
|
3971
|
-
"clay",
|
|
3972
|
-
"sage",
|
|
3973
|
-
"sand",
|
|
3974
|
-
"slate",
|
|
3975
|
-
"sea",
|
|
3976
|
-
"rust",
|
|
3977
|
-
"moss"
|
|
3978
|
-
];
|
|
3979
|
-
HEALTH_LABEL$1 = {
|
|
3980
|
-
"on-track": "On track",
|
|
3981
|
-
"at-risk": "At risk",
|
|
3982
|
-
"off-track": "Off track",
|
|
3983
|
-
paused: "Paused",
|
|
3984
|
-
done: "Done"
|
|
3985
|
-
};
|
|
3986
|
-
TASK_COLUMNS = [
|
|
3987
|
-
"backlog",
|
|
3988
|
-
"todo",
|
|
3989
|
-
"doing",
|
|
3990
|
-
"review",
|
|
3991
|
-
"done"
|
|
3992
|
-
];
|
|
3993
|
-
TASK_STATUS_LABEL = {
|
|
3994
|
-
backlog: "Backlog",
|
|
3995
|
-
todo: "To do",
|
|
3996
|
-
doing: "In play",
|
|
3997
|
-
review: "Review",
|
|
3998
|
-
done: "Done"
|
|
3999
|
-
};
|
|
4000
|
-
PRIORITY_LABEL = {
|
|
4001
|
-
low: "Low",
|
|
4002
|
-
medium: "Medium",
|
|
4003
|
-
high: "High",
|
|
4004
|
-
urgent: "Urgent"
|
|
4005
|
-
};
|
|
4006
|
-
CONSTRAINT_KIND_LABEL = {
|
|
4007
|
-
resource: "A person",
|
|
4008
|
-
policy: "A policy",
|
|
4009
|
-
external: "Outside the building",
|
|
4010
|
-
collision: "A collision"
|
|
4011
|
-
};
|
|
4012
|
-
CONSTRAINT_KIND_HINT = {
|
|
4013
|
-
resource: "Someone whose calendar sets the date.",
|
|
4014
|
-
policy: "A rule that creates the wait.",
|
|
4015
|
-
external: "A vendor, a city, a review board.",
|
|
4016
|
-
collision: "Two things sharing one window."
|
|
4017
|
-
};
|
|
4018
|
-
}));
|
|
4019
|
-
|
|
4020
4319
|
// src/tui/create.ts
|
|
4021
4320
|
init_date_fns();
|
|
4022
4321
|
init_format();
|
|
4322
|
+
init_swatches();
|
|
4023
4323
|
init_indexes();
|
|
4024
4324
|
init_date_text();
|
|
4025
4325
|
init_raid();
|
|
4026
|
-
init_swatches();
|
|
4027
4326
|
const scale = (labels) => [
|
|
4028
4327
|
1,
|
|
4029
4328
|
2,
|
|
@@ -4064,6 +4363,28 @@ function planFromChoices(data, finished, name, programId) {
|
|
|
4064
4363
|
...rest.map((p) => choice(p, false))
|
|
4065
4364
|
];
|
|
4066
4365
|
}
|
|
4366
|
+
/**
|
|
4367
|
+
* The terminal's new task box reads its title as the website's does:
|
|
4368
|
+
* "Book the venue @Quinn by Friday !high #Website" adds “Book the venue”
|
|
4369
|
+
* for Quinn, due Friday, high priority, in Website.
|
|
4370
|
+
*/
|
|
4371
|
+
function readNewTask(text, data, today) {
|
|
4372
|
+
const words = readTaskWords(text, {
|
|
4373
|
+
today,
|
|
4374
|
+
people: data.people,
|
|
4375
|
+
projects: data.projects
|
|
4376
|
+
});
|
|
4377
|
+
const bits = [
|
|
4378
|
+
words.owner && (data.people.find((p) => p.id === words.owner.value)?.name ?? "someone"),
|
|
4379
|
+
words.due && `due ${longDate(words.due.value)}`,
|
|
4380
|
+
words.priority && `${PRIORITY_LABEL[words.priority.value].toLowerCase()} priority`,
|
|
4381
|
+
words.project && (data.projects.find((p) => p.id === words.project.value)?.name ?? "project")
|
|
4382
|
+
].filter((b) => Boolean(b));
|
|
4383
|
+
return {
|
|
4384
|
+
words,
|
|
4385
|
+
line: bits.length && words.title ? [`Adds “${words.title}”`, ...bits].join(" · ") : null
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
4067
4388
|
const CREATE_TITLE = {
|
|
4068
4389
|
portfolio: "New portfolio",
|
|
4069
4390
|
project: "New project",
|
|
@@ -6334,7 +6655,7 @@ applyTheme("dark");
|
|
|
6334
6655
|
* from, the key hints along the bottom, health and priority marks.
|
|
6335
6656
|
*/
|
|
6336
6657
|
/** One line of text. Enter keeps it, Esc leaves it as it was. */
|
|
6337
|
-
function TextField({ label, initial = "", placeholder, hint, onSubmit, onCancel }) {
|
|
6658
|
+
function TextField({ label, initial = "", placeholder, hint, hintFor, onSubmit, onCancel }) {
|
|
6338
6659
|
const [value, setShown] = useState(initial);
|
|
6339
6660
|
const typed = useRef(initial);
|
|
6340
6661
|
const setValue = (next) => {
|
|
@@ -6379,7 +6700,7 @@ function TextField({ label, initial = "", placeholder, hint, onSubmit, onCancel
|
|
|
6379
6700
|
/* @__PURE__ */ jsx(Text, {
|
|
6380
6701
|
color: theme.muted,
|
|
6381
6702
|
wrap: "truncate-end",
|
|
6382
|
-
children: hint ?? "Enter to keep · Esc to leave it"
|
|
6703
|
+
children: hintFor?.(value) ?? hint ?? "Enter to keep · Esc to leave it"
|
|
6383
6704
|
})
|
|
6384
6705
|
]
|
|
6385
6706
|
});
|
|
@@ -12355,6 +12676,8 @@ function Splash({ opening }) {
|
|
|
12355
12676
|
|
|
12356
12677
|
// src/tui/views/overlays.tsx
|
|
12357
12678
|
init_search();
|
|
12679
|
+
init_clock();
|
|
12680
|
+
init_format();
|
|
12358
12681
|
/** Find a task by key or words; Enter opens it. */
|
|
12359
12682
|
function SearchBox({ width, onOpen, onClose }) {
|
|
12360
12683
|
const tasks = useHarbor((s) => s.tasks);
|
|
@@ -12444,11 +12767,19 @@ function SearchBox({ width, onOpen, onClose }) {
|
|
|
12444
12767
|
]
|
|
12445
12768
|
});
|
|
12446
12769
|
}
|
|
12447
|
-
/**
|
|
12770
|
+
/**
|
|
12771
|
+
* Add a task: which project (when it isn't known), then its title, read in
|
|
12772
|
+
* plain words as the website reads it (@name, by Friday, !high, #project).
|
|
12773
|
+
*/
|
|
12448
12774
|
function NewTask({ projectId, status, width, onDone, onFull }) {
|
|
12449
12775
|
const { store } = useDeps();
|
|
12450
12776
|
const projects = useHarbor((s) => s.projects);
|
|
12777
|
+
const people = useHarbor((s) => s.people);
|
|
12451
12778
|
const me = useHarbor((s) => s.currentUserId);
|
|
12779
|
+
const read = (title) => readNewTask(title, {
|
|
12780
|
+
people,
|
|
12781
|
+
projects
|
|
12782
|
+
}, todayISO(clockNow()));
|
|
12452
12783
|
const [project, setProject] = useState(() => projectId ?? soleOpenProject(projects));
|
|
12453
12784
|
const chosen = projects.find((p) => p.id === project);
|
|
12454
12785
|
if (!chosen) return /* @__PURE__ */ jsx(Box, {
|
|
@@ -12468,21 +12799,24 @@ function NewTask({ projectId, status, width, onDone, onFull }) {
|
|
|
12468
12799
|
width,
|
|
12469
12800
|
children: /* @__PURE__ */ jsx(TextField, {
|
|
12470
12801
|
label: `New task in ${chosen.name}${status ? ` · ${STATUS_LABEL[status]}` : ""}`,
|
|
12471
|
-
placeholder: "What needs doing?",
|
|
12802
|
+
placeholder: "What needs doing? Add @name, by Friday, #project",
|
|
12472
12803
|
hint: `Enter adds it (yours, ${STATUS_LABEL[status ?? "todo"].toLowerCase()}) · Esc leaves it`,
|
|
12804
|
+
hintFor: (title) => read(title).line,
|
|
12473
12805
|
onCancel: () => onDone(null),
|
|
12474
12806
|
onSubmit: (title) => {
|
|
12475
12807
|
if (!title.trim()) return onDone(null);
|
|
12476
|
-
const
|
|
12808
|
+
const { words } = read(title.trim());
|
|
12809
|
+
const projectId = words.project?.value ?? chosen.id;
|
|
12810
|
+
const full = roomFor(store, projectId, status ?? "todo");
|
|
12477
12811
|
if (full) return onFull(full);
|
|
12478
12812
|
onDone(store.getState().addTask({
|
|
12479
|
-
projectId
|
|
12480
|
-
title: title.trim(),
|
|
12813
|
+
projectId,
|
|
12814
|
+
title: words.title || title.trim(),
|
|
12481
12815
|
description: "",
|
|
12482
12816
|
status: status ?? "todo",
|
|
12483
|
-
priority: "medium",
|
|
12484
|
-
assigneeId: me || null,
|
|
12485
|
-
dueDate: null
|
|
12817
|
+
priority: words.priority?.value ?? "medium",
|
|
12818
|
+
assigneeId: words.owner?.value ?? (me || null),
|
|
12819
|
+
dueDate: words.due?.value ?? null
|
|
12486
12820
|
}));
|
|
12487
12821
|
}
|
|
12488
12822
|
})
|
|
@@ -13298,6 +13632,36 @@ function applyDock(actions, projectId, changes) {
|
|
|
13298
13632
|
return made;
|
|
13299
13633
|
}
|
|
13300
13634
|
|
|
13635
|
+
// src/lib/time-limits.ts
|
|
13636
|
+
var MAX_ENTRY_MINUTES;
|
|
13637
|
+
var init_time_limits = __esmMin((() => {
|
|
13638
|
+
MAX_ENTRY_MINUTES = 1440;
|
|
13639
|
+
}));
|
|
13640
|
+
|
|
13641
|
+
// src/lib/time-log.ts
|
|
13642
|
+
/** "1h 30m", "45m", "12h". */
|
|
13643
|
+
function formatMinutes(total) {
|
|
13644
|
+
const n = Math.max(0, Math.round(total));
|
|
13645
|
+
const h = Math.floor(n / 60);
|
|
13646
|
+
const m = n % 60;
|
|
13647
|
+
if (h === 0) return `${m}m`;
|
|
13648
|
+
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
13649
|
+
}
|
|
13650
|
+
/** Hours with up to two decimals, for spreadsheets: 90 → 1.5. */
|
|
13651
|
+
function hours(minutes) {
|
|
13652
|
+
return Math.round(minutes / 60 * 100) / 100;
|
|
13653
|
+
}
|
|
13654
|
+
/** All the time on one task. */
|
|
13655
|
+
function taskMinutes(task) {
|
|
13656
|
+
let n = 0;
|
|
13657
|
+
for (const e of task.time ?? []) n += e.minutes;
|
|
13658
|
+
return n;
|
|
13659
|
+
}
|
|
13660
|
+
/** A task's entries, newest day first. */
|
|
13661
|
+
function entriesNewestFirst(task) {
|
|
13662
|
+
return [...task.time ?? []].sort((a, b) => (b.on + b.at).localeCompare(a.on + a.at));
|
|
13663
|
+
}
|
|
13664
|
+
|
|
13301
13665
|
// node_modules/zod/v4/core/util.js
|
|
13302
13666
|
function getEnumValues(entries) {
|
|
13303
13667
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -18813,10 +19177,17 @@ function harborExport(data, harbor, now) {
|
|
|
18813
19177
|
* through Import.
|
|
18814
19178
|
*/
|
|
18815
19179
|
function allTasksCsv(data) {
|
|
19180
|
+
return tasksSheetCsv(data, data.tasks);
|
|
19181
|
+
}
|
|
19182
|
+
/**
|
|
19183
|
+
* These tasks, one row each, as `allTasksCsv` lays them out: the whole
|
|
19184
|
+
* harbor's, or a program's for a sheet link (src/lib/sheet-feed.ts).
|
|
19185
|
+
*/
|
|
19186
|
+
function tasksSheetCsv(data, rows) {
|
|
18816
19187
|
const projects = byId(data.projects);
|
|
18817
19188
|
const programs = byId(data.programs);
|
|
18818
19189
|
const people = byId(data.people);
|
|
18819
|
-
const tasks = byId(
|
|
19190
|
+
const tasks = byId(rows);
|
|
18820
19191
|
const keyOf = (t) => taskKey(t, projects.get(t.projectId)) ?? t.title;
|
|
18821
19192
|
return toCsv([
|
|
18822
19193
|
"Key",
|
|
@@ -18830,8 +19201,9 @@ function allTasksCsv(data) {
|
|
|
18830
19201
|
"Done",
|
|
18831
19202
|
"Blocked by",
|
|
18832
19203
|
"Created",
|
|
18833
|
-
"Description"
|
|
18834
|
-
|
|
19204
|
+
"Description",
|
|
19205
|
+
"Hours logged"
|
|
19206
|
+
], rows.map((t) => {
|
|
18835
19207
|
const project = projects.get(t.projectId);
|
|
18836
19208
|
const program = project ? programs.get(project.programId) : void 0;
|
|
18837
19209
|
return [
|
|
@@ -18846,7 +19218,8 @@ function allTasksCsv(data) {
|
|
|
18846
19218
|
t.status === "done" ? "yes" : "no",
|
|
18847
19219
|
blockerIds(t).map((id) => tasks.get(id)).filter((b) => Boolean(b)).map(keyOf).join("; "),
|
|
18848
19220
|
t.createdAt?.slice(0, 10) ?? "",
|
|
18849
|
-
t.description
|
|
19221
|
+
t.description,
|
|
19222
|
+
t.time?.length ? hours(taskMinutes(t)) : ""
|
|
18850
19223
|
];
|
|
18851
19224
|
}));
|
|
18852
19225
|
}
|
|
@@ -19501,6 +19874,16 @@ var init_fog = __esmMin((() => {
|
|
|
19501
19874
|
}));
|
|
19502
19875
|
|
|
19503
19876
|
// src/lib/mcp/tools.ts
|
|
19877
|
+
/**
|
|
19878
|
+
* What an agent can read about a harbor, as MCP tools. Each tool is a pure
|
|
19879
|
+
* function of one harbor's document (health already stamped) and who is
|
|
19880
|
+
* asking, so it is tested against the demo without a server. The route in
|
|
19881
|
+
* src/routes/api/mcp.ts signs the caller in, picks the harbor and loads it.
|
|
19882
|
+
*
|
|
19883
|
+
* Read only: nothing here changes a workspace. Answers carry ids so an agent
|
|
19884
|
+
* can ask about one thing next, and every list is capped and pageable, since
|
|
19885
|
+
* a harbor can hold thousands of tasks.
|
|
19886
|
+
*/
|
|
19504
19887
|
init_thanks();
|
|
19505
19888
|
init_task_keys();
|
|
19506
19889
|
init_brief();
|
|
@@ -19530,6 +19913,20 @@ init_passage();
|
|
|
19530
19913
|
init_board();
|
|
19531
19914
|
init_holidays();
|
|
19532
19915
|
init_soundings();
|
|
19916
|
+
/** A task's logged time for get_task: the total and the latest entries. */
|
|
19917
|
+
function timeOut(t, people) {
|
|
19918
|
+
const entries = entriesNewestFirst(t);
|
|
19919
|
+
return {
|
|
19920
|
+
total: formatMinutes(taskMinutes(t)),
|
|
19921
|
+
minutes: taskMinutes(t),
|
|
19922
|
+
latest: entries.slice(0, 10).map((e) => ({
|
|
19923
|
+
on: e.on,
|
|
19924
|
+
by: nameOf$2(people, e.personId),
|
|
19925
|
+
time: formatMinutes(e.minutes),
|
|
19926
|
+
...e.note ? { note: e.note } : {}
|
|
19927
|
+
}))
|
|
19928
|
+
};
|
|
19929
|
+
}
|
|
19533
19930
|
/**
|
|
19534
19931
|
* Where the website opens one task or project, switching to its harbor
|
|
19535
19932
|
* first (src/routes/open.tsx). Nothing without an origin.
|
|
@@ -20229,7 +20626,7 @@ const HARBOR_TOOLS = [
|
|
|
20229
20626
|
tool({
|
|
20230
20627
|
name: "get_task",
|
|
20231
20628
|
title: "Task details",
|
|
20232
|
-
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), and its url on the website." + DATA_NOTE,
|
|
20629
|
+
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), and its url on the website." + DATA_NOTE,
|
|
20233
20630
|
input: object({
|
|
20234
20631
|
harbor: harborArg,
|
|
20235
20632
|
task: string().min(1).max(100).describe("Task id or key (\"RR-42\"), from search_tasks or my_work.")
|
|
@@ -20278,6 +20675,7 @@ const HARBOR_TOOLS = [
|
|
|
20278
20675
|
step: i.title,
|
|
20279
20676
|
done: i.done
|
|
20280
20677
|
})),
|
|
20678
|
+
...t.time?.length ? { timeLogged: timeOut(t, people) } : {},
|
|
20281
20679
|
waitingOn: blockersOf(t, data.tasks).map(brief),
|
|
20282
20680
|
waitedOnBy: waiters.map(brief),
|
|
20283
20681
|
...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
|
|
@@ -21340,10 +21738,11 @@ function viewerUpdate(state, partial) {
|
|
|
21340
21738
|
}
|
|
21341
21739
|
/**
|
|
21342
21740
|
* Whether this saved harbor can't be changed from here: the person is a
|
|
21343
|
-
* viewer,
|
|
21741
|
+
* viewer, its free trial has ended with nobody keeping it (keep.ts), or it
|
|
21742
|
+
* was opened offline from this device's copy (`offlineCopy`).
|
|
21344
21743
|
*/
|
|
21345
21744
|
function readOnlyHarbor(s) {
|
|
21346
|
-
return s.cloud && (s.myRole === "viewer" || Boolean(s.keepPaused));
|
|
21745
|
+
return s.cloud && (s.myRole === "viewer" || Boolean(s.keepPaused) || Boolean(s.offlineCopy));
|
|
21347
21746
|
}
|
|
21348
21747
|
var VIEWER_LOCAL, documentKeys;
|
|
21349
21748
|
var init_view_only = __esmMin((() => {
|
|
@@ -21798,6 +22197,7 @@ function sessionSlice({ set, get, write }) {
|
|
|
21798
22197
|
}),
|
|
21799
22198
|
setWorkspaceVersion: (workspaceVersion) => set({ workspaceVersion }),
|
|
21800
22199
|
setPhase: (phase) => set({ phase }),
|
|
22200
|
+
setOfflineCopy: (offlineCopy) => set({ offlineCopy }),
|
|
21801
22201
|
setMyRole: (myRole) => set({ myRole }),
|
|
21802
22202
|
setHarborName: (harborName) => set({ harborName }),
|
|
21803
22203
|
setKeep: (k) => set({
|
|
@@ -22867,6 +23267,37 @@ function tasksSlice({ set, get, write }) {
|
|
|
22867
23267
|
checklist: (t.checklist ?? []).filter((i) => i.id !== itemId)
|
|
22868
23268
|
} : t) });
|
|
22869
23269
|
},
|
|
23270
|
+
logTime: (taskId, entry) => {
|
|
23271
|
+
const minutes = Math.round(entry.minutes);
|
|
23272
|
+
if (!Number.isFinite(minutes) || minutes < 1 || minutes > 1440) return null;
|
|
23273
|
+
const task = get().tasks.find((t) => t.id === taskId);
|
|
23274
|
+
if (!task || (task.time?.length ?? 0) >= 500) return null;
|
|
23275
|
+
const personId = entry.personId ?? get().currentUserId;
|
|
23276
|
+
if (!personId) return null;
|
|
23277
|
+
const id = uid("tm");
|
|
23278
|
+
const note = entry.note?.trim().slice(0, 200);
|
|
23279
|
+
const row = {
|
|
23280
|
+
id,
|
|
23281
|
+
personId,
|
|
23282
|
+
minutes,
|
|
23283
|
+
on: entry.on ?? todayISO(),
|
|
23284
|
+
...note ? { note } : {},
|
|
23285
|
+
at: stamp()
|
|
23286
|
+
};
|
|
23287
|
+
set({ tasks: get().tasks.map((t) => t.id === taskId ? {
|
|
23288
|
+
...t,
|
|
23289
|
+
touchedAt: stamp(),
|
|
23290
|
+
time: [...t.time ?? [], row]
|
|
23291
|
+
} : t) });
|
|
23292
|
+
return id;
|
|
23293
|
+
},
|
|
23294
|
+
removeTime: (taskId, entryId) => {
|
|
23295
|
+
set({ tasks: get().tasks.map((t) => t.id === taskId && t.time?.some((e) => e.id === entryId) ? {
|
|
23296
|
+
...t,
|
|
23297
|
+
touchedAt: stamp(),
|
|
23298
|
+
time: t.time.filter((e) => e.id !== entryId)
|
|
23299
|
+
} : t) });
|
|
23300
|
+
},
|
|
22870
23301
|
addAttachment: (a) => {
|
|
22871
23302
|
const id = a.id ?? uid("f");
|
|
22872
23303
|
const row = {
|
|
@@ -23090,6 +23521,7 @@ var init_tasks = __esmMin((() => {
|
|
|
23090
23521
|
init_automations();
|
|
23091
23522
|
init_focus();
|
|
23092
23523
|
init_format();
|
|
23524
|
+
init_time_limits();
|
|
23093
23525
|
init_soundings();
|
|
23094
23526
|
init_clock();
|
|
23095
23527
|
init_utils();
|
|
@@ -24828,6 +25260,7 @@ function shoreService(raw) {
|
|
|
24828
25260
|
if ((host === "discord.com" || host === "discordapp.com" || host === "ptb.discord.com" || host === "canary.discord.com") && /^\/api\/webhooks\/\d+\/[\w-]+$/.test(path)) return "discord";
|
|
24829
25261
|
if (/^[a-z0-9-]+\.webhook\.office\.com$/.test(host) && path.startsWith("/webhookb2/")) return "teams";
|
|
24830
25262
|
if ((/^[a-z0-9.-]+\.logic\.azure\.com$/.test(host) || /^[a-z0-9.-]+\.environment\.api\.powerplatform\.com$/.test(host)) && path.includes("/workflows/")) return "teams-workflow";
|
|
25263
|
+
if (host === "chat.googleapis.com" && /^\/v1\/spaces\/[\w-]+\/messages$/.test(path) && url.searchParams.get("key") && url.searchParams.get("token")) return "google-chat";
|
|
24831
25264
|
return null;
|
|
24832
25265
|
}
|
|
24833
25266
|
/** "hooks.slack.com/…8f2Q": enough to recognise an address without showing the secret in it. */
|
|
@@ -24846,7 +25279,8 @@ var init_shore = __esmMin((() => {
|
|
|
24846
25279
|
slack: "Slack",
|
|
24847
25280
|
discord: "Discord",
|
|
24848
25281
|
teams: "Teams",
|
|
24849
|
-
"teams-workflow": "Teams"
|
|
25282
|
+
"teams-workflow": "Teams",
|
|
25283
|
+
"google-chat": "Google Chat"
|
|
24850
25284
|
};
|
|
24851
25285
|
SHORE_KINDS = [
|
|
24852
25286
|
{
|
|
@@ -24938,7 +25372,7 @@ function memoryHarbor(start, opts) {
|
|
|
24938
25372
|
}
|
|
24939
25373
|
let row = hooks.get(program.id);
|
|
24940
25374
|
if (ask.url || ask.on?.length || ask.off?.length) {
|
|
24941
|
-
if (ask.url && !shoreService(ask.url)) throw new TransportError("That isn’t a Slack, Discord or
|
|
25375
|
+
if (ask.url && !shoreService(ask.url)) throw new TransportError("That isn’t a Slack, Discord, Teams or Google Chat incoming-webhook address.", 400);
|
|
24942
25376
|
const url = ask.url || row?.url;
|
|
24943
25377
|
if (!url) throw new TransportError("Paste the channel’s address first.", 400);
|
|
24944
25378
|
row = {
|
|
@@ -26555,7 +26989,7 @@ function LifeboatsPanel({ id, width, height, go, active, onClose }) {
|
|
|
26555
26989
|
|
|
26556
26990
|
// src/tui/views/shore.tsx
|
|
26557
26991
|
/**
|
|
26558
|
-
* Ship to shore in the terminal: a program's Slack, Discord or
|
|
26992
|
+
* Ship to shore in the terminal: a program's Slack, Discord, Teams or Google Chat channel
|
|
26559
26993
|
* and which posts go there, as the website's dialog sets it (shore.ts). The
|
|
26560
26994
|
* address is a secret, so once saved only a hint of it shows. Asks /api/app's
|
|
26561
26995
|
* `shore` (the in-memory server keeps a pretend one).
|
|
@@ -26608,7 +27042,7 @@ function ShorePanel({ id, width, height, go, active, onClose }) {
|
|
|
26608
27042
|
if (input === "p") return go.ask("The channel's incoming-webhook address", (text) => {
|
|
26609
27043
|
const url = text.trim();
|
|
26610
27044
|
if (!url) return;
|
|
26611
|
-
if (!shoreService(url)) return go.notify("That isn't a Slack, Discord or
|
|
27045
|
+
if (!shoreService(url)) return go.notify("That isn't a Slack, Discord, Teams or Google Chat incoming-webhook address. It starts https://hooks.slack.com/, https://discord.com/api/webhooks/, https://chat.googleapis.com/ or a Teams workflow address.");
|
|
26612
27046
|
send({ url }, (a) => `Connected to ${a.hook?.service ?? "the channel"}. Turn on what it posts.`);
|
|
26613
27047
|
});
|
|
26614
27048
|
if (!hook) return;
|
|
@@ -26654,7 +27088,7 @@ function ShorePanel({ id, width, height, go, active, onClose }) {
|
|
|
26654
27088
|
/* @__PURE__ */ jsx(Text, {
|
|
26655
27089
|
color: theme.muted,
|
|
26656
27090
|
wrap: "wrap",
|
|
26657
|
-
children: "Harbor posts into one Slack, Discord or
|
|
27091
|
+
children: "Harbor posts into one Slack, Discord, Teams or Google Chat channel for this program instead of sending mail. Each post links back here."
|
|
26658
27092
|
}),
|
|
26659
27093
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
26660
27094
|
failed ? /* @__PURE__ */ jsx(Text, {
|