openclaw-telegram-manager 1.3.0 → 1.3.2

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.
Files changed (44) hide show
  1. package/dist/lib/include-generator.d.ts +1 -1
  2. package/dist/lib/include-generator.d.ts.map +1 -1
  3. package/dist/lib/include-generator.js +33 -2
  4. package/dist/lib/include-generator.js.map +1 -1
  5. package/dist/plugin.js +29 -2
  6. package/dist/setup.js +33 -15
  7. package/dist/setup.js.map +1 -1
  8. package/package.json +2 -3
  9. package/src/commands/archive.ts +0 -89
  10. package/src/commands/doctor-all.ts +0 -243
  11. package/src/commands/doctor.ts +0 -100
  12. package/src/commands/help.ts +0 -11
  13. package/src/commands/init.ts +0 -376
  14. package/src/commands/list.ts +0 -28
  15. package/src/commands/rename.ts +0 -140
  16. package/src/commands/snooze.ts +0 -69
  17. package/src/commands/status.ts +0 -59
  18. package/src/commands/sync.ts +0 -46
  19. package/src/commands/upgrade.ts +0 -64
  20. package/src/index.ts +0 -91
  21. package/src/lib/audit.ts +0 -44
  22. package/src/lib/auth.ts +0 -96
  23. package/src/lib/capsule.ts +0 -206
  24. package/src/lib/config-restart.ts +0 -167
  25. package/src/lib/doctor-checks.ts +0 -639
  26. package/src/lib/include-generator.ts +0 -174
  27. package/src/lib/registry.ts +0 -197
  28. package/src/lib/security.ts +0 -174
  29. package/src/lib/telegram.ts +0 -311
  30. package/src/lib/types.ts +0 -172
  31. package/src/setup.ts +0 -475
  32. package/src/templates/base/COMMANDS.md +0 -3
  33. package/src/templates/base/CRON.md +0 -3
  34. package/src/templates/base/LINKS.md +0 -3
  35. package/src/templates/base/NOTES.md +0 -3
  36. package/src/templates/base/README.md +0 -3
  37. package/src/templates/base/TODO.md +0 -11
  38. package/src/templates/overlays/coding/ARCHITECTURE.md +0 -3
  39. package/src/templates/overlays/coding/DEPLOY.md +0 -3
  40. package/src/templates/overlays/marketing/CAMPAIGNS.md +0 -3
  41. package/src/templates/overlays/marketing/METRICS.md +0 -3
  42. package/src/templates/overlays/research/FINDINGS.md +0 -3
  43. package/src/templates/overlays/research/SOURCES.md +0 -3
  44. package/src/tool.ts +0 -282
