@4fu/arbor-tools 0.1.0 → 2026.1106.1

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.
@@ -12,6 +12,10 @@ import { isAbsolute, join, normalize, resolve } from "node:path";
12
12
  import { formatTime, textOf } from "@4fu/arbor-prompt";
13
13
  import { moduleSpec, refused } from "../host.js";
14
14
  import { envCommand } from "../env-stage.js";
15
+ /** How many returned items carry a 相关 line of their own. */
16
+ const RELATED_ITEMS = 3;
17
+ /** How many neighbours one 相关 line names before it says there are more. */
18
+ const RELATED_LIMIT = 3;
15
19
  /** `journal`: search, show, around, trace, links. */
16
20
  export class JournalModuleGroup {
17
21
  id = "modules.journal";
@@ -41,12 +45,12 @@ export class JournalModuleGroup {
41
45
  const page = await journal.search(stringArg(args, "query"), {
42
46
  ...(numberArg(args, "limit") === undefined ? {} : { limit: numberArg(args, "limit") }),
43
47
  });
44
- return this.answer(request, page.items.map((hit) => `${hit.ref} ${hit.kind} ${hit.summary}`), page);
48
+ return this.answer(request, page.items.map((hit) => ({ ref: hit.ref, line: `${hit.ref} ${hit.kind} ${hit.summary}` })), page);
45
49
  }
46
50
  case "journal.show": {
47
51
  const refs = eventRefsArg(args, "ref");
48
52
  const events = await journal.getMany(refs);
49
- return this.answer(request, events.map((event) => `${event.ref} ${event.kind} ${bodyOf(event)}`), {
53
+ return this.answer(request, events.map((event) => ({ ref: event.ref, line: `${event.ref} ${event.kind} ${bodyOf(event)}` })), {
50
54
  items: events,
51
55
  total: events.length,
52
56
  limit: events.length,
@@ -58,7 +62,7 @@ export class JournalModuleGroup {
58
62
  ...(numberArg(args, "before") === undefined ? {} : { before: numberArg(args, "before") }),
59
63
  ...(numberArg(args, "after") === undefined ? {} : { after: numberArg(args, "after") }),
60
64
  });
61
- return this.answer(request, events.map((event) => `${event.ref} ${event.kind} ${bodyOf(event)}`), {
65
+ return this.answer(request, events.map((event) => ({ ref: event.ref, line: `${event.ref} ${event.kind} ${bodyOf(event)}` })), {
62
66
  items: events,
63
67
  total: events.length,
64
68
  limit: events.length,
@@ -67,7 +71,7 @@ export class JournalModuleGroup {
67
71
  }
68
72
  case "journal.trace": {
69
73
  const node = await journal.trace(refArg(args, "ref"));
70
- return this.answer(request, [traceText(node, 0)], {
74
+ return this.answer(request, [{ ref: node.ref, line: traceText(node, 0) }], {
71
75
  items: [node.ref],
72
76
  total: 1,
73
77
  limit: 1,
@@ -78,21 +82,52 @@ export class JournalModuleGroup {
78
82
  const page = await journal.links(refArg(args, "ref"), {
79
83
  ...(numberArg(args, "offset") === undefined ? {} : { offset: numberArg(args, "offset") }),
80
84
  });
81
- return this.answer(request, page.items.map((item) => `${item.ref} ${item.kind} ${item.summary}`), page);
85
+ // The items are the neighbours already, so this one answer adds no
86
+ // related line of its own.
87
+ return this.answer(request, page.items.map((item) => ({ ref: item.ref, line: `${item.ref} ${item.kind} ${item.summary}` })), page, false);
82
88
  }
83
89
  default:
84
90
  return this.notOwned(request);
85
91
  }
86
92
  }
87
- answer(request, lines, page) {
93
+ /**
94
+ * One answer: the items, then one 相关 line per returned item for the first
95
+ * three of them, then the total and the call that pages the rest
96
+ * (docs/prompting.md#information-density).
97
+ */
98
+ async answer(request, items, page, related = true) {
88
99
  const table = this.services.table;
100
+ const lines = items.map((item) => item.line);
89
101
  const body = lines.length === 0 ? textOf(table, "result.search.empty") : lines.join("\n");
90
102
  return {
91
103
  callId: request.callId,
92
104
  ok: true,
93
- content: `${body}\n${textOf(table, "result.search.total", { total: page.total })}\n${page.nextCall}`,
105
+ content: [
106
+ body,
107
+ ...(related ? await this.related(items) : []),
108
+ textOf(table, "result.search.total", { total: page.total }),
109
+ page.nextCall,
110
+ ].join("\n"),
94
111
  };
95
112
  }
113
+ /** One 相关 line per item, for the first three: no line without neighbours. */
114
+ async related(items) {
115
+ const table = this.services.table;
116
+ const lines = [];
117
+ for (const item of items.slice(0, RELATED_ITEMS)) {
118
+ const page = await this.services.journal.links(item.ref, { limit: RELATED_LIMIT });
119
+ if (page.items.length === 0)
120
+ continue;
121
+ const more = page.total > page.items.length
122
+ ? textOf(table, "result.journal.related.more", { total: page.total, offset: page.items.length })
123
+ : "";
124
+ lines.push(textOf(table, "result.journal.related", {
125
+ refs: page.items.map((one) => one.ref).join("、"),
126
+ more,
127
+ }));
128
+ }
129
+ return lines;
130
+ }
96
131
  notOwned(request) {
97
132
  return {
98
133
  callId: request.callId,
@@ -696,14 +731,24 @@ export class ScheduleModuleGroup {
696
731
  content: textOf(table, "result.schedule.updated", { ref }),
697
732
  };
698
733
  }
699
- /** One repeat rule as one line: only once, a period, or a daily clock time. */
734
+ /** One repeat rule as one line: only once, a period, a daily or a weekly time. */
700
735
  repeatText(repeat) {
701
736
  const table = this.services.table;
702
737
  if (repeat === undefined)
703
738
  return textOf(table, "result.schedule.once");
704
- return "everyMs" in repeat
739
+ const base = "everyMs" in repeat
705
740
  ? textOf(table, "result.schedule.every_ms", { ms: repeat.everyMs })
706
- : textOf(table, "result.schedule.daily", { time: repeat.dailyAt });
741
+ : "weekdays" in repeat
742
+ ? textOf(table, "result.schedule.weekly", {
743
+ weekdays: [...repeat.weekdays].sort().join("、"),
744
+ time: repeat.at,
745
+ })
746
+ : textOf(table, "result.schedule.daily", { time: repeat.dailyAt });
747
+ const bounds = [
748
+ ...(repeat.times === undefined ? [] : [textOf(table, "result.schedule.times", { count: repeat.times })]),
749
+ ...(repeat.until === undefined ? [] : [textOf(table, "result.schedule.until", { time: formatTime(repeat.until) })]),
750
+ ];
751
+ return `${base}${bounds.join("")}`;
707
752
  }
708
753
  /** One argument the script did not name in a shape this module can read. */
709
754
  badInput(field) {
@@ -974,7 +1019,9 @@ function taskRefArg(args, name) {
974
1019
  }
975
1020
  /**
976
1021
  * One repeat rule, `invalid` when the argument is not one this module can read:
977
- * a period in milliseconds, or a clock time a firing repeats at daily.
1022
+ * a period in milliseconds, a clock time a firing repeats at daily, or days of
1023
+ * the week with the clock time it fires at on them. `times` and `until` bound
1024
+ * any of the three.
978
1025
  */
979
1026
  function repeatArg(args) {
980
1027
  const value = args.repeat;
@@ -983,11 +1030,20 @@ function repeatArg(args) {
983
1030
  if (typeof value !== "object" || value === null)
984
1031
  return "invalid";
985
1032
  const repeat = value;
1033
+ const times = typeof repeat.times === "number" && Number.isFinite(repeat.times) && repeat.times >= 0 ? Math.floor(repeat.times) : undefined;
1034
+ const until = typeof repeat.until === "number" && Number.isFinite(repeat.until) ? Math.floor(repeat.until) : undefined;
1035
+ const bounds = { ...(times === undefined ? {} : { times }), ...(until === undefined ? {} : { until }) };
986
1036
  if (typeof repeat.everyMs === "number" && Number.isFinite(repeat.everyMs) && repeat.everyMs > 0) {
987
- return { everyMs: repeat.everyMs };
1037
+ return { everyMs: repeat.everyMs, ...bounds };
988
1038
  }
989
1039
  if (typeof repeat.dailyAt === "string" && /^\d{1,2}:\d{2}$/u.test(repeat.dailyAt.trim())) {
990
- return { dailyAt: repeat.dailyAt.trim() };
1040
+ return { dailyAt: repeat.dailyAt.trim(), ...bounds };
1041
+ }
1042
+ if (Array.isArray(repeat.weekdays) && typeof repeat.at === "string" && /^\d{1,2}:\d{2}$/u.test(repeat.at.trim())) {
1043
+ const weekdays = repeat.weekdays.filter((day) => typeof day === "number" && Number.isInteger(day) && day >= 0 && day <= 6);
1044
+ if (weekdays.length === 0)
1045
+ return "invalid";
1046
+ return { weekdays, at: repeat.at.trim(), ...bounds };
991
1047
  }
992
1048
  return "invalid";
993
1049
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4fu/arbor-tools",
3
- "version": "0.1.0",
3
+ "version": "2026.1106.1",
4
4
  "type": "module",
5
5
  "description": "The entry points: direct tools, code mode modules and the one tool catalog that routes both through the service layer.",
6
6
  "exports": {
@@ -16,6 +16,8 @@
16
16
  "bound.js",
17
17
  "catalog.d.ts",
18
18
  "catalog.js",
19
+ "child-env.d.ts",
20
+ "child-env.js",
19
21
  "code-exec.d.ts",
20
22
  "code-exec.js",
21
23
  "env-stage.d.ts",
@@ -24,6 +26,8 @@
24
26
  "groups/core.js",
25
27
  "groups/env.d.ts",
26
28
  "groups/env.js",
29
+ "groups/external.d.ts",
30
+ "groups/external.js",
27
31
  "groups/files.d.ts",
28
32
  "groups/files.js",
29
33
  "groups/notes.d.ts",
@@ -50,8 +54,8 @@
50
54
  "access": "public"
51
55
  },
52
56
  "dependencies": {
53
- "@4fu/arbor-errors": "0.1.0",
54
- "@4fu/arbor-interfaces": "0.1.0",
55
- "@4fu/arbor-prompt": "0.1.0"
57
+ "@4fu/arbor-errors": "2026.1106.1",
58
+ "@4fu/arbor-interfaces": "2026.1106.1",
59
+ "@4fu/arbor-prompt": "2026.1106.1"
56
60
  }
57
61
  }