@goodandready/dsh-cron 0.2.2 → 0.2.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.
package/lib/telegram.js CHANGED
@@ -37,13 +37,27 @@ export function extractFirstAllowedChatId(text) {
37
37
  return undefined;
38
38
  }
39
39
 
40
+ /**
41
+ * Harness home for this process. DSH_HOME wins so an isolated profile reads
42
+ * its own settings file instead of another home's (same rule as the store).
43
+ */
44
+ export function getDshHomeDir() {
45
+ const fromEnv = process.env.DSH_HOME;
46
+ if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim();
47
+ return path.join(process.env.HOME || os.homedir(), '.dsh');
48
+ }
49
+
50
+ export function getDshSettingsPath() {
51
+ return path.join(getDshHomeDir(), 'settings.yaml');
52
+ }
53
+
40
54
  /**
41
55
  * Read system DSH settings for fallback credentials.
42
56
  * Fails soft with a logged warning so silent misconfiguration is visible (#93).
43
57
  */
44
58
  export function getDshDefaultTelegramCredentials() {
45
59
  try {
46
- const p = path.join(process.env.HOME || os.homedir(), '.dsh', 'settings.yaml');
60
+ const p = getDshSettingsPath();
47
61
  if (fs.existsSync(p)) {
48
62
  let raw;
49
63
  try {
@@ -105,7 +119,7 @@ export function formatTaskTelegramMessage(task, runInfo) {
105
119
  /**
106
120
  * Send telegram message via Bot API.
107
121
  */
108
- export async function sendTelegramMessage({ botToken, chatId, text, parseMode = 'Markdown' }) {
122
+ export async function sendTelegramMessage({ botToken, chatId, text, parseMode = 'Markdown', fetchFn = globalThis.fetch, signal }) {
109
123
  if (!botToken) throw new Error('Telegram botToken is not configured');
110
124
  if (!chatId) throw new Error('Telegram chatId is not configured');
111
125
 
@@ -117,20 +131,22 @@ export async function sendTelegramMessage({ botToken, chatId, text, parseMode =
117
131
  };
118
132
  if (parseMode) body.parse_mode = parseMode;
119
133
 
120
- const res = await fetch(url, {
134
+ const res = await fetchFn(url, {
121
135
  method: 'POST',
122
136
  headers: { 'Content-Type': 'application/json' },
123
137
  body: JSON.stringify(body),
138
+ signal,
124
139
  });
125
140
 
126
141
  const data = await res.json().catch(() => ({}));
127
142
  if (!res.ok || !data.ok) {
128
143
  if (parseMode && data.description && data.description.includes("can't parse entities")) {
129
144
  delete body.parse_mode;
130
- const retryRes = await fetch(url, {
145
+ const retryRes = await fetchFn(url, {
131
146
  method: 'POST',
132
147
  headers: { 'Content-Type': 'application/json' },
133
148
  body: JSON.stringify(body),
149
+ signal,
134
150
  });
135
151
  const retryData = await retryRes.json().catch(() => ({}));
136
152
  if (retryRes.ok && retryData.ok) {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Notification template engine (#25).
3
+ *
4
+ * Messages for every channel are rendered from a template with `{var}`
5
+ * placeholders. Pure functions only — no I/O — so the whole matrix is
6
+ * unit-testable.
7
+ */
8
+
9
+ export const TEMPLATE_VARIABLES = [
10
+ 'title',
11
+ 'id',
12
+ 'status',
13
+ 'output',
14
+ 'error',
15
+ 'duration',
16
+ 'schedule',
17
+ 'time',
18
+ 'tokens',
19
+ 'cost',
20
+ ];
21
+
22
+ export const DEFAULT_PLAIN_TEMPLATE = '⏰ {title}\nStatus: {status}\nSchedule: {schedule}\nDuration: {duration}\n{output}';
23
+
24
+ export const DEFAULT_FAILURE_TEMPLATE = '❌ {title} failed\nSchedule: {schedule}\nDuration: {duration}\n{error}';
25
+
26
+ const MAX_FIELD = 2000;
27
+
28
+ export function truncateText(text, limit = MAX_FIELD) {
29
+ const s = String(text == null ? '' : text);
30
+ if (s.length <= limit) return s;
31
+ return s.slice(0, limit) + '…';
32
+ }
33
+
34
+ export function formatDuration(ms) {
35
+ if (ms === null || ms === undefined || ms === '') return '—';
36
+ const n = Number(ms);
37
+ if (!Number.isFinite(n) || n < 0) return '—';
38
+ if (n < 1000) return `${Math.round(n)} ms`;
39
+ const seconds = n / 1000;
40
+ if (seconds < 60) return `${seconds.toFixed(1)} s`;
41
+ const minutes = Math.floor(seconds / 60);
42
+ return `${minutes}m ${Math.round(seconds % 60)}s`;
43
+ }
44
+
45
+ /** Build the placeholder map for one run. */
46
+ export function buildTemplateVars(task, runInfo = {}) {
47
+ const status = runInfo.status || 'unknown';
48
+ const output = status === 'success' ? (runInfo.output || '') : '';
49
+ const error = runInfo.error || (status === 'error' || status === 'timeout' ? runInfo.output || '' : '');
50
+ const usage = runInfo.usage || {};
51
+ const tokens = (usage.inputTokens || 0) + (usage.outputTokens || 0) + (usage.cacheReadTokens || 0);
52
+ return {
53
+ title: task.title || 'Task',
54
+ id: task.id || '',
55
+ status,
56
+ output: truncateText(output),
57
+ error: truncateText(error),
58
+ duration: formatDuration(runInfo.durationMs),
59
+ schedule: task.scheduleText || task.schedule || '',
60
+ time: new Date(runInfo.at || Date.now()).toISOString(),
61
+ tokens: String(tokens),
62
+ cost: `$${Number(runInfo.costUsd || 0).toFixed(4)}`,
63
+ };
64
+ }
65
+
66
+ /** Replace {var} placeholders; unknown placeholders are left untouched. */
67
+ export function renderTemplate(template, vars) {
68
+ return String(template == null ? '' : template).replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (match, name) => {
69
+ if (Object.prototype.hasOwnProperty.call(vars, name)) {
70
+ const value = vars[name];
71
+ return value === null || value === undefined ? '' : String(value);
72
+ }
73
+ return match;
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Pick the template for a run: failure template for failed runs unless a
79
+ * custom template is configured, plain template otherwise.
80
+ */
81
+ export function resolveTemplateText({ template, task, runInfo }) {
82
+ const custom = typeof template === 'string' && template.trim() ? template.trim() : '';
83
+ if (custom) return renderTemplate(custom, buildTemplateVars(task, runInfo));
84
+ const failed = runInfo.status === 'error' || runInfo.status === 'timeout';
85
+ const base = failed ? DEFAULT_FAILURE_TEMPLATE : DEFAULT_PLAIN_TEMPLATE;
86
+ const text = renderTemplate(base, buildTemplateVars(task, runInfo));
87
+ return text.replace(/\n{3,}/g, '\n\n').trim();
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",