@@ -1,46 +0,0 @@
1
- import { readRegistry } from '../lib/registry.js';
2
- import { checkAuthorization } from '../lib/auth.js';
3
- import { htmlEscape } from '../lib/security.js';
4
- import { generateInclude } from '../lib/include-generator.js';
5
- import { triggerRestart } from '../lib/config-restart.js';
6
- import type { CommandContext, CommandResult } from './help.js';
7
-
8
- export async function handleSync(ctx: CommandContext): Promise<CommandResult> {
9
- const { workspaceDir, configDir, userId, rpc, logger } = ctx;
10
-
11
- if (!userId) {
12
- return { text: 'Missing context: userId not available.' };
13
- }
14
-
15
- const registry = readRegistry(workspaceDir);
16
-
17
- // Auth check (admin tier)
18
- const auth = checkAuthorization(userId, 'sync', registry);
19
- if (!auth.authorized) {
20
- return { text: auth.message ?? 'Not authorized.' };
21
- }
22
-
23
- try {
24
- generateInclude(workspaceDir, registry, configDir);
25
- } catch (err) {
26
- const msg = err instanceof Error ? err.message : String(err);
27
- return {
28
- text: `Sync failed: ${htmlEscape(msg)}`,
29
- parseMode: 'HTML',
30
- };
31
- }
32
-
33
- const restartResult = await triggerRestart(rpc, logger);
34
-
35
- const topicCount = Object.keys(registry.topics).length;
36
- let text = `Include regenerated from ${topicCount} topic(s). Config synced.`;
37
-
38
- if (!restartResult.success && restartResult.fallbackMessage) {
39
- text += '\n' + restartResult.fallbackMessage;
40
- }
41
-
42
- return {
43
- text,
44
- parseMode: 'HTML',
45
- };
46
- }
@@ -1,64 +0,0 @@
1
- import * as path from 'node:path';
2
- import { readRegistry, withRegistry } from '../lib/registry.js';
3
- import { checkAuthorization } from '../lib/auth.js';
4
- import { topicKey, CAPSULE_VERSION } from '../lib/types.js';
5
- import { htmlEscape } from '../lib/security.js';
6
- import { upgradeCapsule } from '../lib/capsule.js';
7
- import type { CommandContext, CommandResult } from './help.js';
8
-
9
- export async function handleUpgrade(ctx: CommandContext): Promise<CommandResult> {
10
- const { workspaceDir, userId, groupId, threadId } = ctx;
11
-
12
- if (!userId || !groupId || !threadId) {
13
- return { text: 'Missing context: userId, groupId, or threadId not available.' };
14
- }
15
-
16
- const registry = readRegistry(workspaceDir);
17
-
18
- // Auth check (user tier)
19
- const auth = checkAuthorization(userId, 'upgrade', registry);
20
- if (!auth.authorized) {
21
- return { text: auth.message ?? 'Not authorized.' };
22
- }
23
-
24
- const key = topicKey(groupId, threadId);
25
- const entry = registry.topics[key];
26
-
27
- if (!entry) {
28
- return { text: 'This topic is not registered. Run /topic init first.' };
29
- }
30
-
31
- if (entry.capsuleVersion >= CAPSULE_VERSION) {
32
- return {
33
- text: `Topic <code>${htmlEscape(entry.slug)}</code> is already at capsule version ${CAPSULE_VERSION}. No upgrade needed.`,
34
- parseMode: 'HTML',
35
- };
36
- }
37
-
38
- const projectsBase = path.join(workspaceDir, 'projects');
39
- const result = upgradeCapsule(projectsBase, entry.slug, entry.type, entry.capsuleVersion);
40
-
41
- if (!result.upgraded) {
42
- return {
43
- text: `No upgrade needed for <code>${htmlEscape(entry.slug)}</code>.`,
44
- parseMode: 'HTML',
45
- };
46
- }
47
-
48
- // Update capsule version in registry
49
- await withRegistry(workspaceDir, (data) => {
50
- const topic = data.topics[key];
51
- if (topic) {
52
- topic.capsuleVersion = result.newVersion;
53
- }
54
- });
55
-
56
- const addedList = result.addedFiles.length > 0
57
- ? `\nAdded files: ${result.addedFiles.map((f) => htmlEscape(f)).join(', ')}`
58
- : '\nNo new files added.';
59
-
60
- return {
61
- text: `Topic <code>${htmlEscape(entry.slug)}</code> upgraded from v${entry.capsuleVersion} to v${result.newVersion}.${addedList}`,
62
- parseMode: 'HTML',
63
- };
64
- }
package/src/index.ts DELETED
@@ -1,91 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import { Type } from '@sinclair/typebox';
4
- import { createTopicManagerTool } from './tool.js';
5
-
6
- /**
7
- * Resolve configDir from the plugin's own file path or well-known locations.
8
- * Plugin is installed at {configDir}/extensions/openclaw-telegram-manager/dist/plugin.js
9
- * (or .../src/index.ts during development).
10
- */
11
- function resolveConfigDir(): string | undefined {
12
- // Try deriving from this file's location by finding the "extensions" segment
13
- const thisFile = new URL(import.meta.url).pathname;
14
- const parts = thisFile.split(path.sep);
15
- const extIndex = parts.lastIndexOf('extensions');
16
- if (extIndex > 0) {
17
- const candidate = parts.slice(0, extIndex).join(path.sep);
18
- if (
19
- fs.existsSync(path.join(candidate, 'openclaw.json')) ||
20
- fs.existsSync(path.join(candidate, 'extensions'))
21
- ) {
22
- return candidate;
23
- }
24
- }
25
-
26
- // Fall back to env / home directory
27
- const envDir = process.env['OPENCLAW_CONFIG_DIR'];
28
- if (envDir && fs.existsSync(envDir)) return path.resolve(envDir);
29
-
30
- const homeDir = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
31
- const defaultDir = path.join(homeDir, '.openclaw');
32
- if (fs.existsSync(defaultDir)) return defaultDir;
33
-
34
- return undefined;
35
- }
36
-
37
- export default function register(api: {
38
- logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void };
39
- configDir?: string;
40
- workspaceDir?: string;
41
- rpc?: { call(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> } | null;
42
- pluginConfig?: { configDir?: string; workspaceDir?: string };
43
- registerTool(def: {
44
- name: string;
45
- description: string;
46
- parameters: unknown;
47
- execute(id: string, params: { command: string }, context?: Record<string, unknown>): Promise<unknown>;
48
- }): void;
49
- }): void {
50
- const resolvedConfigDir = resolveConfigDir();
51
- const configDir = api.configDir ?? api.pluginConfig?.configDir ?? resolvedConfigDir;
52
- const workspaceDir =
53
- api.workspaceDir ??
54
- api.pluginConfig?.workspaceDir ??
55
- (resolvedConfigDir ? path.join(resolvedConfigDir, 'workspace') : undefined);
56
-
57
- if (!configDir || !workspaceDir) {
58
- api.logger.error(
59
- 'telegram-manager: configDir or workspaceDir not available. Plugin cannot initialize.',
60
- );
61
- return;
62
- }
63
-
64
- const tool = createTopicManagerTool({
65
- logger: api.logger,
66
- configDir,
67
- workspaceDir,
68
- rpc: api.rpc,
69
- });
70
-
71
- api.registerTool({
72
- name: 'topic_manager',
73
- description:
74
- 'Manage Telegram forum topics as deterministic workcells. Sub-commands: init, doctor, list, status, sync, rename, upgrade, snooze, archive, unarchive, help.',
75
- parameters: Type.Object({
76
- command: Type.String({
77
- description:
78
- "Sub-command and arguments (e.g., 'init', 'doctor --all', 'rename new-slug')",
79
- }),
80
- }),
81
- async execute(
82
- id: string,
83
- params: { command: string },
84
- context?: Record<string, unknown>,
85
- ) {
86
- return tool.execute(id, params, context);
87
- },
88
- });
89
-
90
- api.logger.info('telegram-manager plugin loaded');
91
- }
package/src/lib/audit.ts DELETED
@@ -1,44 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import type { AuditEntry } from './types.js';
4
-
5
- const AUDIT_FILENAME = 'audit.jsonl';
6
- const FILE_MODE = 0o600;
7
-
8
- /**
9
- * Append an audit entry to the audit.jsonl file.
10
- * Creates the file if it does not exist. Each entry is a single JSON line.
11
- * File permissions are set to 0600.
12
- */
13
- export function appendAudit(workspaceDir: string, entry: AuditEntry): void {
14
- const auditPath = path.join(workspaceDir, 'projects', AUDIT_FILENAME);
15
- const line = JSON.stringify(entry) + '\n';
16
-
17
- const fd = fs.openSync(auditPath, 'a', FILE_MODE);
18
- try {
19
- fs.writeSync(fd, line);
20
- } finally {
21
- fs.closeSync(fd);
22
- }
23
-
24
- // Ensure permissions are correct even if file already existed
25
- fs.chmodSync(auditPath, FILE_MODE);
26
- }
27
-
28
- /**
29
- * Build an AuditEntry with the current timestamp.
30
- */
31
- export function buildAuditEntry(
32
- userId: string,
33
- cmd: string,
34
- slug: string,
35
- detail: string,
36
- ): AuditEntry {
37
- return {
38
- ts: new Date().toISOString(),
39
- userId,
40
- cmd,
41
- slug,
42
- detail,
43
- };
44
- }
package/src/lib/auth.ts DELETED
@@ -1,96 +0,0 @@
1
- import type { Registry } from './types.js';
2
-
3
- // ── Authorization tiers ────────────────────────────────────────────────
4
-
5
- export const AuthTier = {
6
- User: 'User',
7
- Admin: 'Admin',
8
- } as const;
9
-
10
- export type AuthTier = (typeof AuthTier)[keyof typeof AuthTier];
11
-
12
- // ── Command-to-tier mapping ────────────────────────────────────────────
13
-
14
- const ADMIN_COMMANDS = new Set([
15
- 'doctor --all',
16
- 'doctor-all',
17
- 'list',
18
- 'sync',
19
- 'rename',
20
- 'archive',
21
- 'unarchive',
22
- ]);
23
-
24
- const USER_COMMANDS = new Set([
25
- 'init',
26
- 'doctor',
27
- 'status',
28
- 'help',
29
- 'upgrade',
30
- 'snooze',
31
- ]);
32
-
33
- /**
34
- * Get the authorization tier for a command.
35
- * Defaults to Admin for unknown commands.
36
- */
37
- export function getCommandTier(command: string): AuthTier {
38
- const normalized = command.toLowerCase().trim();
39
- if (USER_COMMANDS.has(normalized)) return AuthTier.User;
40
- if (ADMIN_COMMANDS.has(normalized)) return AuthTier.Admin;
41
- // Unknown commands default to admin tier (principle of least privilege)
42
- return AuthTier.Admin;
43
- }
44
-
45
- // ── Authorization check ────────────────────────────────────────────────
46
-
47
- export interface AuthResult {
48
- authorized: boolean;
49
- message?: string;
50
- }
51
-
52
- /**
53
- * Check if a user is authorized to run a command.
54
- *
55
- * Logic:
56
- * - Admin-tier: user must be in topicManagerAdmins
57
- * - User-tier: user must be in topicAllowFrom OR topicManagerAdmins
58
- * - Special case for init: if topicManagerAdmins is empty (first-time setup),
59
- * allow anyone — the first user to init becomes the default admin.
60
- */
61
- export function checkAuthorization(
62
- userId: string,
63
- command: string,
64
- registry: Registry,
65
- topicAllowFrom?: string[],
66
- ): AuthResult {
67
- const tier = getCommandTier(command);
68
- const admins = registry.topicManagerAdmins;
69
-
70
- // First-user bootstrap: if no admins set yet, allow anyone for init
71
- if (command === 'init' && admins.length === 0) {
72
- return { authorized: true };
73
- }
74
-
75
- const isAdmin = admins.includes(userId);
76
-
77
- if (tier === AuthTier.Admin) {
78
- if (isAdmin) return { authorized: true };
79
- return {
80
- authorized: false,
81
- message: 'Not authorized. Ask a telegram-manager admin to run this command.',
82
- };
83
- }
84
-
85
- // User tier: allowed if admin or in topicAllowFrom
86
- if (isAdmin) return { authorized: true };
87
-
88
- if (topicAllowFrom && topicAllowFrom.includes(userId)) {
89
- return { authorized: true };
90
- }
91
-
92
- return {
93
- authorized: false,
94
- message: 'Not authorized. Ask a telegram-manager admin to run this command.',
95
- };
96
- }
@@ -1,206 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import { BASE_FILES, OVERLAY_FILES, CAPSULE_VERSION } from './types.js';
4
- import type { TopicType } from './types.js';
5
- import { jailCheck, rejectSymlink } from './security.js';
6
-
7
- // ── Template content (embedded string constants) ───────────────────────
8
- // These are the source-of-truth defaults, matching src/templates/.
9
-
10
- const BASE_TEMPLATES: Record<string, (slug: string) => string> = {
11
- 'README.md': (slug) =>
12
- `# ${slug}\n\n_Describe what this topic is about._\n`,
13
-
14
- 'STATUS.md': (slug) =>
15
- `# Status: ${slug}\n\n## Last done (UTC)\n\n${new Date().toISOString()}\n\n_No work recorded yet._\n\n## Next 3 actions\n\n1. [T-1] _Define first task in TODO.md_\n2. [T-2] _Define second task in TODO.md_\n3. [T-3] _Define third task in TODO.md_\n`,
16
-
17
- 'TODO.md': (slug) =>
18
- `# TODO: ${slug}\n\n## Backlog\n\n- [T-1] _First task placeholder — replace with actual task_\n- [T-2] _Second task placeholder — replace with actual task_\n- [T-3] _Third task placeholder — replace with actual task_\n\n## Completed\n\n_None yet._\n`,
19
-
20
- 'COMMANDS.md': (slug) =>
21
- `# Commands: ${slug}\n\n_Build, deploy, test, and other commands for this topic. Kept here so they're not lost on reset._\n`,
22
-
23
- 'LINKS.md': (slug) =>
24
- `# Links: ${slug}\n\n_URLs, paths, and service endpoints for this topic._\n`,
25
-
26
- 'CRON.md': (slug) =>
27
- `# Cron: ${slug}\n\n_Cron job IDs and schedules for this topic._\n`,
28
-
29
- 'NOTES.md': (slug) =>
30
- `# Notes: ${slug}\n\n_Anything worth remembering about this topic._\n`,
31
- };
32
-
33
- const OVERLAY_TEMPLATES: Record<string, (slug: string) => string> = {
34
- 'ARCHITECTURE.md': (slug) =>
35
- `# Architecture: ${slug}\n\n_Components, data flow, dependencies, and design decisions._\n`,
36
-
37
- 'DEPLOY.md': (slug) =>
38
- `# Deployment: ${slug}\n\n_Environments, deployment steps, rollback procedures, and infra details._\n`,
39
-
40
- 'SOURCES.md': (slug) =>
41
- `# Sources: ${slug}\n\n_Papers, articles, datasets, APIs, and other reference material._\n`,
42
-
43
- 'FINDINGS.md': (slug) =>
44
- `# Findings: ${slug}\n\n_Conclusions, insights, data summaries, and recommendations._\n`,
45
-
46
- 'CAMPAIGNS.md': (slug) =>
47
- `# Campaigns: ${slug}\n\n_Active campaigns, target audiences, channels, timelines, and budgets._\n`,
48
-
49
- 'METRICS.md': (slug) =>
50
- `# Metrics: ${slug}\n\n_KPIs, conversion rates, engagement stats, and performance data._\n`,
51
- };
52
-
53
- // ── File permissions ───────────────────────────────────────────────────
54
-
55
- const CAPSULE_FILE_MODE = 0o640;
56
-
57
- // ── Scaffold ───────────────────────────────────────────────────────────
58
-
59
- /**
60
- * Scaffold a new capsule directory with base kit + type overlays.
61
- * Uses fs.mkdirSync with exclusive flag as the atomic reservation mechanism.
62
- * Throws if the directory already exists (collision).
63
- */
64
- export function scaffoldCapsule(
65
- projectsBase: string,
66
- slug: string,
67
- type: TopicType,
68
- ): void {
69
- const capsuleDir = path.join(projectsBase, slug);
70
-
71
- // Path jail check
72
- if (!jailCheck(projectsBase, slug)) {
73
- throw new Error(`Path escapes projects directory: ${slug}`);
74
- }
75
-
76
- // Symlink check on parent
77
- if (rejectSymlink(projectsBase)) {
78
- throw new Error(`Projects base is a symlink: ${projectsBase}`);
79
- }
80
-
81
- // Atomic directory creation (exclusive)
82
- fs.mkdirSync(capsuleDir, { recursive: false });
83
-
84
- // Write base files
85
- for (const file of BASE_FILES) {
86
- const templateFn = BASE_TEMPLATES[file];
87
- if (templateFn) {
88
- const filePath = path.join(capsuleDir, file);
89
- fs.writeFileSync(filePath, templateFn(slug), { mode: CAPSULE_FILE_MODE });
90
- }
91
- }
92
-
93
- // Write overlay files for the type
94
- const overlays = OVERLAY_FILES[type];
95
- for (const file of overlays) {
96
- const templateFn = OVERLAY_TEMPLATES[file];
97
- if (templateFn) {
98
- const filePath = path.join(capsuleDir, file);
99
- fs.writeFileSync(filePath, templateFn(slug), { mode: CAPSULE_FILE_MODE });
100
- }
101
- }
102
- }
103
-
104
- // ── Upgrade ────────────────────────────────────────────────────────────
105
-
106
- export interface UpgradeResult {
107
- upgraded: boolean;
108
- newVersion: number;
109
- addedFiles: string[];
110
- }
111
-
112
- /**
113
- * Upgrade an existing capsule to the latest template version.
114
- * Adds missing files without overwriting existing ones.
115
- */
116
- export function upgradeCapsule(
117
- projectsBase: string,
118
- slug: string,
119
- type: TopicType,
120
- currentVersion: number,
121
- ): UpgradeResult {
122
- if (currentVersion >= CAPSULE_VERSION) {
123
- return { upgraded: false, newVersion: currentVersion, addedFiles: [] };
124
- }
125
-
126
- const capsuleDir = path.join(projectsBase, slug);
127
-
128
- if (!jailCheck(projectsBase, slug)) {
129
- throw new Error(`Path escapes projects directory: ${slug}`);
130
- }
131
-
132
- if (rejectSymlink(capsuleDir)) {
133
- throw new Error(`Capsule directory is a symlink: ${capsuleDir}`);
134
- }
135
-
136
- const addedFiles: string[] = [];
137
-
138
- // Add missing base files
139
- for (const file of BASE_FILES) {
140
- const filePath = path.join(capsuleDir, file);
141
- if (!fs.existsSync(filePath)) {
142
- const templateFn = BASE_TEMPLATES[file];
143
- if (templateFn) {
144
- fs.writeFileSync(filePath, templateFn(slug), { mode: CAPSULE_FILE_MODE });
145
- addedFiles.push(file);
146
- }
147
- }
148
- }
149
-
150
- // Add missing overlay files
151
- const overlays = OVERLAY_FILES[type];
152
- for (const file of overlays) {
153
- const filePath = path.join(capsuleDir, file);
154
- if (!fs.existsSync(filePath)) {
155
- const templateFn = OVERLAY_TEMPLATES[file];
156
- if (templateFn) {
157
- fs.writeFileSync(filePath, templateFn(slug), { mode: CAPSULE_FILE_MODE });
158
- addedFiles.push(file);
159
- }
160
- }
161
- }
162
-
163
- return {
164
- upgraded: true,
165
- newVersion: CAPSULE_VERSION,
166
- addedFiles,
167
- };
168
- }
169
-
170
- // ── Validate ───────────────────────────────────────────────────────────
171
-
172
- export interface CapsuleValidation {
173
- missing: string[];
174
- present: string[];
175
- }
176
-
177
- /**
178
- * Validate that a capsule has all expected files.
179
- * Returns lists of present and missing files.
180
- */
181
- export function validateCapsule(
182
- projectsBase: string,
183
- slug: string,
184
- type: TopicType,
185
- ): CapsuleValidation {
186
- const capsuleDir = path.join(projectsBase, slug);
187
-
188
- if (!jailCheck(projectsBase, slug)) {
189
- throw new Error(`Path escapes projects directory: ${slug}`);
190
- }
191
-
192
- const expectedFiles = [...BASE_FILES, ...OVERLAY_FILES[type]];
193
- const missing: string[] = [];
194
- const present: string[] = [];
195
-
196
- for (const file of expectedFiles) {
197
- const filePath = path.join(capsuleDir, file);
198
- if (fs.existsSync(filePath)) {
199
- present.push(file);
200
- } else {
201
- missing.push(file);
202
- }
203
- }
204
-
205
- return { missing, present };
206
- }