@harborppm/cli 0.76.0 → 0.78.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/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$1, pad$1, TOKENS, TOKEN, ISO, MINUTE;
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$1 = [
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$1[d.getDay()],
166
- EEE: (d) => DAYS$1[d.getDay()].slice(0, 3)
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
- /** Add a task: which project (when it isn't known), then its title. */
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 full = roomFor(store, chosen.id, status ?? "todo");
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: chosen.id,
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
  })
@@ -12885,6 +13219,7 @@ const PAGES = {
12885
13219
  programs: "/programs",
12886
13220
  projects: "/projects",
12887
13221
  risks: "/risks",
13222
+ goals: "/goals",
12888
13223
  calendar: "/calendar",
12889
13224
  people: "/people"
12890
13225
  };
@@ -13298,6 +13633,117 @@ function applyDock(actions, projectId, changes) {
13298
13633
  return made;
13299
13634
  }
13300
13635
 
13636
+ // src/lib/time-limits.ts
13637
+ var MAX_ENTRY_MINUTES;
13638
+ var init_time_limits = __esmMin((() => {
13639
+ MAX_ENTRY_MINUTES = 1440;
13640
+ }));
13641
+
13642
+ // src/lib/time-log.ts
13643
+ /** "1h 30m", "45m", "12h". */
13644
+ function formatMinutes(total) {
13645
+ const n = Math.max(0, Math.round(total));
13646
+ const h = Math.floor(n / 60);
13647
+ const m = n % 60;
13648
+ if (h === 0) return `${m}m`;
13649
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
13650
+ }
13651
+ /** Hours with up to two decimals, for spreadsheets: 90 → 1.5. */
13652
+ function hours(minutes) {
13653
+ return Math.round(minutes / 60 * 100) / 100;
13654
+ }
13655
+ /** All the time on one task. */
13656
+ function taskMinutes(task) {
13657
+ let n = 0;
13658
+ for (const e of task.time ?? []) n += e.minutes;
13659
+ return n;
13660
+ }
13661
+ /** A task's entries, newest day first. */
13662
+ function entriesNewestFirst(task) {
13663
+ return [...task.time ?? []].sort((a, b) => (b.on + b.at).localeCompare(a.on + a.at));
13664
+ }
13665
+
13666
+ // src/lib/goals.ts
13667
+ init_indexes();
13668
+ const GOAL_STATUS_LABEL = {
13669
+ achieved: "Achieved",
13670
+ "on-track": "On track",
13671
+ "at-risk": "At risk",
13672
+ "off-track": "Off track",
13673
+ "not-started": "Not started"
13674
+ };
13675
+ const clamp = (n) => Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;
13676
+ /** The projects a work key result counts: those it names, and every project in a program it names. */
13677
+ function linkedProjects(kr, projects) {
13678
+ const ids = new Set(kr.linkIds ?? []);
13679
+ if (ids.size === 0) return [];
13680
+ return projects.filter((p) => ids.has(p.id) || ids.has(p.programId));
13681
+ }
13682
+ /** How far along one key result is, 0 to 1. */
13683
+ function keyResultProgress(kr, work) {
13684
+ if (kr.kind === "number") {
13685
+ const start = kr.start ?? 0;
13686
+ const span = (kr.target ?? 0) - start;
13687
+ if (span === 0) return (kr.current ?? start) >= (kr.target ?? 0) ? 1 : 0;
13688
+ return clamp(((kr.current ?? start) - start) / span);
13689
+ }
13690
+ let done = 0;
13691
+ let all = 0;
13692
+ for (const p of linkedProjects(kr, work.projects)) for (const t of tasksOf(work.tasks, p.id)) {
13693
+ all++;
13694
+ if (t.status === "done") done++;
13695
+ }
13696
+ return all === 0 ? 0 : done / all;
13697
+ }
13698
+ /** How far along a goal is: the average of its key results, 0 to 1. */
13699
+ function goalProgress(goal, work) {
13700
+ const krs = goal.keyResults ?? [];
13701
+ if (krs.length === 0) return 0;
13702
+ return krs.reduce((sum, kr) => sum + keyResultProgress(kr, work), 0) / krs.length;
13703
+ }
13704
+ /** How much of the goal's time has gone by, 0 to 1, from when it was set to its due day. */
13705
+ function timeGone(goal, today) {
13706
+ if (!goal.due) return null;
13707
+ const start = Date.parse(`${goal.createdAt.slice(0, 10)}T00:00:00Z`);
13708
+ const end = Date.parse(`${goal.due}T00:00:00Z`);
13709
+ const now = Date.parse(`${today}T00:00:00Z`);
13710
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
13711
+ if (end <= start) return now >= end ? 1 : 0;
13712
+ return clamp((now - start) / (end - start));
13713
+ }
13714
+ /**
13715
+ * Where a goal stands. Achieved at 100%. Otherwise, with a due day, progress
13716
+ * is held against the share of time gone by: within 10 points is on track,
13717
+ * within 25 at risk, further behind (or past due) off track. Without a due
13718
+ * day, any progress is on track.
13719
+ */
13720
+ function goalStatus(progress, goal, today) {
13721
+ if (progress >= 1) return "achieved";
13722
+ const gone = timeGone(goal, today);
13723
+ if (gone == null) return progress > 0 ? "on-track" : "not-started";
13724
+ if (goal.due && today > goal.due) return "off-track";
13725
+ if (progress === 0 && gone < .1) return "not-started";
13726
+ const behind = gone - progress;
13727
+ if (behind <= .1) return "on-track";
13728
+ if (behind <= .25) return "at-risk";
13729
+ return "off-track";
13730
+ }
13731
+ /** "12 of 40 stores", "8 of 20 tasks done". */
13732
+ function keyResultText(kr, work) {
13733
+ if (kr.kind === "number") {
13734
+ const unit = kr.unit ? ` ${kr.unit}` : "";
13735
+ return `${fmt(kr.current ?? kr.start ?? 0)} of ${fmt(kr.target ?? 0)}${unit}`;
13736
+ }
13737
+ let done = 0;
13738
+ let all = 0;
13739
+ for (const p of linkedProjects(kr, work.projects)) for (const t of tasksOf(work.tasks, p.id)) {
13740
+ all++;
13741
+ if (t.status === "done") done++;
13742
+ }
13743
+ return all === 0 ? "No tasks yet" : `${done} of ${all} tasks done`;
13744
+ }
13745
+ const fmt = (n) => Number.isInteger(n) ? n.toLocaleString("en-US") : String(Math.round(n * 100) / 100);
13746
+
13301
13747
  // node_modules/zod/v4/core/util.js
