pawmode 1.5.1 → 1.7.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.
@@ -0,0 +1,3 @@
1
+ import { addPermissions, getPermissionRule, readSettings, removePermissions, writeSettings } from "./permissions-BHOAvP8i.js";
2
+
3
+ export { writeSettings };
@@ -1,4 +1,3 @@
1
- import { addJob, canRunWithinBudget, getJob, getTodaysCost, installSystemJob, listJobs, parseHumanSchedule, readCostTracker, readScheduleConfig, recordCost, removeJob, removeSystemJob, runJob, toggleJob, writeScheduleConfig } from "./scheduler-DAmd0GzB.js";
2
- import "./skills-CMqq9k1-.js";
1
+ import { addJob, canRunWithinBudget, getJob, getTodaysCost, installSystemJob, listJobs, parseHumanSchedule, readCostTracker, readScheduleConfig, recordCost, removeJob, removeSystemJob, runJob, toggleJob, writeScheduleConfig } from "./scheduler-DOWqpB2d.js";
3
2
 
4
3
  export { writeScheduleConfig };
@@ -1,17 +1,14 @@
1
- import { listInstalledSkills } from "./skills-CMqq9k1-.js";
2
1
  import * as p from "@clack/prompts";
3
- import * as os$2 from "node:os";
4
- import * as os$1 from "node:os";
5
2
  import chalk from "chalk";
6
3
  import { execSync } from "node:child_process";
4
+ import * as os$2 from "node:os";
5
+ import * as os$1 from "node:os";
7
6
  import * as fs$2 from "node:fs";
8
7
  import * as fs$1 from "node:fs";
9
8
  import * as path$2 from "node:path";
10
9
  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
10
  import * as crypto from "node:crypto";
11
+ import { query } from "@anthropic-ai/claude-agent-sdk";
15
12
 
16
13
  //#region src/core/branding.ts
17
14
  const accent = chalk.hex("#b4783c");
@@ -173,23 +170,48 @@ function showPuppyDisclaimer() {
173
170
 
174
171
  //#endregion
175
172
  //#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");
173
+ const PLUGIN_SPEC = "telegram@claude-plugins-official";
174
+ const CHANNEL_DIR = path$2.join(os$2.homedir(), ".claude", "channels", "telegram");
175
+ const ENV_FILE = path$2.join(CHANNEL_DIR, ".env");
176
+ const ACCESS_FILE = path$2.join(CHANNEL_DIR, "access.json");
178
177
  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);
178
+ fs$2.mkdirSync(CHANNEL_DIR, {
179
+ recursive: true,
180
+ mode: 448
181
+ });
182
+ fs$2.writeFileSync(ENV_FILE, `TELEGRAM_BOT_TOKEN=${config.botToken}\n`, { mode: 384 });
183
+ const access = {
184
+ dmPolicy: "allowlist",
185
+ allowFrom: config.allowedUserIds,
186
+ groups: {},
187
+ pending: {},
188
+ mentionPatterns: ["paw"]
189
+ };
190
+ fs$2.writeFileSync(ACCESS_FILE, JSON.stringify(access, null, 2), { mode: 384 });
182
191
  }
183
192
  function readTelegramConfig() {
184
193
  try {
185
- const raw = fs$2.readFileSync(CONFIG_PATH, "utf-8");
186
- return JSON.parse(raw);
194
+ const env = fs$2.readFileSync(ENV_FILE, "utf-8");
195
+ const token = env.match(/TELEGRAM_BOT_TOKEN=(.+)/)?.[1]?.trim();
196
+ if (!token) return null;
197
+ let allowedUserIds = [];
198
+ try {
199
+ const access = JSON.parse(fs$2.readFileSync(ACCESS_FILE, "utf-8"));
200
+ if (Array.isArray(access.allowFrom)) allowedUserIds = access.allowFrom.map(String);
201
+ } catch {}
202
+ return {
203
+ botToken: token,
204
+ allowedUserIds,
205
+ workspaceDir: os$2.homedir(),
206
+ model: "sonnet",
207
+ skills: []
208
+ };
187
209
  } catch {
188
210
  return null;
189
211
  }
190
212
  }
191
213
  function telegramConfigExists() {
192
- return fs$2.existsSync(CONFIG_PATH);
214
+ return fs$2.existsSync(ENV_FILE);
193
215
  }
