pawmode 1.0.1 → 1.2.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 +224 -66
- package/dist/dashboard-server--wwlA0Pa.js +426 -0
- package/dist/dashboard-server-Cg_1CvKn.js +3 -0
- package/dist/index.js +668 -831
- package/dist/permissions-AJXigU7k.js +3 -0
- package/dist/scheduler-DAmd0GzB.js +888 -0
- package/dist/scheduler-DppXPNqK.js +4 -0
- package/dist/{skills-DwMXaN3R.js → skills-CUY0swcW.js} +1 -1
- package/package.json +1 -1
- package/skills/c-clipboard/SKILL.md +53 -0
- package/skills/c-contacts/SKILL.md +63 -0
- package/skills/c-core/SKILL.md +17 -1
- package/skills/c-memory/SKILL.md +32 -0
- package/skills/c-obsidian/SKILL.md +22 -3
- package/skills/c-schedule/SKILL.md +98 -0
- package/skills/c-timer/SKILL.md +59 -0
- package/skills/c-video-edit/SKILL.md +147 -0
- package/skills/c-weather/SKILL.md +61 -0
- package/dist/permissions-CoaVX2ZM.js +0 -3
- /package/dist/{permissions-BHOAvP8i.js → permissions-BlGEHCXO.js} +0 -0
- /package/dist/{skills-CJ_pyPlv.js → skills-CMqq9k1-.js} +0 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
import { listInstalledSkills } from "./skills-CMqq9k1-.js";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import * as os$2 from "node:os";
|
|
4
|
+
import * as os$1 from "node:os";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import * as fs$2 from "node:fs";
|
|
8
|
+
import * as fs$1 from "node:fs";
|
|
9
|
+
import * as path$2 from "node:path";
|
|
10
|
+
import * as path$1 from "node:path";
|
|
11
|
+
import { Bot } from "grammy";
|
|
12
|
+
import { hydrate } from "@grammyjs/hydrate";
|
|
13
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
14
|
+
import * as crypto from "node:crypto";
|
|
15
|
+
|
|
16
|
+
//#region src/core/branding.ts
|
|
17
|
+
const accent = chalk.hex("#b4783c");
|
|
18
|
+
const subtle = chalk.hex("#8a5a2a");
|
|
19
|
+
const dim = chalk.dim;
|
|
20
|
+
const bold = chalk.bold;
|
|
21
|
+
const pawClr = chalk.hex("#b4783c");
|
|
22
|
+
const PAW_ART = [
|
|
23
|
+
" ▃▅",
|
|
24
|
+
" ▁██▁ ▄█▁",
|
|
25
|
+
" ▁▁▂▆▇██▃ ▅█▆",
|
|
26
|
+
" ▅▆█▇▆▄███▆▂ ▁▂▆▇██▇▅▁",
|
|
27
|
+
" ▁▃█▆▁ ▄███▄▁ ▆▇▇▅▄▄███▇▃▁",
|
|
28
|
+
" ▁▃█▄ ▁████▂ ▁▅█▃ ▁▃████▂",
|
|
29
|
+
" ▄█▇▁ ▂████▄▁ ▁▃█▇ ▁████▅▂",
|
|
30
|
+
" ▅█▆ ▂█████▁ ▄█▇▁ ▂████▃",
|
|
31
|
+
" ▂▁ ▆█▆ ▂█████▁ ▄█▇ ▁▂████▂",
|
|
32
|
+
" ▁▅█▂ ▆█▆▁ ▁▃▇████▆▁ ▄█▇ ▂▄█████▂",
|
|
33
|
+
" ▁██▄▂ ▂██▇▆▃▄▇██████ ▄██▄▇▇▃▃▆█████▁",
|
|
34
|
+
" ▂█████▇▅▂▁ ▁▄▇█████████▆▂ ▃▇████████████▁ ▁▂▁",
|
|
35
|
+
" ▄█▇▃ ▁███▆▁ ▁▃▆▇▇▇▇▃▄▂ ▂▆█████████▅ ▁▇█▂",
|
|
36
|
+
" ▃▆▄▁ ▅███▆▃ ▄▄▅▅▅▅▂▁ ▂▄▃▄██▂",
|
|
37
|
+
" ▆█▄ ▃████▄ ▁▄▄██████▄▃▃▂ ▁▂▆▇▇▆████▇▁",
|
|
38
|
+
" ▆█▄ ▂████▄ ▂▅▇▇▅▅▁▃▅█▇████▆▂ ▅▇▇▅▄▁ ▂▆████▂",
|
|
39
|
+
" ▇█▄ ▅████▄ ▇█▄ ▁▃▇▆████▇▂ ▂▃█▇▃ ▁ ▃███▂",
|
|
40
|
+
" ▇██▅ ▂▆████▄▁ ▆█▄▁ ▁ ▅▄▅█████▅ ▆█▆▂ ▃▆███▁",
|
|
41
|
+
" ▁████████████▃ ▂▇▆▂ ▄█████▆ ▃██▂ ▂▆███▆",
|
|
42
|
+
" ▃█████████▅▂ ██▂ ▄█████▆ ▅██▁ ▁████▇",
|
|
43
|
+
" ▂▄▄▄▄▄▄ ▇█▃▁ ▁▅█████▅ ▃██▄▇▆▁▁▄▇████▆",
|
|
44
|
+
" ▁▁▁▇█▂▁ ▁ ▄██████▆▂ ▇███████████▅▁",
|
|
45
|
+
" ▁▁▄▅▆██▅▂ ▁▁ ▄███████▄ ▄▅██████▆▅▂",
|
|
46
|
+
" ▄██▆▅▄▂▁ ▁ ▃▆██████▇▄▂ ▁▁▁▁▁▁▁",
|
|
47
|
+
" ▂██▄▁▁ ▁▄▅██████▇▆▂▁",
|
|
48
|
+
" ▃▇▇▂ ▁▁ ▁ ▃▇██████▇▃▁",
|
|
49
|
+
" ▁▄█▆ ▁ ▁▆█▇█████▆▂",
|
|
50
|
+
" ▄██▇ ▃▅███████▅▁",
|
|
51
|
+
" ▄██▇▁ ▂▂▂▁▁▂▆▇▇▇▇▇▃▁ ▄▆▄▂▇█████▃",
|
|
52
|
+
" ▃▇██▇▃ ▁▂▆▆██▆▇█████████▆▄▁▁ ▁▁▁▄▇██████▃",
|
|
53
|
+
" ▄█████▆▆▇███████████████████▆▃▁ ▁▄▇███████▃",
|
|
54
|
+
" ▁▅█████████████████████████████▅█████████▄▁",
|
|
55
|
+
" ▁▂▆█████████████████████████████████████▂",
|
|
56
|
+
" ▁▂▆▆▆▆▆▆▃▂▂▂▂▂▂▂▂▂▂▂▂▂▅▆███████████▇▆▁",
|
|
57
|
+
" ▃▃▇▇▇▇▇▇▇▃▃"
|
|
58
|
+
];
|
|
59
|
+
const PAW_ROWS = PAW_ART.length;
|
|
60
|
+
function sleep(ms) {
|
|
61
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
62
|
+
}
|
|
63
|
+
const TITLE_3D = [
|
|
64
|
+
"█▀▀█░█▀▀▄░█▀▀▀░█▄ █░█▀▀▄░▄▀▀▄░█ █░",
|
|
65
|
+
"█ █░█▀▀ ░█▀▀ ░█ ▀█░█▀▀ ░█▀▀█░█ █ █░",
|
|
66
|
+
"▀▀▀▀░▀ ░▀▀▀▀░▀ ▀░▀ ░▀ ▀░ ▀ ▀ ░",
|
|
67
|
+
"░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░"
|
|
68
|
+
];
|
|
69
|
+
const TITLE_MARGIN = " ";
|
|
70
|
+
function renderTitle() {
|
|
71
|
+
const lines = TITLE_3D.map((artLine) => {
|
|
72
|
+
let result = "";
|
|
73
|
+
let buf = "";
|
|
74
|
+
let bufType = "space";
|
|
75
|
+
const flush = () => {
|
|
76
|
+
if (!buf) return;
|
|
77
|
+
if (bufType === "main") result += pawClr(buf);
|
|
78
|
+
else if (bufType === "depth") result += subtle(buf);
|
|
79
|
+
else result += buf;
|
|
80
|
+
buf = "";
|
|
81
|
+
};
|
|
82
|
+
for (const ch of artLine) {
|
|
83
|
+
const type = ch === "░" ? "depth" : ch === " " ? "space" : "main";
|
|
84
|
+
if (type !== bufType) {
|
|
85
|
+
flush();
|
|
86
|
+
bufType = type;
|
|
87
|
+
}
|
|
88
|
+
buf += ch;
|
|
89
|
+
}
|
|
90
|
+
flush();
|
|
91
|
+
const margin = artLine === TITLE_3D[TITLE_3D.length - 1] ? TITLE_MARGIN.slice(1) : TITLE_MARGIN;
|
|
92
|
+
return margin + result;
|
|
93
|
+
});
|
|
94
|
+
const sub = dim(" Personal Assistant Wizard for Claude Code");
|
|
95
|
+
return lines.join("\n") + "\n" + sub;
|
|
96
|
+
}
|
|
97
|
+
const MOOD_HEX = {
|
|
98
|
+
wave: "#b4783c",
|
|
99
|
+
think: "#b4783c",
|
|
100
|
+
happy: "#b4783c",
|
|
101
|
+
work: "#9a6832",
|
|
102
|
+
done: "#c88a48",
|
|
103
|
+
warn: "#dca03c"
|
|
104
|
+
};
|
|
105
|
+
function pawColor(mood) {
|
|
106
|
+
return chalk.hex(MOOD_HEX[mood]);
|
|
107
|
+
}
|
|
108
|
+
function renderPaw(color) {
|
|
109
|
+
return PAW_ART.map((line) => color(line)).join("\n");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Banner: brown paw + ASCII title.
|
|
113
|
+
*/
|
|
114
|
+
async function showBanner() {
|
|
115
|
+
console.log(renderPaw(pawClr));
|
|
116
|
+
console.log("");
|
|
117
|
+
console.log(renderTitle());
|
|
118
|
+
console.log("");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Show paw between wizard steps — mood-colored, brief flash, then clears.
|
|
122
|
+
*/
|
|
123
|
+
async function pawStep(mood, message) {
|
|
124
|
+
const color = pawColor(mood);
|
|
125
|
+
process.stdout.write(renderPaw(color) + "\n");
|
|
126
|
+
if (message) console.log(` ${accent(message)}`);
|
|
127
|
+
await sleep(300);
|
|
128
|
+
const lines = PAW_ROWS + (message ? 1 : 0);
|
|
129
|
+
process.stdout.write(`\x1B[${lines}A\x1B[J`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Inline pulse indicator for quick transitions.
|
|
133
|
+
*/
|
|
134
|
+
async function pawPulse(mood, message) {
|
|
135
|
+
if (!message) return;
|
|
136
|
+
const line = ` ${accent("◉")} ${subtle(message)}`;
|
|
137
|
+
for (let i = 0; i < 3; i++) {
|
|
138
|
+
if (i > 0) process.stdout.write("\x1B[1A");
|
|
139
|
+
const s = i % 2 === 0 ? bold : dim;
|
|
140
|
+
process.stdout.write(`\x1B[2K${s(line)}\n`);
|
|
141
|
+
await sleep(80);
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(`\x1B[1A\x1B[2K${line}\n`);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Mini one-liner.
|
|
147
|
+
*/
|
|
148
|
+
function showMini() {
|
|
149
|
+
console.log(accent(" ◉ openpaw") + dim(" — Personal Assistant Wizard for Claude Code"));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Puppy disclaimer about --dangerously-skip-permissions.
|
|
153
|
+
*/
|
|
154
|
+
function showPuppyDisclaimer() {
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(pawClr(" /\\_/\\"));
|
|
157
|
+
console.log(pawClr(" ( o.o )") + ` ${bold("WOOF! One important sniff...")}`);
|
|
158
|
+
console.log(pawClr(" > ^ <"));
|
|
159
|
+
console.log("");
|
|
160
|
+
console.log(` ${accent("You're about to let Claude off the leash!")}`);
|
|
161
|
+
console.log(dim(" (--dangerously-skip-permissions)"));
|
|
162
|
+
console.log("");
|
|
163
|
+
console.log(" This lets Claude run commands without asking each time.");
|
|
164
|
+
console.log(" It's how your assistant actually gets things done —");
|
|
165
|
+
console.log(" checking email, playing music, managing files.");
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log(dim(" OpenPaw's safety hooks still block the dangerous stuff"));
|
|
168
|
+
console.log(dim(" (mass deletes, credential leaks, etc)."));
|
|
169
|
+
console.log("");
|
|
170
|
+
console.log(dim(" You can always run 'claude' normally without this."));
|
|
171
|
+
console.log("");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/core/telegram.ts
|
|
176
|
+
const CONFIG_DIR$1 = path$2.join(os$2.homedir(), ".config", "openpaw");
|
|
177
|
+
const CONFIG_PATH = path$2.join(CONFIG_DIR$1, "telegram.json");
|
|
178
|
+
function writeTelegramConfig(config) {
|
|
179
|
+
fs$2.mkdirSync(CONFIG_DIR$1, { recursive: true });
|
|
180
|
+
fs$2.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
181
|
+
fs$2.chmodSync(CONFIG_PATH, 384);
|
|
182
|
+
}
|
|
183
|
+
function readTelegramConfig() {
|
|
184
|
+
try {
|
|
185
|
+
const raw = fs$2.readFileSync(CONFIG_PATH, "utf-8");
|
|
186
|
+
return JSON.parse(raw);
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function telegramConfigExists() {
|
|
192
|
+
return fs$2.existsSync(CONFIG_PATH);
|
|
193
|
+
}
|
|
194
|
+
async function telegramQuestionnaire() {
|
|
195
|
+
p.log.info(dim("Let's set up your Telegram bot! You'll need:"));
|
|
196
|
+
p.log.info(` ${accent("1.")} Message ${bold("@BotFather")} on Telegram → /newbot`);
|
|
197
|
+
p.log.info(` ${accent("2.")} Message ${bold("@userinfobot")} to get your user ID`);
|
|
198
|
+
console.log("");
|
|
199
|
+
const botToken = await p.text({
|
|
200
|
+
message: "Paste your bot token (from @BotFather):",
|
|
201
|
+
placeholder: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
|
|
202
|
+
validate: (v) => {
|
|
203
|
+
if (v.length === 0) return "Bot token is required";
|
|
204
|
+
if (!v.includes(":")) return "That doesn't look like a bot token (should contain ':')";
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
if (p.isCancel(botToken)) return null;
|
|
209
|
+
const userId = await p.text({
|
|
210
|
+
message: "Your Telegram user ID (from @userinfobot):",
|
|
211
|
+
placeholder: "123456789",
|
|
212
|
+
validate: (v) => {
|
|
213
|
+
if (v.length === 0) return "User ID is required";
|
|
214
|
+
if (!/^\d+$/.test(v)) return "User ID should be a number";
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
if (p.isCancel(userId)) return null;
|
|
219
|
+
return {
|
|
220
|
+
botToken,
|
|
221
|
+
allowedUserIds: [userId.trim()],
|
|
222
|
+
workspaceDir: os$2.homedir(),
|
|
223
|
+
model: "sonnet",
|
|
224
|
+
skills: []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const sessions = new Map();
|
|
228
|
+
const MODEL_MAP$1 = {
|
|
229
|
+
sonnet: "claude-sonnet-4-5-20250514",
|
|
230
|
+
opus: "claude-opus-4-6",
|
|
231
|
+
haiku: "claude-haiku-4-5-20251001"
|
|
232
|
+
};
|
|
233
|
+
function getModelId(shortName) {
|
|
234
|
+
return MODEL_MAP$1[shortName] || MODEL_MAP$1.sonnet;
|
|
235
|
+
}
|
|
236
|
+
async function startTelegramBot(config) {
|
|
237
|
+
const bot = new Bot(config.botToken);
|
|
238
|
+
bot.use(hydrate());
|
|
239
|
+
const allowedIds = new Set(config.allowedUserIds.map(Number));
|
|
240
|
+
let currentModel = config.model || "sonnet";
|
|
241
|
+
bot.use(async (ctx, next) => {
|
|
242
|
+
if (!ctx.from || !allowedIds.has(ctx.from.id)) {
|
|
243
|
+
await ctx.reply("Woof! I don't know you. Unauthorized. 🐾");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await next();
|
|
247
|
+
});
|
|
248
|
+
const installedSkills = listInstalledSkills();
|
|
249
|
+
const skillCommands = installedSkills.filter((id) => id !== "core" && id !== "memory").map((id) => ({
|
|
250
|
+
command: id,
|
|
251
|
+
description: `Use the ${id} skill`
|
|
252
|
+
}));
|
|
253
|
+
const allCommands = [
|
|
254
|
+
{
|
|
255
|
+
command: "start",
|
|
256
|
+
description: "Start the bot"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
command: "model",
|
|
260
|
+
description: "Switch Claude model (sonnet/opus/haiku)"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
command: "skills",
|
|
264
|
+
description: "List installed skills"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
command: "stop",
|
|
268
|
+
description: "Cancel current operation"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
command: "clear",
|
|
272
|
+
description: "Reset conversation"
|
|
273
|
+
},
|
|
274
|
+
...skillCommands
|
|
275
|
+
];
|
|
276
|
+
try {
|
|
277
|
+
await bot.api.setMyCommands(allCommands);
|
|
278
|
+
} catch {}
|
|
279
|
+
bot.command("start", async (ctx) => {
|
|
280
|
+
const skills = installedSkills.filter((id) => id !== "core" && id !== "memory");
|
|
281
|
+
await ctx.reply(`*PAW MODE active* 🐾
|
|
282
|
+
|
|
283
|
+
I'm your personal assistant, powered by OpenPaw.
|
|
284
|
+
Model: \`${currentModel}\`\nSkills: ${skills.length > 0 ? skills.map((s) => `/${s}`).join(", ") : "none"}\n\nJust send me a message or use a /command!`, { parse_mode: "Markdown" });
|
|
285
|
+
});
|
|
286
|
+
bot.command("model", async (ctx) => {
|
|
287
|
+
const arg = ctx.match?.trim().toLowerCase();
|
|
288
|
+
if (!arg || ![
|
|
289
|
+
"sonnet",
|
|
290
|
+
"opus",
|
|
291
|
+
"haiku"
|
|
292
|
+
].includes(arg)) {
|
|
293
|
+
await ctx.reply(`Current model: \`${currentModel}\`\n\nSwitch with:\n/model sonnet\n/model opus\n/model haiku`, { parse_mode: "Markdown" });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
currentModel = arg;
|
|
297
|
+
config.model = arg;
|
|
298
|
+
writeTelegramConfig(config);
|
|
299
|
+
await ctx.reply(`Model switched to \`${currentModel}\` 🐾`, { parse_mode: "Markdown" });
|
|
300
|
+
});
|
|
301
|
+
bot.command("skills", async (ctx) => {
|
|
302
|
+
const skills = installedSkills.filter((id) => id !== "core" && id !== "memory");
|
|
303
|
+
if (skills.length === 0) {
|
|
304
|
+
await ctx.reply("No skills installed yet. Run `openpaw setup` first! 🐾");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const list = skills.map((s) => `• /${s}`).join("\n");
|
|
308
|
+
await ctx.reply(`*Installed skills:*\n\n${list}`, { parse_mode: "Markdown" });
|
|
309
|
+
});
|
|
310
|
+
bot.command("stop", async (ctx) => {
|
|
311
|
+
const userId = ctx.from.id;
|
|
312
|
+
const session = sessions.get(userId);
|
|
313
|
+
if (session?.controller) {
|
|
314
|
+
session.controller.abort();
|
|
315
|
+
sessions.delete(userId);
|
|
316
|
+
await ctx.reply("Operation cancelled. 🐾");
|
|
317
|
+
} else await ctx.reply("Nothing running right now. 🐾");
|
|
318
|
+
});
|
|
319
|
+
bot.command("clear", async (ctx) => {
|
|
320
|
+
const userId = ctx.from.id;
|
|
321
|
+
sessions.delete(userId);
|
|
322
|
+
await ctx.reply("Conversation cleared! Fresh start. 🐾");
|
|
323
|
+
});
|
|
324
|
+
for (const skillId of installedSkills) {
|
|
325
|
+
if (skillId === "core" || skillId === "memory") continue;
|
|
326
|
+
bot.command(skillId, async (ctx) => {
|
|
327
|
+
const args = ctx.match || "";
|
|
328
|
+
const prompt = args ? `Use the c-${skillId} skill: ${args}` : `What can the c-${skillId} skill do? Give a brief overview.`;
|
|
329
|
+
await handleClaudeMessage(ctx, prompt, currentModel, config);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
bot.on("message:text", async (ctx) => {
|
|
333
|
+
await handleClaudeMessage(ctx, ctx.msg.text, currentModel, config);
|
|
334
|
+
});
|
|
335
|
+
bot.catch((err) => {
|
|
336
|
+
console.error("Bot error:", err.message || err);
|
|
337
|
+
});
|
|
338
|
+
process.on("SIGINT", () => {
|
|
339
|
+
console.log("\nShutting down gracefully... 🐾");
|
|
340
|
+
bot.stop();
|
|
341
|
+
process.exit(0);
|
|
342
|
+
});
|
|
343
|
+
process.on("SIGTERM", () => {
|
|
344
|
+
bot.stop();
|
|
345
|
+
process.exit(0);
|
|
346
|
+
});
|
|
347
|
+
console.log("");
|
|
348
|
+
console.log(` 🐾 ${bold("OpenPaw Telegram Bridge")}`);
|
|
349
|
+
console.log(` Model: ${accent(currentModel)}`);
|
|
350
|
+
console.log(` Skills: ${accent(String(installedSkills.length))}`);
|
|
351
|
+
console.log(` Workspace: ${dim(config.workspaceDir)}`);
|
|
352
|
+
console.log(` Allowed users: ${dim(config.allowedUserIds.join(", "))}`);
|
|
353
|
+
console.log("");
|
|
354
|
+
console.log(dim(" Listening for messages... (Ctrl+C to stop)"));
|
|
355
|
+
console.log("");
|
|
356
|
+
await bot.start();
|
|
357
|
+
}
|
|
358
|
+
async function handleClaudeMessage(ctx, prompt, model, config) {
|
|
359
|
+
const userId = ctx.from.id;
|
|
360
|
+
const existing = sessions.get(userId);
|
|
361
|
+
if (existing?.controller) existing.controller.abort();
|
|
362
|
+
const controller = new AbortController();
|
|
363
|
+
const session = sessions.get(userId) || {};
|
|
364
|
+
session.controller = controller;
|
|
365
|
+
sessions.set(userId, session);
|
|
366
|
+
const statusMsg = await ctx.reply("Thinking... 🐾");
|
|
367
|
+
let fullText = "";
|
|
368
|
+
let lastEditTime = 0;
|
|
369
|
+
const EDIT_INTERVAL = 1500;
|
|
370
|
+
try {
|
|
371
|
+
const q = query({
|
|
372
|
+
prompt,
|
|
373
|
+
options: {
|
|
374
|
+
model: getModelId(model),
|
|
375
|
+
permissionMode: "bypassPermissions",
|
|
376
|
+
allowDangerouslySkipPermissions: true,
|
|
377
|
+
cwd: config.workspaceDir,
|
|
378
|
+
abortController: controller,
|
|
379
|
+
maxTurns: 25,
|
|
380
|
+
...session.sessionId ? { resume: session.sessionId } : {}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
for await (const message of q) {
|
|
384
|
+
if (controller.signal.aborted) break;
|
|
385
|
+
if (message.type === "system" && "session_id" in message) session.sessionId = message.session_id;
|
|
386
|
+
if (message.type === "assistant") {
|
|
387
|
+
const msgContent = message.message;
|
|
388
|
+
const text = msgContent.content.filter((block) => block.type === "text").map((block) => block.text || "").join("");
|
|
389
|
+
if (text) {
|
|
390
|
+
fullText = text;
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
if (now - lastEditTime > EDIT_INTERVAL) {
|
|
393
|
+
lastEditTime = now;
|
|
394
|
+
const truncated = fullText.length > 4e3 ? `${fullText.slice(0, 4e3)}...` : fullText;
|
|
395
|
+
try {
|
|
396
|
+
await statusMsg.editText(truncated);
|
|
397
|
+
} catch {}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (message.type === "result") {
|
|
402
|
+
const result = message.result;
|
|
403
|
+
if (result) fullText = result;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (fullText) {
|
|
407
|
+
const truncated = fullText.length > 4e3 ? `${fullText.slice(0, 4e3)}...` : fullText;
|
|
408
|
+
try {
|
|
409
|
+
await statusMsg.editText(truncated);
|
|
410
|
+
} catch {
|
|
411
|
+
await ctx.reply(truncated);
|
|
412
|
+
}
|
|
413
|
+
} else await statusMsg.editText("Done! (no text output) 🐾");
|
|
414
|
+
} catch (err) {
|
|
415
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
416
|
+
if (errorMsg.includes("abort") || controller.signal.aborted) return;
|
|
417
|
+
try {
|
|
418
|
+
await statusMsg.editText(`Woof, something went wrong: ${errorMsg.slice(0, 200)} 🐾`);
|
|
419
|
+
} catch {
|
|
420
|
+
await ctx.reply(`Woof, something went wrong: ${errorMsg.slice(0, 200)} 🐾`);
|
|
421
|
+
}
|
|
422
|
+
} finally {
|
|
423
|
+
session.controller = void 0;
|
|
424
|
+
sessions.set(userId, session);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/core/scheduler.ts
|
|
430
|
+
const CONFIG_DIR = path$1.join(os$1.homedir(), ".config", "openpaw");
|
|
431
|
+
const SCHEDULE_PATH = path$1.join(CONFIG_DIR, "schedules.json");
|
|
432
|
+
const COST_PATH = path$1.join(CONFIG_DIR, "schedule-costs.json");
|
|
433
|
+
const RESULTS_DIR = path$1.join(CONFIG_DIR, "schedule-results");
|
|
434
|
+
const LOGS_DIR = path$1.join(CONFIG_DIR, "logs");
|
|
435
|
+
const PLIST_DIR = path$1.join(os$1.homedir(), "Library", "LaunchAgents");
|
|
436
|
+
const MODEL_MAP = {
|
|
437
|
+
sonnet: "claude-sonnet-4-5-20250514",
|
|
438
|
+
opus: "claude-opus-4-6",
|
|
439
|
+
haiku: "claude-haiku-4-5-20251001"
|
|
440
|
+
};
|
|
441
|
+
function ensureDirs() {
|
|
442
|
+
for (const dir of [
|
|
443
|
+
CONFIG_DIR,
|
|
444
|
+
RESULTS_DIR,
|
|
445
|
+
LOGS_DIR
|
|
446
|
+
]) fs$1.mkdirSync(dir, { recursive: true });
|
|
447
|
+
}
|
|
448
|
+
function readScheduleConfig() {
|
|
449
|
+
try {
|
|
450
|
+
const raw = fs$1.readFileSync(SCHEDULE_PATH, "utf-8");
|
|
451
|
+
return JSON.parse(raw);
|
|
452
|
+
} catch {
|
|
453
|
+
return {
|
|
454
|
+
jobs: [],
|
|
455
|
+
dailyCostCapUsd: 5,
|
|
456
|
+
defaultModel: "sonnet"
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function writeScheduleConfig(config) {
|
|
461
|
+
ensureDirs();
|
|
462
|
+
fs$1.writeFileSync(SCHEDULE_PATH, JSON.stringify(config, null, 2));
|
|
463
|
+
fs$1.chmodSync(SCHEDULE_PATH, 384);
|
|
464
|
+
}
|
|
465
|
+
function readCostTracker() {
|
|
466
|
+
try {
|
|
467
|
+
const raw = fs$1.readFileSync(COST_PATH, "utf-8");
|
|
468
|
+
return JSON.parse(raw);
|
|
469
|
+
} catch {
|
|
470
|
+
return {
|
|
471
|
+
entries: [],
|
|
472
|
+
dailyTotals: {}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function writeCostTracker(tracker) {
|
|
477
|
+
ensureDirs();
|
|
478
|
+
fs$1.writeFileSync(COST_PATH, JSON.stringify(tracker, null, 2));
|
|
479
|
+
}
|
|
480
|
+
function parseHumanSchedule(input) {
|
|
481
|
+
const s = input.trim().toLowerCase();
|
|
482
|
+
if (/^\S+\s+\S+\s+\S+\s+\S+\s+\S+$/.test(s) && /\d/.test(s)) return {
|
|
483
|
+
cron: s,
|
|
484
|
+
human: `cron: ${s}`
|
|
485
|
+
};
|
|
486
|
+
const parseTime = (t) => {
|
|
487
|
+
const ampm = t.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/);
|
|
488
|
+
if (ampm) {
|
|
489
|
+
let hour = Number.parseInt(ampm[1], 10);
|
|
490
|
+
const minute = ampm[2] ? Number.parseInt(ampm[2], 10) : 0;
|
|
491
|
+
if (ampm[3] === "pm" && hour !== 12) hour += 12;
|
|
492
|
+
if (ampm[3] === "am" && hour === 12) hour = 0;
|
|
493
|
+
return {
|
|
494
|
+
hour,
|
|
495
|
+
minute
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const h24 = t.match(/^(\d{1,2}):(\d{2})$/);
|
|
499
|
+
if (h24) return {
|
|
500
|
+
hour: Number.parseInt(h24[1], 10),
|
|
501
|
+
minute: Number.parseInt(h24[2], 10)
|
|
502
|
+
};
|
|
503
|
+
return null;
|
|
504
|
+
};
|
|
505
|
+
const everyMin = s.match(/^every\s+(\d+)\s+min(?:ute)?s?$/);
|
|
506
|
+
if (everyMin) {
|
|
507
|
+
const n = everyMin[1];
|
|
508
|
+
return {
|
|
509
|
+
cron: `*/${n} * * * *`,
|
|
510
|
+
human: `every ${n} minutes`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const everyHr = s.match(/^every\s+(\d+)\s+hours?$/);
|
|
514
|
+
if (everyHr) {
|
|
515
|
+
const n = everyHr[1];
|
|
516
|
+
return {
|
|
517
|
+
cron: `0 */${n} * * *`,
|
|
518
|
+
human: `every ${n} hours`
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const daily = s.match(/^daily\s+(.+)$/);
|
|
522
|
+
if (daily) {
|
|
523
|
+
const t = parseTime(daily[1]);
|
|
524
|
+
if (t) return {
|
|
525
|
+
cron: `${t.minute} ${t.hour} * * *`,
|
|
526
|
+
human: `daily at ${daily[1]}`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const weekdays = s.match(/^weekdays?\s+(.+)$/);
|
|
530
|
+
if (weekdays) {
|
|
531
|
+
const t = parseTime(weekdays[1]);
|
|
532
|
+
if (t) return {
|
|
533
|
+
cron: `${t.minute} ${t.hour} * * 1-5`,
|
|
534
|
+
human: `weekdays at ${weekdays[1]}`
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const weekends = s.match(/^weekends?\s+(.+)$/);
|
|
538
|
+
if (weekends) {
|
|
539
|
+
const t = parseTime(weekends[1]);
|
|
540
|
+
if (t) return {
|
|
541
|
+
cron: `${t.minute} ${t.hour} * * 0,6`,
|
|
542
|
+
human: `weekends at ${weekends[1]}`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const dayMap = {
|
|
546
|
+
sunday: "0",
|
|
547
|
+
sundays: "0",
|
|
548
|
+
sun: "0",
|
|
549
|
+
monday: "1",
|
|
550
|
+
mondays: "1",
|
|
551
|
+
mon: "1",
|
|
552
|
+
tuesday: "2",
|
|
553
|
+
tuesdays: "2",
|
|
554
|
+
tue: "2",
|
|
555
|
+
wednesday: "3",
|
|
556
|
+
wednesdays: "3",
|
|
557
|
+
wed: "3",
|
|
558
|
+
thursday: "4",
|
|
559
|
+
thursdays: "4",
|
|
560
|
+
thu: "4",
|
|
561
|
+
friday: "5",
|
|
562
|
+
fridays: "5",
|
|
563
|
+
fri: "5",
|
|
564
|
+
saturday: "6",
|
|
565
|
+
saturdays: "6",
|
|
566
|
+
sat: "6"
|
|
567
|
+
};
|
|
568
|
+
const dayMatch = s.match(/^(\w+)\s+(.+)$/);
|
|
569
|
+
if (dayMatch && dayMap[dayMatch[1]]) {
|
|
570
|
+
const t = parseTime(dayMatch[2]);
|
|
571
|
+
if (t) {
|
|
572
|
+
const dow = dayMap[dayMatch[1]];
|
|
573
|
+
return {
|
|
574
|
+
cron: `${t.minute} ${t.hour} * * ${dow}`,
|
|
575
|
+
human: `${dayMatch[1]}s at ${dayMatch[2]}`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
cron: s,
|
|
581
|
+
human: s
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function addJob(opts) {
|
|
585
|
+
const config = readScheduleConfig();
|
|
586
|
+
const job = {
|
|
587
|
+
...opts,
|
|
588
|
+
id: crypto.randomUUID().slice(0, 8),
|
|
589
|
+
createdAt: new Date().toISOString()
|
|
590
|
+
};
|
|
591
|
+
config.jobs.push(job);
|
|
592
|
+
writeScheduleConfig(config);
|
|
593
|
+
return job;
|
|
594
|
+
}
|
|
595
|
+
function removeJob(id) {
|
|
596
|
+
const config = readScheduleConfig();
|
|
597
|
+
const idx = config.jobs.findIndex((j) => j.id === id);
|
|
598
|
+
if (idx === -1) return false;
|
|
599
|
+
config.jobs.splice(idx, 1);
|
|
600
|
+
writeScheduleConfig(config);
|
|
601
|
+
removeSystemJob(id);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
function getJob(id) {
|
|
605
|
+
return readScheduleConfig().jobs.find((j) => j.id === id);
|
|
606
|
+
}
|
|
607
|
+
function listJobs() {
|
|
608
|
+
return readScheduleConfig().jobs;
|
|
609
|
+
}
|
|
610
|
+
function toggleJob(id, enabled) {
|
|
611
|
+
const config = readScheduleConfig();
|
|
612
|
+
const job = config.jobs.find((j) => j.id === id);
|
|
613
|
+
if (!job) return false;
|
|
614
|
+
job.enabled = enabled;
|
|
615
|
+
writeScheduleConfig(config);
|
|
616
|
+
if (enabled) installSystemJob(job);
|
|
617
|
+
else removeSystemJob(id);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
function todayStr() {
|
|
621
|
+
return new Date().toISOString().slice(0, 10);
|
|
622
|
+
}
|
|
623
|
+
function getTodaysCost() {
|
|
624
|
+
const tracker = readCostTracker();
|
|
625
|
+
return tracker.dailyTotals[todayStr()] || 0;
|
|
626
|
+
}
|
|
627
|
+
function canRunWithinBudget(perRunBudget) {
|
|
628
|
+
const config = readScheduleConfig();
|
|
629
|
+
return getTodaysCost() + perRunBudget <= config.dailyCostCapUsd;
|
|
630
|
+
}
|
|
631
|
+
function recordCost(jobId, costUsd) {
|
|
632
|
+
const tracker = readCostTracker();
|
|
633
|
+
const date = todayStr();
|
|
634
|
+
tracker.entries.push({
|
|
635
|
+
date,
|
|
636
|
+
jobId,
|
|
637
|
+
costUsd,
|
|
638
|
+
timestamp: new Date().toISOString()
|
|
639
|
+
});
|
|
640
|
+
tracker.dailyTotals[date] = (tracker.dailyTotals[date] || 0) + costUsd;
|
|
641
|
+
const cutoff = new Date();
|
|
642
|
+
cutoff.setDate(cutoff.getDate() - 30);
|
|
643
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
644
|
+
tracker.entries = tracker.entries.filter((e) => e.date >= cutoffStr);
|
|
645
|
+
for (const d of Object.keys(tracker.dailyTotals)) if (d < cutoffStr) delete tracker.dailyTotals[d];
|
|
646
|
+
writeCostTracker(tracker);
|
|
647
|
+
}
|
|
648
|
+
function plistLabel(jobId) {
|
|
649
|
+
return `com.openpaw.schedule.${jobId}`;
|
|
650
|
+
}
|
|
651
|
+
function plistPath(jobId) {
|
|
652
|
+
return path$1.join(PLIST_DIR, `${plistLabel(jobId)}.plist`);
|
|
653
|
+
}
|
|
654
|
+
function resolveOpenpawBin() {
|
|
655
|
+
try {
|
|
656
|
+
return execSync("which openpaw", { encoding: "utf-8" }).trim();
|
|
657
|
+
} catch {
|
|
658
|
+
return "";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function generatePlist(job) {
|
|
662
|
+
const [minute, hour, _dom, _month, dow] = job.schedule.split(" ");
|
|
663
|
+
const openpawBin = resolveOpenpawBin();
|
|
664
|
+
let programArgs;
|
|
665
|
+
if (openpawBin) programArgs = ` <string>${openpawBin}</string>\n <string>schedule</string>\n <string>run</string>\n <string>${job.id}</string>`;
|
|
666
|
+
else {
|
|
667
|
+
const npxPath = execSync("which npx", { encoding: "utf-8" }).trim();
|
|
668
|
+
programArgs = ` <string>${npxPath}</string>\n <string>openpaw</string>\n <string>schedule</string>\n <string>run</string>\n <string>${job.id}</string>`;
|
|
669
|
+
}
|
|
670
|
+
let calendarInterval = " <dict>\n";
|
|
671
|
+
if (minute !== "*" && !minute.startsWith("*/")) calendarInterval += ` <key>Minute</key>\n <integer>${Number.parseInt(minute, 10)}</integer>\n`;
|
|
672
|
+
if (hour !== "*" && !hour.startsWith("*/")) calendarInterval += ` <key>Hour</key>\n <integer>${Number.parseInt(hour, 10)}</integer>\n`;
|
|
673
|
+
if (dow !== "*") {
|
|
674
|
+
const days = dow.split(",");
|
|
675
|
+
if (days.length === 1 && !dow.includes("-")) calendarInterval += ` <key>Weekday</key>\n <integer>${Number.parseInt(dow, 10)}</integer>\n`;
|
|
676
|
+
}
|
|
677
|
+
calendarInterval += " </dict>";
|
|
678
|
+
const isInterval = minute.startsWith("*/") || hour.startsWith("*/");
|
|
679
|
+
let intervalOrCalendar;
|
|
680
|
+
if (isInterval) {
|
|
681
|
+
let seconds = 0;
|
|
682
|
+
if (minute.startsWith("*/")) seconds = Number.parseInt(minute.slice(2), 10) * 60;
|
|
683
|
+
else if (hour.startsWith("*/")) seconds = Number.parseInt(hour.slice(2), 10) * 3600;
|
|
684
|
+
intervalOrCalendar = ` <key>StartInterval</key>\n <integer>${seconds}</integer>`;
|
|
685
|
+
} else if (dow.includes("-")) {
|
|
686
|
+
const [start, end] = dow.split("-").map(Number);
|
|
687
|
+
const entries = [];
|
|
688
|
+
for (let d = start; d <= end; d++) {
|
|
689
|
+
let entry = " <dict>\n";
|
|
690
|
+
if (minute !== "*") entry += ` <key>Minute</key>\n <integer>${Number.parseInt(minute, 10)}</integer>\n`;
|
|
691
|
+
if (hour !== "*") entry += ` <key>Hour</key>\n <integer>${Number.parseInt(hour, 10)}</integer>\n`;
|
|
692
|
+
entry += ` <key>Weekday</key>\n <integer>${d}</integer>\n`;
|
|
693
|
+
entry += " </dict>";
|
|
694
|
+
entries.push(entry);
|
|
695
|
+
}
|
|
696
|
+
intervalOrCalendar = ` <key>StartCalendarInterval</key>\n <array>\n${entries.join("\n")}\n </array>`;
|
|
697
|
+
} else intervalOrCalendar = ` <key>StartCalendarInterval</key>\n${calendarInterval}`;
|
|
698
|
+
const logPath = path$1.join(LOGS_DIR, `${job.id}.log`);
|
|
699
|
+
const errPath = path$1.join(LOGS_DIR, `${job.id}.err`);
|
|
700
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
701
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
702
|
+
<plist version="1.0">
|
|
703
|
+
<dict>
|
|
704
|
+
<key>Label</key>
|
|
705
|
+
<string>${plistLabel(job.id)}</string>
|
|
706
|
+
<key>ProgramArguments</key>
|
|
707
|
+
<array>
|
|
708
|
+
${programArgs}
|
|
709
|
+
</array>
|
|
710
|
+
${intervalOrCalendar}
|
|
711
|
+
<key>StandardOutPath</key>
|
|
712
|
+
<string>${logPath}</string>
|
|
713
|
+
<key>StandardErrorPath</key>
|
|
714
|
+
<string>${errPath}</string>
|
|
715
|
+
<key>EnvironmentVariables</key>
|
|
716
|
+
<dict>
|
|
717
|
+
<key>PATH</key>
|
|
718
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${path$1.join(os$1.homedir(), ".npm-global", "bin")}</string>
|
|
719
|
+
<key>HOME</key>
|
|
720
|
+
<string>${os$1.homedir()}</string>
|
|
721
|
+
</dict>
|
|
722
|
+
</dict>
|
|
723
|
+
</plist>`;
|
|
724
|
+
}
|
|
725
|
+
function installSystemJob(job) {
|
|
726
|
+
ensureDirs();
|
|
727
|
+
if (process.platform === "darwin") try {
|
|
728
|
+
fs$1.mkdirSync(PLIST_DIR, { recursive: true });
|
|
729
|
+
const plist = generatePlist(job);
|
|
730
|
+
const p$1 = plistPath(job.id);
|
|
731
|
+
try {
|
|
732
|
+
execSync(`launchctl unload "${p$1}" 2>/dev/null`, { stdio: "ignore" });
|
|
733
|
+
} catch {}
|
|
734
|
+
fs$1.writeFileSync(p$1, plist);
|
|
735
|
+
execSync(`launchctl load "${p$1}"`);
|
|
736
|
+
return true;
|
|
737
|
+
} catch {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
const openpawBin = resolveOpenpawBin() || "npx openpaw";
|
|
742
|
+
const line = `${job.schedule} ${openpawBin} schedule run ${job.id} >> ${LOGS_DIR}/${job.id}.log 2>&1 # openpaw:${job.id}`;
|
|
743
|
+
const existing = execSync("crontab -l 2>/dev/null || true", { encoding: "utf-8" });
|
|
744
|
+
const filtered = existing.split("\n").filter((l) => !l.includes(`openpaw:${job.id}`));
|
|
745
|
+
filtered.push(line);
|
|
746
|
+
const content = filtered.filter(Boolean).join("\n") + "\n";
|
|
747
|
+
execSync(`echo '${content.replace(/'/g, "'\\''")}' | crontab -`);
|
|
748
|
+
return true;
|
|
749
|
+
} catch {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function removeSystemJob(jobId) {
|
|
754
|
+
if (process.platform === "darwin") {
|
|
755
|
+
const p$1 = plistPath(jobId);
|
|
756
|
+
try {
|
|
757
|
+
execSync(`launchctl unload "${p$1}" 2>/dev/null`, { stdio: "ignore" });
|
|
758
|
+
} catch {}
|
|
759
|
+
try {
|
|
760
|
+
fs$1.unlinkSync(p$1);
|
|
761
|
+
} catch {}
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
const existing = execSync("crontab -l 2>/dev/null || true", { encoding: "utf-8" });
|
|
766
|
+
const filtered = existing.split("\n").filter((l) => !l.includes(`openpaw:${jobId}`));
|
|
767
|
+
const content = filtered.filter(Boolean).join("\n") + "\n";
|
|
768
|
+
execSync(`echo '${content.replace(/'/g, "'\\''")}' | crontab -`);
|
|
769
|
+
return true;
|
|
770
|
+
} catch {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function deliverResult(job, text) {
|
|
775
|
+
const delivery = job.delivery;
|
|
776
|
+
const truncated = text.length > 4e3 ? `${text.slice(0, 4e3)}...` : text;
|
|
777
|
+
if (delivery.type === "telegram") {
|
|
778
|
+
const tgConfig = readTelegramConfig();
|
|
779
|
+
if (tgConfig) {
|
|
780
|
+
const header = `*[${job.name}]*\n\n`;
|
|
781
|
+
const body = header + truncated;
|
|
782
|
+
for (const userId of tgConfig.allowedUserIds) try {
|
|
783
|
+
await fetch(`https://api.telegram.org/bot${tgConfig.botToken}/sendMessage`, {
|
|
784
|
+
method: "POST",
|
|
785
|
+
headers: { "Content-Type": "application/json" },
|
|
786
|
+
body: JSON.stringify({
|
|
787
|
+
chat_id: userId,
|
|
788
|
+
text: body,
|
|
789
|
+
parse_mode: "Markdown"
|
|
790
|
+
})
|
|
791
|
+
});
|
|
792
|
+
} catch {}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (delivery.type === "notify") try {
|
|
796
|
+
const title = `OpenPaw: ${job.name}`;
|
|
797
|
+
const msg = truncated.slice(0, 200);
|
|
798
|
+
execSync(`terminal-notifier -title "${title}" -message "${msg.replace(/"/g, "\\\"")}"`, { stdio: "ignore" });
|
|
799
|
+
} catch {}
|
|
800
|
+
ensureDirs();
|
|
801
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
802
|
+
const resultPath = path$1.join(RESULTS_DIR, `${job.id}-${ts}.md`);
|
|
803
|
+
fs$1.writeFileSync(resultPath, `# ${job.name}\n\n**Run:** ${new Date().toISOString()}\n**Model:** ${job.model}\n\n---\n\n${text}`);
|
|
804
|
+
}
|
|
805
|
+
async function runJob(jobId) {
|
|
806
|
+
const job = getJob(jobId);
|
|
807
|
+
if (!job) return {
|
|
808
|
+
success: false,
|
|
809
|
+
error: `Job ${jobId} not found`
|
|
810
|
+
};
|
|
811
|
+
if (!job.enabled) return {
|
|
812
|
+
success: false,
|
|
813
|
+
error: `Job ${jobId} is disabled`
|
|
814
|
+
};
|
|
815
|
+
if (!canRunWithinBudget(job.maxBudgetUsd)) {
|
|
816
|
+
const config = readScheduleConfig();
|
|
817
|
+
const j = config.jobs.find((x) => x.id === jobId);
|
|
818
|
+
if (j) {
|
|
819
|
+
j.lastRunAt = new Date().toISOString();
|
|
820
|
+
j.lastRunResult = "budget_exceeded";
|
|
821
|
+
writeScheduleConfig(config);
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
success: false,
|
|
825
|
+
error: "Daily cost cap exceeded"
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
let fullText = "";
|
|
829
|
+
let costUsd = 0;
|
|
830
|
+
try {
|
|
831
|
+
const modelId = MODEL_MAP[job.model] || MODEL_MAP.sonnet;
|
|
832
|
+
const q = query({
|
|
833
|
+
prompt: job.prompt,
|
|
834
|
+
options: {
|
|
835
|
+
model: modelId,
|
|
836
|
+
permissionMode: "bypassPermissions",
|
|
837
|
+
allowDangerouslySkipPermissions: true,
|
|
838
|
+
cwd: os$1.homedir(),
|
|
839
|
+
maxTurns: 25
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
for await (const message of q) {
|
|
843
|
+
if (message.type === "assistant") {
|
|
844
|
+
const msgContent = message.message;
|
|
845
|
+
const text = msgContent.content.filter((block) => block.type === "text").map((block) => block.text || "").join("");
|
|
846
|
+
if (text) fullText = text;
|
|
847
|
+
}
|
|
848
|
+
if (message.type === "result") {
|
|
849
|
+
const result = message;
|
|
850
|
+
if (result.result) fullText = result.result;
|
|
851
|
+
if (result.cost_usd) costUsd = result.cost_usd;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (costUsd === 0) costUsd = Math.max(.01, fullText.length / 1e3 * .003);
|
|
855
|
+
recordCost(jobId, costUsd);
|
|
856
|
+
await deliverResult(job, fullText || "Job completed with no output.");
|
|
857
|
+
const config = readScheduleConfig();
|
|
858
|
+
const j = config.jobs.find((x) => x.id === jobId);
|
|
859
|
+
if (j) {
|
|
860
|
+
j.lastRunAt = new Date().toISOString();
|
|
861
|
+
j.lastRunCostUsd = costUsd;
|
|
862
|
+
j.lastRunResult = "success";
|
|
863
|
+
writeScheduleConfig(config);
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
success: true,
|
|
867
|
+
result: fullText,
|
|
868
|
+
costUsd
|
|
869
|
+
};
|
|
870
|
+
} catch (err) {
|
|
871
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
872
|
+
recordCost(jobId, .01);
|
|
873
|
+
const config = readScheduleConfig();
|
|
874
|
+
const j = config.jobs.find((x) => x.id === jobId);
|
|
875
|
+
if (j) {
|
|
876
|
+
j.lastRunAt = new Date().toISOString();
|
|
877
|
+
j.lastRunResult = "error";
|
|
878
|
+
writeScheduleConfig(config);
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
success: false,
|
|
882
|
+
error: errorMsg
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
//#endregion
|
|
888
|
+
export { accent, addJob, bold, canRunWithinBudget, dim, getJob, getTodaysCost, installSystemJob, listJobs, parseHumanSchedule, pawPulse, pawStep, readCostTracker, readScheduleConfig, readTelegramConfig, recordCost, removeJob, removeSystemJob, runJob, showBanner, showMini, showPuppyDisclaimer, startTelegramBot, subtle, telegramConfigExists, telegramQuestionnaire, toggleJob, writeScheduleConfig, writeTelegramConfig };
|