13302
13748
  function getEnumValues(entries) {
13303
13749
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -18837,7 +19283,8 @@ function tasksSheetCsv(data, rows) {
18837
19283
  "Done",
18838
19284
  "Blocked by",
18839
19285
  "Created",
18840
- "Description"
19286
+ "Description",
19287
+ "Hours logged"
18841
19288
  ], rows.map((t) => {
18842
19289
  const project = projects.get(t.projectId);
18843
19290
  const program = project ? programs.get(project.programId) : void 0;
@@ -18853,7 +19300,8 @@ function tasksSheetCsv(data, rows) {
18853
19300
  t.status === "done" ? "yes" : "no",
18854
19301
  blockerIds(t).map((id) => tasks.get(id)).filter((b) => Boolean(b)).map(keyOf).join("; "),
18855
19302
  t.createdAt?.slice(0, 10) ?? "",
18856
- t.description
19303
+ t.description,
19304
+ t.time?.length ? hours(taskMinutes(t)) : ""
18857
19305
  ];
18858
19306
  }));
18859
19307
  }
@@ -19508,6 +19956,16 @@ var init_fog = __esmMin((() => {
19508
19956
  }));
19509
19957
 
19510
19958
  // src/lib/mcp/tools.ts
19959
+ /**
19960
+ * What an agent can read about a harbor, as MCP tools. Each tool is a pure
19961
+ * function of one harbor's document (health already stamped) and who is
19962
+ * asking, so it is tested against the demo without a server. The route in
19963
+ * src/routes/api/mcp.ts signs the caller in, picks the harbor and loads it.
19964
+ *
19965
+ * Read only: nothing here changes a workspace. Answers carry ids so an agent
19966
+ * can ask about one thing next, and every list is capped and pageable, since
19967
+ * a harbor can hold thousands of tasks.
19968
+ */
19511
19969
  init_thanks();
19512
19970
  init_task_keys();
19513
19971
  init_brief();
@@ -19537,6 +19995,20 @@ init_passage();
19537
19995
  init_board();
19538
19996
  init_holidays();
19539
19997
  init_soundings();
