dsh-mishu 0.7.2 → 0.7.4

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.
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The bridge to DSH's own Schedule service (`@deepseek-ai/dsh-schedule`, the
3
+ * "自动化任务" page).
4
+ *
5
+ * Two directions, both kept small on purpose:
6
+ *
7
+ * - The daily brief can be handed to Schedule: the same text the desktop
8
+ * notification carries, delivered as a turn in a conversation instead. That
9
+ * is the one task the secretary creates on its own, and it is marked in its
10
+ * prompt so a later visit recognises it even after you edited the title.
11
+ * - Every task on the Host is listed for the settings page, so what the
12
+ * secretary scheduled and what you scheduled yourself sit in one place.
13
+ *
14
+ * Schedule is optional: a Host without the service simply has no bridge, and
15
+ * the page says so instead of offering a button that cannot work.
16
+ */
17
+ import { bi, BRIEF_MARK, briefPrompt, briefTiming, briefTitle, BRIEF_TASK_KEY, isBriefTask, sortTasks, taskView } from "../core/index.js";
18
+ import { MishuError } from "../core/i18n.js";
19
+ /** A code the service returns instead of throwing (`schedule_not_found`, `schedule_conflict`, …). */
20
+ const isMiss = (value) => typeof value === 'object' && value !== null && 'code' in value && typeof value.code === 'string';
21
+ /** The service's own words, wrapped so the page shows one sentence in one language. */
22
+ function wrap(error, _lang) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ const code = error.code;
25
+ const hint = typeof code === 'string' ? `(${code})` : '';
26
+ return new MishuError(bi(`DSH 的自动化任务没接受这个请求:${message}${hint}`, `DSH's scheduled tasks refused this: ${message}${hint}`));
27
+ }
28
+ const BRIEF_ERROR_KEY = 'schedule.briefError';
29
+ export class ScheduleBridge {
30
+ service;
31
+ store;
32
+ lang;
33
+ /** The brief's time and zone, as the reminder settings hold them. */
34
+ briefClock;
35
+ constructor(service, store, lang, briefClock) {
36
+ this.service = service;
37
+ this.store = store;
38
+ this.lang = lang;
39
+ this.briefClock = briefClock;
40
+ }
41
+ /** A restarted service: same bridge, new handle. */
42
+ replace(service) {
43
+ this.service = service;
44
+ }
45
+ /** The page's language when it asked, else the notebook's. A task's wording is read by a person, not logged. */
46
+ useLang(lang) {
47
+ return lang ?? this.lang();
48
+ }
49
+ savedId() {
50
+ return this.store()?.get(BRIEF_TASK_KEY);
51
+ }
52
+ /** Every task on the Host, newest rules first, plus whether the brief is armed. */
53
+ async view(asked) {
54
+ const lang = this.useLang(asked);
55
+ const briefId = this.savedId();
56
+ let entries = [];
57
+ let error = null;
58
+ try {
59
+ const page = await this.service.list({ limit: 100 });
60
+ entries = page.items;
61
+ }
62
+ catch (problem) {
63
+ error = wrap(problem, lang).text[lang];
64
+ }
65
+ const store = this.store();
66
+ const clock = this.briefClock();
67
+ const timing = briefTiming(clock.time, clock.timeZone);
68
+ const armed = entries.some(entry => isBriefTask(entry, briefId) && (entry.status ?? 'active') === 'active');
69
+ return {
70
+ available: true,
71
+ tasks: sortTasks(entries.map(entry => taskView(entry, lang, briefId))),
72
+ brief: {
73
+ id: briefId ?? null,
74
+ time: clock.time,
75
+ timeZone: timing.time_zone,
76
+ armed,
77
+ error: error ?? store?.get(BRIEF_ERROR_KEY) ?? null,
78
+ },
79
+ };
80
+ }
81
+ /**
82
+ * Arm the brief as a daily task, or move it when the time changed. Reuses
83
+ * the task it made before — editing it in the automation page keeps it.
84
+ */
85
+ async armBrief(asked) {
86
+ const store = this.store();
87
+ if (store === undefined)
88
+ throw new MishuError(bi('小秘书的记事本没有打开', 'the notebook is not open'));
89
+ const lang = this.useLang(asked);
90
+ const clock = this.briefClock();
91
+ const timing = { kind: 'daily', daily: briefTiming(clock.time, clock.timeZone) };
92
+ const request = { title: briefTitle(lang), prompt: briefPrompt(lang), timing };
93
+ const existing = this.savedId();
94
+ try {
95
+ let id = existing;
96
+ if (id !== undefined) {
97
+ const current = await this.service.view({ id });
98
+ if (isMiss(current))
99
+ id = undefined;
100
+ else {
101
+ const updated = await this.service.edit({
102
+ id,
103
+ action: 'update',
104
+ patch: { ...request, active: true },
105
+ ...current.updatedAt === undefined ? {} : { expectedUpdatedAt: current.updatedAt },
106
+ });
107
+ if (isMiss(updated))
108
+ id = undefined;
109
+ }
110
+ }
111
+ if (id === undefined) {
112
+ const created = await this.service.create({ ...request, target: { kind: 'new_once', create: {} } });
113
+ if (isMiss(created))
114
+ throw new MishuError(bi(`自动化任务没有建成:${created.code}`, `the task was not created: ${created.code}`));
115
+ id = created.id;
116
+ store.set(BRIEF_TASK_KEY, id);
117
+ }
118
+ store.set(BRIEF_ERROR_KEY, null);
119
+ store.log(Date.now(), 'schedule-brief', { id, time: clock.time, zone: briefTiming(clock.time, clock.timeZone).time_zone });
120
+ }
121
+ catch (problem) {
122
+ const wrapped = problem instanceof MishuError ? problem : wrap(problem, lang);
123
+ store.set(BRIEF_ERROR_KEY, wrapped.text[lang]);
124
+ throw wrapped;
125
+ }
126
+ return this.view(lang);
127
+ }
128
+ /**
129
+ * Move the brief when its time changed on the settings page, and say
130
+ * nothing when it was never armed. Errors are kept, not thrown: this runs
131
+ * behind a save that has already succeeded.
132
+ */
133
+ async followBrief(asked) {
134
+ const store = this.store();
135
+ const id = this.savedId();
136
+ if (store === undefined || id === undefined)
137
+ return;
138
+ try {
139
+ const current = await this.service.view({ id });
140
+ if (isMiss(current)) {
141
+ store.set(BRIEF_TASK_KEY, null);
142
+ return;
143
+ }
144
+ if ((current.status ?? 'active') !== 'active')
145
+ return;
146
+ const clock = this.briefClock();
147
+ const timing = briefTiming(clock.time, clock.timeZone);
148
+ if (current.kind === 'daily' && current.time === timing.time && current.timeZone === timing.time_zone)
149
+ return;
150
+ const updated = await this.service.edit({
151
+ id,
152
+ action: 'update',
153
+ patch: { timing: { kind: 'daily', daily: timing } },
154
+ ...current.updatedAt === undefined ? {} : { expectedUpdatedAt: current.updatedAt },
155
+ });
156
+ const lang = this.useLang(asked);
157
+ if (isMiss(updated))
158
+ store.set(BRIEF_ERROR_KEY, bi(`改不动:${updated.code}`, `could not move it: ${updated.code}`)[lang]);
159
+ else
160
+ store.log(Date.now(), 'schedule-brief-moved', { id, time: clock.time });
161
+ }
162
+ catch (problem) {
163
+ const lang = this.useLang(asked);
164
+ store.set(BRIEF_ERROR_KEY, wrap(problem, lang).text[lang]);
165
+ }
166
+ }
167
+ /** Take the brief task off the Host; the desktop notification is unaffected. */
168
+ async disarmBrief(asked) {
169
+ const store = this.store();
170
+ if (store === undefined)
171
+ throw new MishuError(bi('小秘书的记事本没有打开', 'the notebook is not open'));
172
+ const lang = this.useLang(asked);
173
+ const id = this.savedId();
174
+ if (id !== undefined) {
175
+ try {
176
+ const result = await this.service.edit({ id, action: 'delete' });
177
+ // Already gone is the same as gone.
178
+ if (isMiss(result) && result.code !== 'schedule_not_found')
179
+ throw new MishuError(bi(`删不掉:${result.code}`, `could not delete: ${result.code}`));
180
+ }
181
+ catch (problem) {
182
+ throw problem instanceof MishuError ? problem : wrap(problem, lang);
183
+ }
184
+ store.set(BRIEF_TASK_KEY, null);
185
+ store.log(Date.now(), 'schedule-brief-off', { id });
186
+ }
187
+ store.set(BRIEF_ERROR_KEY, null);
188
+ return this.view(lang);
189
+ }
190
+ /** Delete any task on the Host — the secretary only ever deletes its own, but the page lists all. */
191
+ async delete(id, asked) {
192
+ const lang = this.useLang(asked);
193
+ try {
194
+ const result = await this.service.edit({ id, action: 'delete' });
195
+ if (isMiss(result) && result.code === 'schedule_not_found')
196
+ throw new MishuError(bi('这个任务已经不在了', 'that task is already gone'));
197
+ }
198
+ catch (problem) {
199
+ throw problem instanceof MishuError ? problem : wrap(problem, lang);
200
+ }
201
+ if (this.savedId() === id)
202
+ this.store()?.set(BRIEF_TASK_KEY, null);
203
+ return this.view(lang);
204
+ }
205
+ /** Run a task now, without waiting for its time. */
206
+ async run(id, asked) {
207
+ const lang = this.useLang(asked);
208
+ try {
209
+ const result = await this.service.run({ id });
210
+ if (isMiss(result)) {
211
+ throw new MishuError(result.code === 'schedule_execution_pending'
212
+ ? bi('这个任务正在跑,等它跑完', 'that task is already running')
213
+ : bi('这个任务已经不在了', 'that task is already gone'));
214
+ }
215
+ }
216
+ catch (problem) {
217
+ throw problem instanceof MishuError ? problem : wrap(problem, lang);
218
+ }
219
+ return this.view(lang);
220
+ }
221
+ }
@@ -29,6 +29,11 @@ export type AskRequest = {
29
29
  kind: 'mail-setup';
30
30
  preset: string;
31
31
  error?: string;
32
+ } | {
33
+ kind: 'watch-error';
34
+ watchId: number;
35
+ } | {
36
+ kind: 'watch-setup';
32
37
  };