194
216
  async function telegramQuestionnaire() {
195
217
  p.log.info(dim("Let's set up your Telegram bot! You'll need:"));
@@ -224,205 +246,55 @@ async function telegramQuestionnaire() {
224
246
  skills: []
225
247
  };
226
248
  }
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
- ];
249
+ function telegramPluginInstalled() {
276
250
  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);
251
+ const out = execSync("claude plugin list", {
252
+ encoding: "utf-8",
253
+ stdio: [
254
+ "ignore",
255
+ "pipe",
256
+ "ignore"
257
+ ]
330
258
  });
259
+ if (out.includes("telegram@claude-plugins-official")) return true;
260
+ } catch {}
261
+ return fs$2.existsSync(path$2.join(os$2.homedir(), ".claude", "plugins", "cache", "claude-plugins-official", "telegram"));
262
+ }
263
+ /** Install the official Telegram plugin via the Claude Code CLI. */
264
+ function installTelegramPlugin() {
265
+ try {
266
+ execSync(`claude plugin install ${PLUGIN_SPEC}`, { stdio: "ignore" });
267
+ return telegramPluginInstalled();
268
+ } catch {
269
+ return false;
331
270
  }
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;
271
+ }
272
+ /** Enable the plugin (idempotent; ignored if already enabled). */
273
+ function enableTelegramPlugin() {
370
274
  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);
275
+ execSync(`claude plugin enable ${PLUGIN_SPEC}`, { stdio: "ignore" });
276
+ } catch {}
277
+ }
278
+ /**
279
+ * Ensure the official plugin is installed, configured, and enabled.
280
+ * Returns a human-readable status for the wizard/CLI to print.
281
+ */
282
+ function ensureTelegramReady() {
283
+ if (!telegramConfigExists()) return {
284
+ ok: false,
285
+ message: "No Telegram config yet — run `openpaw telegram setup`."
286
+ };
287
+ if (!telegramPluginInstalled()) {
288
+ if (!installTelegramPlugin()) return {
289
+ ok: false,
290
+ message: `Couldn't auto-install the plugin. Install it once with:\n claude plugin install ${PLUGIN_SPEC}`
291
+ };
425
292
  }
293
+ enableTelegramPlugin();
294
+ return {
295
+ ok: true,
296
+ message: "Telegram bridge ready — it runs inside Claude Code."
297
+ };
426
298
  }
427
299
 
428
300
  //#endregion
@@ -885,4 +757,4 @@ async function runJob(jobId) {
885
757
  }
886
758
 
887
759
  //#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 };
760
+ export { accent, addJob, bold, canRunWithinBudget, dim, ensureTelegramReady, getJob, getTodaysCost, installSystemJob, listJobs, parseHumanSchedule, pawPulse, pawStep, readCostTracker, readScheduleConfig, readTelegramConfig, recordCost, removeJob, removeSystemJob, runJob, showBanner, showMini, showPuppyDisclaimer, subtle, telegramConfigExists, telegramQuestionnaire, toggleJob, writeScheduleConfig, writeTelegramConfig };
@@ -1,3 +1,3 @@
1
- import { getDefaultSkillsDir, getInstalledSkillPath, getSkillTemplatePath, installSkill, isSkillInstalled, listInstalledSkills, removeSkill } from "./skills-CMqq9k1-.js";
1
+ import { getDefaultSkillsDir, getInstalledSkillPath, getSkillTemplatePath, installSkill, isSkillInstalled, listInstalledSkills, removeSkill } from "./skills-CJ_pyPlv.js";
2
2
 
3
3
  export { installSkill };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pawmode",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Open-source Personal Assistant Wizard for Claude Code. Turn Claude Code into your PA with CLI tools and skills.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,11 +43,9 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/claude-agent-sdk": "^0.2.63",
45
45
  "@clack/prompts": "^0.10.0",
46
- "@grammyjs/hydrate": "^1.6.0",
47
46
  "chalk": "^5.4.1",
48
47
  "commander": "^14.0.0",
49
- "gradient-string": "^3.0.0",
50
- "grammy": "^1.40.1"
48
+ "gradient-string": "^3.0.0"
51
49
  },
52
50
  "devDependencies": {
53
51
  "@biomejs/biome": "^1.9.0",