19998
+ /** A task's logged time for get_task: the total and the latest entries. */
19999
+ function timeOut(t, people) {
20000
+ const entries = entriesNewestFirst(t);
20001
+ return {
20002
+ total: formatMinutes(taskMinutes(t)),
20003
+ minutes: taskMinutes(t),
20004
+ latest: entries.slice(0, 10).map((e) => ({
20005
+ on: e.on,
20006
+ by: nameOf$2(people, e.personId),
20007
+ time: formatMinutes(e.minutes),
20008
+ ...e.note ? { note: e.note } : {}
20009
+ }))
20010
+ };
20011
+ }
19540
20012
  /**
19541
20013
  * Where the website opens one task or project, switching to its harbor
19542
20014
  * first (src/routes/open.tsx). Nothing without an origin.
@@ -20236,7 +20708,7 @@ const HARBOR_TOOLS = [
20236
20708
  tool({
20237
20709
  name: "get_task",
20238
20710
  title: "Task details",
20239
- 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,
20711
+ 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,
20240
20712
  input: object({
20241
20713
  harbor: harborArg,
20242
20714
  task: string().min(1).max(100).describe("Task id or key (\"RR-42\"), from search_tasks or my_work.")
@@ -20285,6 +20757,7 @@ const HARBOR_TOOLS = [
20285
20757
  step: i.title,
20286
20758
  done: i.done
20287
20759
  })),
20760
+ ...t.time?.length ? { timeLogged: timeOut(t, people) } : {},
20288
20761
  waitingOn: blockersOf(t, data.tasks).map(brief),
20289
20762
  waitedOnBy: waiters.map(brief),
20290
20763
  ...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
@@ -20899,6 +21372,54 @@ const HARBOR_TOOLS = [
20899
21372
  };
20900
21373
  }
20901
21374
  }),
21375
+ tool({
21376
+ name: "list_goals",
21377
+ title: "Goals",
21378
+ description: "Each portfolio's goals: what it should achieve by when, who owns it, and its key results (a number someone updates, or work in Harbor counted by finished tasks), with how far along each is and whether the goal is on track for its due day (progress held against the time gone by)." + DATA_NOTE,
21379
+ input: object({
21380
+ harbor: harborArg,
21381
+ portfolio: string().max(200).optional().describe("Portfolio id or name. Default: all.")
21382
+ }),
21383
+ run: (ctx, input) => {
21384
+ const { data } = ctx;
21385
+ const today = todayISO(ctx.now);
21386
+ const people = byId(data.people);
21387
+ const named = byId([...data.programs, ...data.projects]);
21388
+ return { portfolios: (input.portfolio ? [findOne(data.portfolios, input.portfolio, "portfolio")] : data.portfolios).map((pf) => ({
21389
+ id: pf.id,
21390
+ name: pf.name,
21391
+ goals: (pf.goals ?? []).map((g) => {
21392
+ const progress = goalProgress(g, data);
21393
+ const status = goalStatus(progress, g, today);
21394
+ return {
21395
+ id: g.id,
21396
+ title: g.title,
21397
+ owner: g.ownerId ? people.get(g.ownerId)?.name ?? null : null,
21398
+ due: g.due,
21399
+ ...g.why ? { why: g.why } : {},
21400
+ progress: Math.round(progress * 100),
21401
+ status,
21402
+ statusText: GOAL_STATUS_LABEL[status],
21403
+ keyResults: g.keyResults.map((kr) => ({
21404
+ id: kr.id,
21405
+ title: kr.title,
21406
+ kind: kr.kind,
21407
+ text: keyResultText(kr, data),
21408
+ progress: Math.round(keyResultProgress(kr, data) * 100),
21409
+ ...kr.kind === "number" ? {
21410
+ start: kr.start ?? 0,
21411
+ target: kr.target,
21412
+ current: kr.current ?? kr.start ?? 0
21413
+ } : {
21414
+ counts: (kr.linkIds ?? []).map((id) => named.get(id)?.name ?? id),
21415
+ projects: linkedProjects(kr, data.projects).length
21416
+ }
21417
+ }))
21418
+ };
21419
+ })
21420
+ })) };
21421
+ }
21422
+ }),
20902
21423
  tool({
20903
21424
  name: "long_pole",
20904
21425
  title: "What's holding this up",
@@ -22876,6 +23397,37 @@ function tasksSlice({ set, get, write }) {
22876
23397
  checklist: (t.checklist ?? []).filter((i) => i.id !== itemId)
22877
23398
  } : t) });
22878
23399
  },
23400
+ logTime: (taskId, entry) => {
23401
+ const minutes = Math.round(entry.minutes);
23402
+ if (!Number.isFinite(minutes) || minutes < 1 || minutes > 1440) return null;
23403
+ const task = get().tasks.find((t) => t.id === taskId);
23404
+ if (!task || (task.time?.length ?? 0) >= 500) return null;
23405
+ const personId = entry.personId ?? get().currentUserId;
23406
+ if (!personId) return null;
23407
+ const id = uid("tm");
23408
+ const note = entry.note?.trim().slice(0, 200);
23409
+ const row = {
23410
+ id,
23411
+ personId,
23412
+ minutes,
23413
+ on: entry.on ?? todayISO(),
23414
+ ...note ? { note } : {},
23415
+ at: stamp()
23416
+ };
23417
+ set({ tasks: get().tasks.map((t) => t.id === taskId ? {
23418
+ ...t,
23419
+ touchedAt: stamp(),
23420
+ time: [...t.time ?? [], row]
23421
+ } : t) });
23422
+ return id;
23423
+ },
23424
+ removeTime: (taskId, entryId) => {
23425
+ set({ tasks: get().tasks.map((t) => t.id === taskId && t.time?.some((e) => e.id === entryId) ? {
23426
+ ...t,
23427
+ touchedAt: stamp(),
23428
+ time: t.time.filter((e) => e.id !== entryId)
23429
+ } : t) });
23430
+ },
22879
23431
  addAttachment: (a) => {
22880
23432
  const id = a.id ?? uid("f");
22881
23433
  const row = {
@@ -23099,6 +23651,7 @@ var init_tasks = __esmMin((() => {
23099
23651
  init_automations();
23100
23652
  init_focus();
23101
23653
  init_format();
23654
+ init_time_limits();
23102
23655
  init_soundings();
23103
23656
  init_clock();
23104
23657
  init_utils();