33
38
  export interface AskOptions {
34
39
  now: number;
@@ -17,7 +17,7 @@ export interface BoardCard {
17
17
  /** Matter id, or series id for a series card. */
18
18
  id: number;
19
19
  target: 'matter' | 'series';
20
- source: 'github' | 'mail' | 'manual';
20
+ source: 'github' | 'mail' | 'manual' | 'watch';
21
21
  title: string;
22
22
  /** One short line: when it is due, or why it is here. */
23
23
  meta: string;
@@ -58,6 +58,12 @@ export interface BoardData {
58
58
  state: 'never' | 'ok' | 'error';
59
59
  text: string;
60
60
  }>;
61
+ watch: Array<{
62
+ id: number;
63
+ label: string;
64
+ state: 'never' | 'ok' | 'error';
65
+ text: string;
66
+ }>;
61
67
  };
62
68
  markRead: MarkReadMode;
63
69
  columns: {
@@ -83,6 +89,11 @@ export interface BoardOptions {
83
89
  id: number;
84
90
  label: string;
85
91
  }>;
92
+ /** Things you watch yourself, for their status line. */
93
+ watches?: Array<{
94
+ id: number;
95
+ label: string;
96
+ }>;
86
97
  }
87
98
  export declare function buildBoard(store: MishuStore, options: BoardOptions): BoardData;
88
99
  export interface MatterDetail {
@@ -27,6 +27,11 @@ export interface BriefOptions {
27
27
  id: number;
28
28
  label: string;
29
29
  }>;
30
+ /** Things you watch yourself, for their status lines. */
31
+ watches?: Array<{
32
+ id: number;
33
+ label: string;
34
+ }>;
30
35
  }
31
36
  export declare function renderBrief(store: MishuStore, options: BriefOptions): string;
32
37
  /** Recently closed matters with reasons, so a wrong closure can be reopened. */
@@ -28,6 +28,10 @@ export { DEFAULT_NOTIFY, deliver, inQuiet, localClock, notifySettings, notifyTic
28
28
  export { restartPlan, scheduleRestart, type RestartPlan, type RestartStarted } from './restart.ts';
29
29
  export { FileSecrets, KeychainOrFile, MemorySecrets, secretEnvName, secretFromEnv, systemSecrets, type SecretStatus, type SecretStore } from './secrets.ts';
30
30
  export { deleteMailAccount, effectiveSettings, mailAccounts, saveMailAccount, saveSettings, type Defaults, type UserSettings } from './settings.ts';
31
+ export { deleteWatch, dueWatches, entryKey, parseKeywords, resolveWatch, saveWatch, watchById, watchReportKey, watchSources, WATCH_SOURCE, type WatchInput, type WatchReport, type WatchSource, } from './watch/source.ts';
32
+ export { describeWatchError, httpFetch, parseFeed, parseJson, parsePage, plainText, readWatchBody, type WatchFetch, type WatchItem } from './watch/fetch.ts';
33
+ export { describeKeywords, describeWatch, diffItems, judgeWatch, syncWatch } from './watch/sync.ts';
34
+ export { BRIEF_MARK, BRIEF_TASK_KEY, briefPrompt, briefTiming, briefTitle, clockText, describeTiming, instantText, isBriefTask, sortTasks, taskView, type BriefTiming, type ScheduleEntry, type ScheduleKind, type ScheduleTaskView, } from './schedule/tasks.ts';
31
35
  export declare const DEFAULT_HOME = "~/.mishu";
32
36
  /** `~` and `~/…` expand to the home directory on every platform. */
33
37
  export declare function expandHome(path: string): string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * DSH's own scheduled tasks, seen from the secretary's side.
3
+ *
4
+ * DSH keeps a Schedule service (`@deepseek-ai/dsh-schedule`) that delivers a
5
+ * prompt to a Session at a wall-clock time: the "自动化任务" page. This module
6
+ * only describes what that service returns — it never imports DSH, so the
7
+ * wording, the time formatting and the check for "is this the brief task"
8
+ * stay testable without a host.
9
+ *
10
+ * The one task the secretary creates itself is the daily brief: same content
11
+ * as the desktop notification, delivered into a conversation instead, so it
12
+ * reaches you when DSH is open but you are not looking at the board.
13
+ */
14
+ import { type Lang } from '../i18n.ts';
15
+ /** Where the notebook remembers the brief task it created, so it can be updated instead of duplicated. */
16
+ export declare const BRIEF_TASK_KEY = "schedule.briefTask";
17
+ /** Marks the prompt the secretary writes, so a task edited by hand is still recognisable. */
18
+ export declare const BRIEF_MARK = "[dsh-mishu brief]";
19
+ export type ScheduleKind = 'after' | 'at' | 'every' | 'daily' | 'weekly' | 'cron' | 'rrule';
20
+ /**
21
+ * One task as the Schedule service hands it out: its rule fields are spread
22
+ * onto the entry, so which ones are present says what kind it is.
23
+ */
24
+ export interface ScheduleEntry {
25
+ id: string;
26
+ kind: ScheduleKind;
27
+ title: string;
28
+ prompt: string;
29
+ /** Next occurrence, RFC 3339 in UTC. */
30
+ scheduledAt?: string;
31
+ afterSeconds?: number;
32
+ everySeconds?: number;
33
+ /** Local wall-clock time, `HH:mm:ss.SSS`. */
34
+ time?: string;
35
+ timeZone?: string;
36
+ /** ISO weekdays, 1 = Monday. */
37
+ weekdays?: number[];
38
+ expression?: string;
39
+ rrule?: string;
40
+ status?: 'active' | 'paused' | 'ended' | 'inactive';
41
+ createdAt?: string;
42
+ updatedAt?: string;
43
+ executionActive?: boolean;
44
+ lastDelivery?: {
45
+ at?: string;
46
+ };
47
+ }
48
+ /** One task, worded for the settings page. */
49
+ export interface ScheduleTaskView {
50
+ id: string;
51
+ title: string;
52
+ /** The rule, in words: "每天 08:30" / "every 15 minutes". */
53
+ when: string;
54
+ /** Next occurrence, `11-12 08:30` in the task's own zone; null once it has ended. */
55
+ next: string | null;
56
+ status: 'active' | 'paused' | 'ended';
57
+ /** What the task tells the session, shortened for a tooltip. */
58
+ prompt: string;
59
+ /** Created by the secretary itself (the daily brief). */
60
+ mine: boolean;
61
+ running: boolean;
62
+ lastDelivery: string | null;
63
+ }
64
+ /** `HH:mm:ss.SSS` → `HH:mm`, or the raw value when it is not a clock time. */
65
+ export declare function clockText(time: string | undefined): string;
66
+ /** An RFC 3339 instant as a local day and clock time, `11-12 08:30`. */
67
+ export declare function instantText(iso: string | undefined, timeZone: string, lang: Lang): string;
68
+ /** The rule in words. Every branch names the unit a person set, never a cron string alone. */
69
+ export declare function describeTiming(entry: ScheduleEntry, lang: Lang): string;
70
+ /** One entry, as the settings page shows it. */
71
+ export declare function taskView(entry: ScheduleEntry, lang: Lang, briefTaskId: string | undefined): ScheduleTaskView;
72
+ /** Sorted for reading: what still runs first, then what has ended. */
73
+ export declare function sortTasks(tasks: ScheduleTaskView[]): ScheduleTaskView[];
74
+ /** The prompt the brief task carries: what the agent should do at that hour. */
75
+ export declare function briefPrompt(lang: Lang): string;
76
+ /** Title of the brief task, in the language it was created in. */
77
+ export declare function briefTitle(lang: Lang): string;
78
+ export interface BriefTiming {
79
+ time: string;
80
+ time_zone: string;
81
+ }
82
+ /**
83
+ * The daily rule for the brief, from a `HH:mm` setting and a zone. Seconds are
84
+ * zero so the task fires on the minute; the zone falls back to this computer's
85
+ * when the settings page has not picked one yet.
86
+ */
87
+ export declare function briefTiming(time: string, timeZone: string): BriefTiming;
88
+ /** Whether a task already is the brief for this notebook. */
89
+ export declare function isBriefTask(entry: ScheduleEntry, briefTaskId: string | undefined): boolean;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Reading a watched address: a feed, a JSON list, or just a page.
3
+ *
4
+ * Whatever comes back is turned into a handful of items — a title, a bit of
5
+ * text and a stable key — so the rest of the code never has to care whether
6
+ * it was RSS, Atom, JSON or plain HTML. No parser library: the shapes that
7
+ * matter here are small, and a wrong guess costs nothing (the page is read
8
+ * as text and still works).
9
+ */
10
+ import { type Bi } from '../i18n.ts';
11
+ import { type WatchSource } from './source.ts';
12
+ export interface WatchItem {
13
+ /** Stable across checks: the same entry twice is the same item. */
14
+ key: string;
15
+ title: string;
16
+ /** The text to look for keywords in, and to quote back. */
17
+ text: string;
18
+ url: string | null;
19
+ /** When the source says it happened; the fetch time when it does not. */
20
+ at: number;
21
+ }
22
+ export interface WatchFetch {
23
+ (url: string): Promise<{
24
+ status: number;
25
+ body: string;
26
+ contentType: string;
27
+ }>;
28
+ }
29
+ /** The real fetch: a plain GET with a short timeout and no cookies. */
30
+ export declare const httpFetch: WatchFetch;
31
+ /** Markup out, whitespace collapsed. */
32
+ export declare function plainText(markup: string): string;
33
+ /** RSS `<item>` and Atom `<entry>` blocks, whichever the document has. */
34
+ export declare function parseFeed(body: string, now: number, base: string): WatchItem[] | undefined;
35
+ /**
36
+ * A JSON body: an array of objects, or an object with one array inside. Each
37
+ * element's title-like and text-like fields become an item.
38
+ */
39
+ export declare function parseJson(body: string, now: number, base: string): WatchItem[] | undefined;
40
+ /** A page with no feed: its title and text, as one item keyed by a hash of the text. */
41
+ export declare function parsePage(body: string, now: number, base: string): WatchItem[];
42
+ /** Whatever the address returned, as items. */
43
+ export declare function readWatchBody(options: {
44
+ body: string;
45
+ contentType: string;
46
+ url: string;
47
+ now: number;
48
+ }): WatchItem[];
49
+ /** The sentence shown when a watch cannot be read. */
50
+ export declare function describeWatchError(error: unknown, source: WatchSource): Bi;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Things you watch yourself: a page, a feed or an API you care about.
3
+ *
4
+ * GitHub and mail are sources the secretary knows how to read. Everything
5
+ * else a person cares about — a game's patch notes, a job posting, a price,
6
+ * a status page — is a URL. You add it here with the words that matter to
7
+ * you, and the secretary looks at it now and then: when it changes, or when
8
+ * one of your words shows up, it becomes a card like anything else.
9
+ *
10
+ * Only where and what to look for is kept here. What was seen last lives in
11
+ * the notebook's key-value table, so an entry never grows without bound.
12
+ */
13
+ import { type Bi } from '../i18n.ts';
14
+ import type { MishuStore } from '../store.ts';
15
+ export declare const WATCH_SOURCE = "watch";
16
+ export interface WatchSource {
17
+ id: number;
18
+ /** What you call it: "补丁公告", "机票价格". */
19
+ label: string;
20
+ url: string;
21
+ /**
22
+ * Words to watch for, lower-cased. Empty: any change is worth a card.
23
+ * A feed is read per entry; a page is read as one text.
24
+ */
25
+ keywords: string[];
26
+ enabled: boolean;
27
+ /** Minutes between checks. */
28
+ intervalMinutes: number;
29
+ }
30
+ export interface WatchInput {
31
+ id?: number;
32
+ label?: string;
33
+ url?: string;
34
+ keywords?: string | string[];
35
+ enabled?: boolean;
36
+ intervalMinutes?: number;
37
+ }
38
+ export interface WatchReport {
39
+ at: number;
40
+ id: number;
41
+ /** Entries or text pieces the fetch turned up. */
42
+ items: number;
43
+ opened: number;
44
+ closed: number;
45
+ error?: Bi;
46
+ }
47
+ export declare function watchSources(store: MishuStore): WatchSource[];
48
+ export declare function watchById(store: MishuStore, id: number): WatchSource | undefined;
49
+ export declare function watchReportKey(id: number): string;
50
+ /** Where one watch keeps what it has already seen. */
51
+ export declare function watchStateKey(id: number): string;
52
+ export declare function watchSeenKey(id: number): string;
53
+ /** A stable id for a feed entry: its guid, else its link, else its title. */
54
+ export declare function entryKey(entry: {
55
+ id?: string;
56
+ link?: string;
57
+ title?: string;
58
+ }): string;
59
+ /** Split what you typed into words: commas, 、, spaces or new lines all work. */
60
+ export declare function parseKeywords(value: string | string[] | undefined): string[];
61
+ /** Check and complete what the settings page sent; throws a sentence a person can act on. */
62
+ export declare function resolveWatch(input: WatchInput, id: number): WatchSource;
63
+ /** Add (no id) or update one watch. */
64
+ export declare function saveWatch(store: MishuStore, input: WatchInput, now: number): WatchSource;
65
+ /** Remove one watch and close whatever it still has open. */
66
+ export declare function deleteWatch(store: MishuStore, id: number, now: number): boolean;
67
+ /** The watches whose next check is due. */
68
+ export declare function dueWatches(store: MishuStore, now: number): WatchSource[];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One watch, one check: read the address, compare with what was seen last
3
+ * time, and turn anything new into a card.
4
+ *
5
+ * A card per watch, not per entry: "补丁公告 · 3 条新内容" is what you want to
6
+ * see, not three cards that say the same thing. Marking it done closes it;
7
+ * the next change opens it again with what changed.
8
+ *
9
+ * The first check only writes down what is there now — otherwise adding a
10
+ * feed would hand you every entry it ever published. Words you asked to watch
11
+ * for are the exception: those are shown right away, because you asked.
12
+ */
13
+ import type { MishuStore } from '../store.ts';
14
+ import type { Verdict } from '../github/rules.ts';
15
+ import { type WatchFetch, type WatchItem } from './fetch.ts';
16
+ import { type WatchReport, type WatchSource } from './source.ts';
17
+ interface WatchState {
18
+ /** Keys of entries already reported, newest last. */
19
+ seen: string[];
20
+ /** Fingerprint of the whole page, for addresses that are not feeds. */
21
+ digest?: string;
22
+ /** First check done. */
23
+ primed: boolean;
24
+ }
25
+ /** Which items are new since last time, and whether the whole page changed. */
26
+ export declare function diffItems(items: WatchItem[], state: WatchState): {
27
+ fresh: WatchItem[];
28
+ changed: boolean;
29
+ digest: string;
30
+ };
31
+ export declare function judgeWatch(options: {
32
+ source: WatchSource;
33
+ items: WatchItem[];
34
+ fresh: WatchItem[];
35
+ changed: boolean;
36
+ first: boolean;
37
+ now: number;
38
+ }): Verdict | undefined;
39
+ export declare function syncWatch(options: {
40
+ store: MishuStore;
41
+ source: WatchSource;
42
+ fetcher: WatchFetch;
43
+ now: number;
44
+ }): Promise<WatchReport>;
45
+ /** Everything about one watch the settings page shows. */
46
+ export declare function describeWatch(store: MishuStore, source: WatchSource, lang: 'zh' | 'en'): string;
47
+ /** The words, for showing back to you. */
48
+ export declare function describeKeywords(source: WatchSource, lang: 'zh' | 'en'): string;
49
+ /** Guard for the settings page: an address that cannot be a watch at all. */
50
+ export declare function assertWatchable(source: WatchSource): void;
51
+ export {};
@@ -4,8 +4,9 @@
4
4
  * what you set on the settings page on top), the keychain, and one sync of
5
5
  * every source at a time.
6
6
  */
7
- import { GithubDeviceLogin, type Defaults, type Lang, type MailAccount, type MailFetcher, type MailSyncReport, type MishuStore, type Channel, type ChannelId, type NotifyReport, type RestartPlan, type RestartStarted, type SecretStore, type TokenResult } from '../core/index.ts';
7
+ import { GithubDeviceLogin, type Defaults, type Lang, type MailAccount, type MailFetcher, type MailSyncReport, type MishuStore, type Channel, type ChannelId, type NotifyReport, type RestartPlan, type RestartStarted, type SecretStore, type TokenResult, type WatchFetch, type WatchReport, type WatchSource } from '../core/index.ts';
8
8
  import type { MishuConfig } from './config.ts';
9
+ import { ScheduleBridge, type ScheduleLike } from './schedule.ts';
9
10
  interface Logger {
10
11
  info(message: string): void;
11
12
  warn(message: string): void;
@@ -15,6 +16,8 @@ export declare class Runtime {
15
16
  readonly config: MishuConfig;
16
17
  readonly secrets: SecretStore;
17
18
  readonly fetcher: MailFetcher;
19
+ /** How watched addresses are read; tests swap it for a fake. */
20
+ watchFetch: WatchFetch;
18
21
  private readonly logger;
19
22
  private syncing;
20
23
  /** Where reminders can go; tests swap them for recorders. */
@@ -29,6 +32,11 @@ export declare class Runtime {
29
32
  kind: 'none';
30
33
  }>): RestartStarted;
31
34
  };
35
+ /**
36
+ * DSH's own scheduled tasks, when this Host has them. Attached by the shell
37
+ * once the `schedule` service shows up; absent on a Host without it.
38
+ */
39
+ schedule: ScheduleBridge | undefined;
32
40
  constructor(config: MishuConfig, logger: Logger, secrets?: SecretStore, fetcher?: MailFetcher);
33
41
  defaults(): Defaults;
34
42
  settings(): Defaults;
@@ -36,6 +44,14 @@ export declare class Runtime {
36
44
  fixedLang(): Lang | undefined;
37
45
  lang(): Lang;
38
46
  accounts(): MailAccount[];
47
+ watches(): WatchSource[];
48
+ /**
49
+ * Wire in DSH's Schedule service, once. The bridge reads the brief's time
50
+ * from the reminder settings, so a brief moved on this page moves the task
51
+ * too. Called again with the same service (a service restart) keeps the
52
+ * bridge and hands it the new handle.
53
+ */
54
+ attachSchedule(service: ScheduleLike): ScheduleBridge;
39
55
  brief(lang?: Lang): string;
40
56
  /** The password for an account: keychain first, then the documented environment variable. */
41
57
  mailPassword(id: number): Promise<string | undefined>;
@@ -44,6 +60,8 @@ export declare class Runtime {
44
60
  private syncGithubNow;
45
61
  /** One account; the result is also kept as its last report. */
46
62
  syncMailAccount(account: MailAccount): Promise<MailSyncReport | undefined>;
63
+ /** One watch; the result is also kept as its last report. */
64
+ syncWatchSource(source: WatchSource): Promise<WatchReport | undefined>;
47
65
  /** The channels that are turned on and work on this computer. */
48
66
  activeChannels(): Promise<Channel[]>;
49
67
  /** The board as the brief describes it. */