Compare commits
22 Commits
19a04e9b0d
...
v0.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e22c0fdeb | |||
| 1f4d54e474 | |||
| b7a39b45d7 | |||
| 1bfdc91f90 | |||
| 58a90ac9d7 | |||
| 684dbdc6a4 | |||
| e92de12cf9 | |||
| 1f784a6a04 | |||
| ab37c2e69f | |||
| c8f3e0db44 | |||
| 02772a3910 | |||
| 85a25fd995 | |||
| 20f302367c | |||
| 54c6bfded0 | |||
| ca5472bc31 | |||
| 55b5a31c3c | |||
| 01e9891243 | |||
| 446a424c1f | |||
| 02a0d515d9 | |||
| 2bf3816efc | |||
| 96902bab44 | |||
| 280c5351e2 |
14
.env.example
14
.env.example
@@ -18,3 +18,17 @@ BETTER_AUTH_URL=http://localhost:4000
|
|||||||
|
|
||||||
# Gateway
|
# Gateway
|
||||||
GATEWAY_PORT=4000
|
GATEWAY_PORT=4000
|
||||||
|
|
||||||
|
# Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable)
|
||||||
|
# DISCORD_BOT_TOKEN=
|
||||||
|
# DISCORD_GUILD_ID=
|
||||||
|
# DISCORD_GATEWAY_URL=http://localhost:4000
|
||||||
|
|
||||||
|
# Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable)
|
||||||
|
# TELEGRAM_BOT_TOKEN=
|
||||||
|
# TELEGRAM_GATEWAY_URL=http://localhost:4000
|
||||||
|
|
||||||
|
# Authentik SSO (optional — set AUTHENTIK_CLIENT_ID to enable)
|
||||||
|
# AUTHENTIK_ISSUER=https://auth.example.com
|
||||||
|
# AUTHENTIK_CLIENT_ID=
|
||||||
|
# AUTHENTIK_CLIENT_SECRET=
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
variables:
|
variables:
|
||||||
- &node_image 'node:22-alpine'
|
- &node_image 'node:22-alpine'
|
||||||
- &install_deps |
|
- &enable_pnpm 'corepack enable'
|
||||||
corepack enable
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
when:
|
when:
|
||||||
- event: [push, pull_request, manual]
|
- event: [push, pull_request, manual]
|
||||||
|
|
||||||
|
# Steps run sequentially to avoid OOM on the CI runner.
|
||||||
|
# node_modules is installed once by the install step and shared across
|
||||||
|
# all subsequent steps via Woodpecker's shared workspace volume.
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
install:
|
install:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- *install_deps
|
- corepack enable
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- *install_deps
|
- *enable_pnpm
|
||||||
- pnpm typecheck
|
- pnpm typecheck
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- install
|
||||||
@@ -24,34 +27,31 @@ steps:
|
|||||||
lint:
|
lint:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- *install_deps
|
- *enable_pnpm
|
||||||
- pnpm lint
|
- pnpm lint
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- typecheck
|
||||||
|
|
||||||
format:
|
format:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- *install_deps
|
- *enable_pnpm
|
||||||
- pnpm format:check
|
- pnpm format:check
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- lint
|
||||||
|
|
||||||
test:
|
test:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- *install_deps
|
- *enable_pnpm
|
||||||
- pnpm test
|
- pnpm test
|
||||||
depends_on:
|
depends_on:
|
||||||
- install
|
- format
|
||||||
|
|
||||||
build:
|
build:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- *install_deps
|
- *enable_pnpm
|
||||||
- pnpm build
|
- pnpm build
|
||||||
depends_on:
|
depends_on:
|
||||||
- typecheck
|
|
||||||
- lint
|
|
||||||
- format
|
|
||||||
- test
|
- test
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"@mosaic/brain": "workspace:^",
|
"@mosaic/brain": "workspace:^",
|
||||||
"@mosaic/coord": "workspace:^",
|
"@mosaic/coord": "workspace:^",
|
||||||
"@mosaic/db": "workspace:^",
|
"@mosaic/db": "workspace:^",
|
||||||
|
"@mosaic/discord-plugin": "workspace:^",
|
||||||
|
"@mosaic/telegram-plugin": "workspace:^",
|
||||||
"@mosaic/log": "workspace:^",
|
"@mosaic/log": "workspace:^",
|
||||||
"@mosaic/memory": "workspace:^",
|
"@mosaic/memory": "workspace:^",
|
||||||
"@mosaic/types": "workspace:^",
|
"@mosaic/types": "workspace:^",
|
||||||
|
|||||||
@@ -86,4 +86,52 @@ describe('Resource ownership checks', () => {
|
|||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forbids creating a task with an unowned project', async () => {
|
||||||
|
const brain = createBrain();
|
||||||
|
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||||
|
const controller = new TasksController(brain as never);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.create(
|
||||||
|
{
|
||||||
|
title: 'Task',
|
||||||
|
projectId: 'project-1',
|
||||||
|
},
|
||||||
|
{ id: 'user-1' },
|
||||||
|
),
|
||||||
|
).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forbids listing tasks for an unowned project', async () => {
|
||||||
|
const brain = createBrain();
|
||||||
|
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||||
|
const controller = new TasksController(brain as never);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.list({ id: 'user-1' }, 'project-1', undefined, undefined),
|
||||||
|
).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lists only tasks for the current user owned projects when no filter is provided', async () => {
|
||||||
|
const brain = createBrain();
|
||||||
|
brain.projects.findAll.mockResolvedValue([
|
||||||
|
{ id: 'project-1', ownerId: 'user-1' },
|
||||||
|
{ id: 'project-2', ownerId: 'user-2' },
|
||||||
|
]);
|
||||||
|
brain.missions.findAll.mockResolvedValue([{ id: 'mission-1', projectId: 'project-1' }]);
|
||||||
|
brain.tasks.findAll.mockResolvedValue([
|
||||||
|
{ id: 'task-1', projectId: 'project-1' },
|
||||||
|
{ id: 'task-2', missionId: 'mission-1' },
|
||||||
|
{ id: 'task-3', projectId: 'project-2' },
|
||||||
|
]);
|
||||||
|
const controller = new TasksController(brain as never);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.list({ id: 'user-1' }, undefined, undefined, undefined),
|
||||||
|
).resolves.toEqual([
|
||||||
|
{ id: 'task-1', projectId: 'project-1' },
|
||||||
|
{ id: 'task-2', missionId: 'mission-1' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { CoordModule } from './coord/coord.module.js';
|
|||||||
import { MemoryModule } from './memory/memory.module.js';
|
import { MemoryModule } from './memory/memory.module.js';
|
||||||
import { LogModule } from './log/log.module.js';
|
import { LogModule } from './log/log.module.js';
|
||||||
import { SkillsModule } from './skills/skills.module.js';
|
import { SkillsModule } from './skills/skills.module.js';
|
||||||
|
import { PluginModule } from './plugin/plugin.module.js';
|
||||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -32,6 +33,7 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|||||||
MemoryModule,
|
MemoryModule,
|
||||||
LogModule,
|
LogModule,
|
||||||
SkillsModule,
|
SkillsModule,
|
||||||
|
PluginModule,
|
||||||
],
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ async function bootstrap(): Promise<void> {
|
|||||||
throw new Error('BETTER_AUTH_SECRET is required');
|
throw new Error('BETTER_AUTH_SECRET is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env['AUTHENTIK_CLIENT_ID'] &&
|
||||||
|
(!process.env['AUTHENTIK_CLIENT_SECRET'] || !process.env['AUTHENTIK_ISSUER'])
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
'[warn] AUTHENTIK_CLIENT_ID is set but AUTHENTIK_CLIENT_SECRET or AUTHENTIK_ISSUER is missing — Authentik SSO will not work',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const logger = new Logger('Bootstrap');
|
const logger = new Logger('Bootstrap');
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
AppModule,
|
AppModule,
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export class MissionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Body() dto: CreateMissionDto) {
|
async create(@Body() dto: CreateMissionDto, @CurrentUser() user: { id: string }) {
|
||||||
|
if (dto.projectId) {
|
||||||
|
await this.getOwnedProject(dto.projectId, user.id, 'Mission');
|
||||||
|
}
|
||||||
return this.brain.missions.create({
|
return this.brain.missions.create({
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
|
|||||||
5
apps/gateway/src/plugin/plugin.interface.ts
Normal file
5
apps/gateway/src/plugin/plugin.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface IChannelPlugin {
|
||||||
|
readonly name: string;
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
110
apps/gateway/src/plugin/plugin.module.ts
Normal file
110
apps/gateway/src/plugin/plugin.module.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
Global,
|
||||||
|
Inject,
|
||||||
|
Logger,
|
||||||
|
Module,
|
||||||
|
type OnModuleDestroy,
|
||||||
|
type OnModuleInit,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DiscordPlugin } from '@mosaic/discord-plugin';
|
||||||
|
import { TelegramPlugin } from '@mosaic/telegram-plugin';
|
||||||
|
import { PluginService } from './plugin.service.js';
|
||||||
|
import type { IChannelPlugin } from './plugin.interface.js';
|
||||||
|
|
||||||
|
export const PLUGIN_REGISTRY = Symbol('PLUGIN_REGISTRY');
|
||||||
|
|
||||||
|
class DiscordChannelPluginAdapter implements IChannelPlugin {
|
||||||
|
readonly name = 'discord';
|
||||||
|
|
||||||
|
constructor(private readonly plugin: DiscordPlugin) {}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
await this.plugin.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
await this.plugin.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TelegramChannelPluginAdapter implements IChannelPlugin {
|
||||||
|
readonly name = 'telegram';
|
||||||
|
|
||||||
|
constructor(private readonly plugin: TelegramPlugin) {}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
await this.plugin.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
await this.plugin.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_GATEWAY_URL = 'http://localhost:4000';
|
||||||
|
|
||||||
|
function createPluginRegistry(): IChannelPlugin[] {
|
||||||
|
const plugins: IChannelPlugin[] = [];
|
||||||
|
const discordToken = process.env['DISCORD_BOT_TOKEN'];
|
||||||
|
const discordGuildId = process.env['DISCORD_GUILD_ID'];
|
||||||
|
const discordGatewayUrl = process.env['DISCORD_GATEWAY_URL'] ?? DEFAULT_GATEWAY_URL;
|
||||||
|
|
||||||
|
if (discordToken) {
|
||||||
|
plugins.push(
|
||||||
|
new DiscordChannelPluginAdapter(
|
||||||
|
new DiscordPlugin({
|
||||||
|
token: discordToken,
|
||||||
|
guildId: discordGuildId,
|
||||||
|
gatewayUrl: discordGatewayUrl,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const telegramToken = process.env['TELEGRAM_BOT_TOKEN'];
|
||||||
|
const telegramGatewayUrl = process.env['TELEGRAM_GATEWAY_URL'] ?? DEFAULT_GATEWAY_URL;
|
||||||
|
|
||||||
|
if (telegramToken) {
|
||||||
|
plugins.push(
|
||||||
|
new TelegramChannelPluginAdapter(
|
||||||
|
new TelegramPlugin({
|
||||||
|
token: telegramToken,
|
||||||
|
gatewayUrl: telegramGatewayUrl,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PLUGIN_REGISTRY,
|
||||||
|
useFactory: (): IChannelPlugin[] => createPluginRegistry(),
|
||||||
|
},
|
||||||
|
PluginService,
|
||||||
|
],
|
||||||
|
exports: [PluginService, PLUGIN_REGISTRY],
|
||||||
|
})
|
||||||
|
export class PluginModule implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(PluginModule.name);
|
||||||
|
|
||||||
|
constructor(@Inject(PLUGIN_REGISTRY) private readonly plugins: IChannelPlugin[]) {}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
for (const plugin of this.plugins) {
|
||||||
|
this.logger.log(`Starting plugin: ${plugin.name}`);
|
||||||
|
await plugin.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy(): Promise<void> {
|
||||||
|
for (const plugin of [...this.plugins].reverse()) {
|
||||||
|
this.logger.log(`Stopping plugin: ${plugin.name}`);
|
||||||
|
await plugin.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
apps/gateway/src/plugin/plugin.service.ts
Normal file
16
apps/gateway/src/plugin/plugin.service.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { PLUGIN_REGISTRY } from './plugin.module.js';
|
||||||
|
import type { IChannelPlugin } from './plugin.interface.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PluginService {
|
||||||
|
constructor(@Inject(PLUGIN_REGISTRY) private readonly plugins: IChannelPlugin[]) {}
|
||||||
|
|
||||||
|
getPlugins(): IChannelPlugin[] {
|
||||||
|
return this.plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugin(name: string): IChannelPlugin | undefined {
|
||||||
|
return this.plugins.find((plugin: IChannelPlugin) => plugin.name === name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,17 +28,48 @@ export class TasksController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async list(
|
async list(
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
@Query('projectId') projectId?: string,
|
@Query('projectId') projectId?: string,
|
||||||
@Query('missionId') missionId?: string,
|
@Query('missionId') missionId?: string,
|
||||||
@Query('status') status?: string,
|
@Query('status') status?: string,
|
||||||
) {
|
) {
|
||||||
if (projectId) return this.brain.tasks.findByProject(projectId);
|
if (projectId) {
|
||||||
if (missionId) return this.brain.tasks.findByMission(missionId);
|
await this.getOwnedProject(projectId, user.id, 'Task');
|
||||||
if (status)
|
return this.brain.tasks.findByProject(projectId);
|
||||||
return this.brain.tasks.findByStatus(
|
}
|
||||||
status as Parameters<typeof this.brain.tasks.findByStatus>[0],
|
if (missionId) {
|
||||||
);
|
await this.getOwnedMission(missionId, user.id, 'Task');
|
||||||
return this.brain.tasks.findAll();
|
return this.brain.tasks.findByMission(missionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [projects, missions, tasks] = await Promise.all([
|
||||||
|
this.brain.projects.findAll(),
|
||||||
|
this.brain.missions.findAll(),
|
||||||
|
status
|
||||||
|
? this.brain.tasks.findByStatus(
|
||||||
|
status as Parameters<typeof this.brain.tasks.findByStatus>[0],
|
||||||
|
)
|
||||||
|
: this.brain.tasks.findAll(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ownedProjectIds = new Set(
|
||||||
|
projects.filter((project) => project.ownerId === user.id).map((project) => project.id),
|
||||||
|
);
|
||||||
|
const ownedMissionIds = new Set(
|
||||||
|
missions
|
||||||
|
.filter(
|
||||||
|
(ownedMission) =>
|
||||||
|
typeof ownedMission.projectId === 'string' &&
|
||||||
|
ownedProjectIds.has(ownedMission.projectId),
|
||||||
|
)
|
||||||
|
.map((ownedMission) => ownedMission.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return tasks.filter(
|
||||||
|
(task) =>
|
||||||
|
(task.projectId ? ownedProjectIds.has(task.projectId) : false) ||
|
||||||
|
(task.missionId ? ownedMissionIds.has(task.missionId) : false),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@@ -47,7 +78,13 @@ export class TasksController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Body() dto: CreateTaskDto) {
|
async create(@Body() dto: CreateTaskDto, @CurrentUser() user: { id: string }) {
|
||||||
|
if (dto.projectId) {
|
||||||
|
await this.getOwnedProject(dto.projectId, user.id, 'Task');
|
||||||
|
}
|
||||||
|
if (dto.missionId) {
|
||||||
|
await this.getOwnedMission(dto.missionId, user.id, 'Task');
|
||||||
|
}
|
||||||
return this.brain.tasks.create({
|
return this.brain.tasks.create({
|
||||||
title: dto.title,
|
title: dto.title,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
"@mosaic/db": ["../../packages/db/src/index.ts"],
|
"@mosaic/db": ["../../packages/db/src/index.ts"],
|
||||||
"@mosaic/log": ["../../packages/log/src/index.ts"],
|
"@mosaic/log": ["../../packages/log/src/index.ts"],
|
||||||
"@mosaic/memory": ["../../packages/memory/src/index.ts"],
|
"@mosaic/memory": ["../../packages/memory/src/index.ts"],
|
||||||
"@mosaic/types": ["../../packages/types/src/index.ts"]
|
"@mosaic/types": ["../../packages/types/src/index.ts"],
|
||||||
|
"@mosaic/discord-plugin": ["../../plugins/discord/src/index.ts"],
|
||||||
|
"@mosaic/telegram-plugin": ["../../plugins/telegram/src/index.ts"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
**ID:** mvp-20260312
|
**ID:** mvp-20260312
|
||||||
**Statement:** Build Mosaic Stack v0.1.0 — a self-hosted, multi-user AI agent platform with web dashboard, TUI, remote control, shared memory, mission orchestration, and extensible skill/plugin architecture. All TypeScript. Pi as agent harness. Brain as knowledge layer. Queue as coordination backbone.
|
**Statement:** Build Mosaic Stack v0.1.0 — a self-hosted, multi-user AI agent platform with web dashboard, TUI, remote control, shared memory, mission orchestration, and extensible skill/plugin architecture. All TypeScript. Pi as agent harness. Brain as knowledge layer. Queue as coordination backbone.
|
||||||
**Phase:** Execution
|
**Phase:** Execution
|
||||||
**Current Milestone:** Phase 5: Remote Control (v0.0.6)
|
**Current Milestone:** Phase 6: CLI & Tools (v0.0.7)
|
||||||
**Progress:** 5 / 8 milestones
|
**Progress:** 6 / 8 milestones
|
||||||
**Status:** active
|
**Status:** active
|
||||||
**Last Updated:** 2026-03-13 UTC
|
**Last Updated:** 2026-03-14 UTC
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
| 2 | ms-159 | Phase 2: Agent Layer (v0.0.3) | done | — | — | 2026-03-13 | 2026-03-12 |
|
| 2 | ms-159 | Phase 2: Agent Layer (v0.0.3) | done | — | — | 2026-03-13 | 2026-03-12 |
|
||||||
| 3 | ms-160 | Phase 3: Web Dashboard (v0.0.4) | done | — | — | 2026-03-12 | 2026-03-13 |
|
| 3 | ms-160 | Phase 3: Web Dashboard (v0.0.4) | done | — | — | 2026-03-12 | 2026-03-13 |
|
||||||
| 4 | ms-161 | Phase 4: Memory & Intelligence (v0.0.5) | done | — | — | 2026-03-13 | 2026-03-13 |
|
| 4 | ms-161 | Phase 4: Memory & Intelligence (v0.0.5) | done | — | — | 2026-03-13 | 2026-03-13 |
|
||||||
| 5 | ms-162 | Phase 5: Remote Control (v0.0.6) | not-started | — | — | — | — |
|
| 5 | ms-162 | Phase 5: Remote Control (v0.0.6) | done | — | #99 | 2026-03-14 | 2026-03-14 |
|
||||||
| 6 | ms-163 | Phase 6: CLI & Tools (v0.0.7) | not-started | — | — | — | — |
|
| 6 | ms-163 | Phase 6: CLI & Tools (v0.0.7) | not-started | — | — | — | — |
|
||||||
| 7 | ms-164 | Phase 7: Polish & Beta (v0.1.0) | not-started | — | — | — | — |
|
| 7 | ms-164 | Phase 7: Polish & Beta (v0.1.0) | not-started | — | — | — | — |
|
||||||
|
|
||||||
@@ -68,7 +68,8 @@
|
|||||||
| 7 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-007 |
|
| 7 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-007 |
|
||||||
| 8 | claude-opus-4-6 | 2026-03-12 | — | context limit | Phase 2 complete |
|
| 8 | claude-opus-4-6 | 2026-03-12 | — | context limit | Phase 2 complete |
|
||||||
| 9 | claude-opus-4-6 | 2026-03-12 | — | context limit | P3-007 |
|
| 9 | claude-opus-4-6 | 2026-03-12 | — | context limit | P3-007 |
|
||||||
| 10 | claude-opus-4-6 | 2026-03-13 | — | active | P3-008 |
|
| 10 | claude-opus-4-6 | 2026-03-13 | — | context limit | P3-008 |
|
||||||
|
| 11 | claude-opus-4-6 | 2026-03-14 | — | active | P5-005 |
|
||||||
|
|
||||||
## Scratchpad
|
## Scratchpad
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,11 @@
|
|||||||
| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 |
|
| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 |
|
||||||
| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 |
|
| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 |
|
||||||
| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 |
|
| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 |
|
||||||
| P5-001 | not-started | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 |
|
| P5-001 | done | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 |
|
||||||
| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |
|
| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |
|
||||||
| P5-003 | not-started | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 |
|
| P5-003 | done | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 |
|
||||||
| P5-004 | not-started | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 |
|
| P5-004 | done | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 |
|
||||||
| P5-005 | not-started | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | — | #45 |
|
| P5-005 | done | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | #99 | #45 |
|
||||||
| P6-001 | not-started | Phase 6 | @mosaic/cli — unified CLI binary + subcommands | — | #46 |
|
| P6-001 | not-started | Phase 6 | @mosaic/cli — unified CLI binary + subcommands | — | #46 |
|
||||||
| P6-002 | not-started | Phase 6 | @mosaic/prdy — migrate PRD wizard from v0 | — | #47 |
|
| P6-002 | not-started | Phase 6 | @mosaic/prdy — migrate PRD wizard from v0 | — | #47 |
|
||||||
| P6-003 | not-started | Phase 6 | @mosaic/quality-rails — migrate scaffolder from v0 | — | #48 |
|
| P6-003 | not-started | Phase 6 | @mosaic/quality-rails — migrate scaffolder from v0 | — | #48 |
|
||||||
|
|||||||
40
docs/plans/authentik-sso-setup.md
Normal file
40
docs/plans/authentik-sso-setup.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Authentik SSO Setup
|
||||||
|
|
||||||
|
## Create the Authentik application
|
||||||
|
|
||||||
|
1. In Authentik, create an OAuth2/OpenID Provider.
|
||||||
|
2. Create an Application and link it to that provider.
|
||||||
|
3. Copy the generated client ID and client secret.
|
||||||
|
|
||||||
|
## Required environment variables
|
||||||
|
|
||||||
|
Set these values for the gateway/auth runtime:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AUTHENTIK_CLIENT_ID=your-client-id
|
||||||
|
AUTHENTIK_CLIENT_SECRET=your-client-secret
|
||||||
|
AUTHENTIK_ISSUER=https://authentik.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
`AUTHENTIK_ISSUER` should be the Authentik base URL, for example `https://authentik.example.com`.
|
||||||
|
|
||||||
|
## Redirect URI
|
||||||
|
|
||||||
|
Configure this redirect URI in the Authentik provider/application:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{BETTER_AUTH_URL}/api/auth/callback/authentik
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://mosaic.example.com/api/auth/callback/authentik
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test the flow
|
||||||
|
|
||||||
|
1. Start the gateway with `BETTER_AUTH_URL` and the Authentik environment variables set.
|
||||||
|
2. Open the Mosaic login flow and choose the Authentik provider.
|
||||||
|
3. Complete the Authentik login.
|
||||||
|
4. Confirm the browser returns to Mosaic and a session is created successfully.
|
||||||
30
docs/scratchpads/41-plugin-host.md
Normal file
30
docs/scratchpads/41-plugin-host.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Scratchpad — P5-001 Plugin Host
|
||||||
|
|
||||||
|
- Task: P5-001 / issue #41
|
||||||
|
- Branch: feat/p5-plugin-host
|
||||||
|
- Objective: add global NestJS plugin host module, wire Discord import, register active plugins from env, and attach to AppModule.
|
||||||
|
- TDD: skipped as optional for module wiring/integration work; relying on targeted typecheck/lint and module-level situational verification.
|
||||||
|
- Constraints: ESM .js imports, explicit @Inject(), follow existing gateway patterns, do not touch TASKS.md.
|
||||||
|
|
||||||
|
## Progress Log
|
||||||
|
|
||||||
|
- 2026-03-13: session started in worktree; loading gateway/plugin package context.
|
||||||
|
- 2026-03-13: implemented initial plugin module, service, interface, and AppModule wiring; pending verification.
|
||||||
|
- 2026-03-13: added `@mosaic/discord-plugin` as a gateway workspace dependency and regenerated `pnpm-lock.yaml`.
|
||||||
|
- 2026-03-13: built gateway dependency chain so workspace packages exported `dist/*` for clean TypeScript resolution in this fresh worktree.
|
||||||
|
- 2026-03-13: verification complete.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `pnpm --filter @mosaic/gateway... build` ✅
|
||||||
|
- `pnpm --filter @mosaic/gateway typecheck` ✅
|
||||||
|
- `pnpm --filter @mosaic/gateway lint` ✅
|
||||||
|
- `pnpm format:check` ✅
|
||||||
|
- `pnpm typecheck` ✅
|
||||||
|
- `pnpm lint` ✅
|
||||||
|
|
||||||
|
## Review
|
||||||
|
|
||||||
|
- Automated review: `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted`
|
||||||
|
- Manual review: diff inspection of gateway plugin host changes
|
||||||
|
- Result: no blocker findings
|
||||||
@@ -73,6 +73,19 @@ User confirmed: start the planning gate.
|
|||||||
| ------- | ---------- | --------- | ---------- | ----------------------------------------------------------------------------------- |
|
| ------- | ---------- | --------- | ---------- | ----------------------------------------------------------------------------------- |
|
||||||
| 7-8 | 2026-03-12 | Phase 2 | P2-007 | 19 unit tests (routing + coord). PR #79 merged, issue #25 closed. Phase 2 complete. |
|
| 7-8 | 2026-03-12 | Phase 2 | P2-007 | 19 unit tests (routing + coord). PR #79 merged, issue #25 closed. Phase 2 complete. |
|
||||||
|
|
||||||
|
### Session 11 — Phase 5 completion
|
||||||
|
|
||||||
|
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||||
|
| ------- | ---------- | --------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 11 | 2026-03-14 | Phase 5 | P5-005 | Wired Telegram plugin into gateway (was stubbed). Updated .env.example with all P5 env vars. PR #99 merged, issue #45 closed. Phase 5 complete. |
|
||||||
|
|
||||||
|
**Findings during verification:**
|
||||||
|
|
||||||
|
- Telegram plugin was built but not wired into gateway (stub warning in plugin.module.ts)
|
||||||
|
- Discord plugin was fully wired
|
||||||
|
- SSO/Authentik OIDC adapter was fully wired
|
||||||
|
- All three quality gates passing
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
(none at this time)
|
(none at this time)
|
||||||
|
|||||||
50
docs/scratchpads/p5-003-telegram-plugin.md
Normal file
50
docs/scratchpads/p5-003-telegram-plugin.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Scratchpad — P5-003 Telegram Plugin
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement `@mosaic/telegram-plugin` by matching the established Discord plugin pattern with Telegraf + socket.io-client, add package docs, and pass package typecheck/lint.
|
||||||
|
|
||||||
|
## Requirements Source
|
||||||
|
|
||||||
|
- docs/PRD.md: Phase 5 remote control / Telegram plugin
|
||||||
|
- docs/TASKS.md: P5-003
|
||||||
|
- User task brief dated 2026-03-13
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Inspect Discord plugin behavior and package conventions
|
||||||
|
2. Add Telegram runtime dependencies if missing
|
||||||
|
3. Implement Telegram plugin with matching gateway event flow
|
||||||
|
4. Add README usage documentation
|
||||||
|
5. Run package typecheck and lint
|
||||||
|
6. Run code review and remediate findings
|
||||||
|
7. Commit, push, open PR, notify, remove worktree
|
||||||
|
|
||||||
|
## TDD Rationale
|
||||||
|
|
||||||
|
ASSUMPTION: No existing telegram package test harness or fixture coverage makes package-level TDD
|
||||||
|
disproportionate for this plugin scaffold task. Validation will rely on typecheck, lint, and
|
||||||
|
manual structural parity with the Discord plugin.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Telegram API typings may differ from Discord’s event shapes and require narrower guards.
|
||||||
|
- Socket event payloads may already include `role` in shared gateway expectations.
|
||||||
|
|
||||||
|
## Progress Log
|
||||||
|
|
||||||
|
- 2026-03-13: Loaded Mosaic/global/repo guidance, mission files, Discord reference implementation, and Telegram package scaffold.
|
||||||
|
- 2026-03-13: Added `telegraf` and `socket.io-client` to `@mosaic/telegram-plugin`.
|
||||||
|
- 2026-03-13: Implemented Telegram message forwarding, gateway streaming accumulation, response chunking, and package README.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
- `pnpm --filter @mosaic/telegram-plugin typecheck` → pass
|
||||||
|
- `pnpm --filter @mosaic/telegram-plugin lint` → pass
|
||||||
|
- `pnpm typecheck` → pass
|
||||||
|
- `pnpm lint` → pass
|
||||||
|
|
||||||
|
## Review
|
||||||
|
|
||||||
|
- Automated uncommitted review wrapper was invoked for the current delta.
|
||||||
|
- Manual review completed against Discord parity, gateway event contracts, and package docs; no additional blockers found.
|
||||||
44
docs/scratchpads/p5-004-authentik-sso.md
Normal file
44
docs/scratchpads/p5-004-authentik-sso.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# P5-004 Scratchpad
|
||||||
|
|
||||||
|
- Objective: Add optional Authentik OIDC SSO adapter via Better Auth genericOAuth.
|
||||||
|
- Task ref: P5-004
|
||||||
|
- Issue ref: #96
|
||||||
|
- Plan:
|
||||||
|
1. Inspect auth/gateway surfaces and Better Auth plugin shape.
|
||||||
|
2. Add failing coverage for auth config/startup validation where feasible.
|
||||||
|
3. Implement adapter, docs, and warnings.
|
||||||
|
4. Run targeted typechecks, lint, and review.
|
||||||
|
|
||||||
|
- TDD note: no low-friction auth plugin or bootstrap-env test seam exists for `packages/auth/src/auth.ts` or `apps/gateway/src/main.ts`. This change is configuration-oriented and does not alter an existing behavioral contract with a current test harness. I skipped new tests for this pass and relied on exact typecheck/lint/test commands plus manual review.
|
||||||
|
|
||||||
|
- Changes:
|
||||||
|
1. Added conditional Better Auth `genericOAuth` plugin registration for the `authentik` provider in `packages/auth/src/auth.ts`.
|
||||||
|
2. Added a soft startup warning in `apps/gateway/src/main.ts` for incomplete Authentik env configuration.
|
||||||
|
3. Added `docs/plans/authentik-sso-setup.md` with env, redirect URI, and test-flow guidance.
|
||||||
|
4. Confirmed `packages/auth/src/index.ts` already exports `AuthConfig`; no change required there.
|
||||||
|
|
||||||
|
- Verification:
|
||||||
|
1. `pnpm --filter @mosaic/db build`
|
||||||
|
2. `pnpm --filter @mosaic/auth typecheck`
|
||||||
|
3. `pnpm --filter @mosaic/gateway typecheck`
|
||||||
|
4. `pnpm lint`
|
||||||
|
5. `pnpm format:check`
|
||||||
|
6. `pnpm --filter @mosaic/auth test`
|
||||||
|
7. `pnpm --filter @mosaic/gateway test`
|
||||||
|
|
||||||
|
- Results:
|
||||||
|
1. `@mosaic/auth` typecheck passed after replacing the non-existent `enabled` field with conditional plugin registration.
|
||||||
|
2. `@mosaic/gateway` typecheck passed.
|
||||||
|
3. Repo lint passed.
|
||||||
|
4. Prettier check passed after formatting `apps/gateway/src/main.ts`.
|
||||||
|
5. `@mosaic/auth` tests reported `No test files found, exiting with code 0`.
|
||||||
|
6. `@mosaic/gateway` tests passed: `3` files, `20` tests.
|
||||||
|
|
||||||
|
- Review:
|
||||||
|
1. Manual review of the diff found no blocker issues.
|
||||||
|
2. External `codex-code-review.sh --uncommitted` was attempted but did not return a usable verdict in-session; no automated review findings were available from that run.
|
||||||
|
|
||||||
|
- Situational evidence:
|
||||||
|
1. Provider activation is env-gated by `AUTHENTIK_CLIENT_ID`.
|
||||||
|
2. Misconfigured optional SSO surfaces a warning instead of crashing gateway startup.
|
||||||
|
3. Setup doc records the expected redirect path: `{BETTER_AUTH_URL}/api/auth/callback/authentik`.
|
||||||
58
docs/scratchpads/task-mission-ownership-20260313.md
Normal file
58
docs/scratchpads/task-mission-ownership-20260313.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Task Ownership Gap Fix Scratchpad
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
- Date: 2026-03-13
|
||||||
|
- Worktree: `/home/jwoltje/src/mosaic-mono-v1-worktrees/fix-task-ownership`
|
||||||
|
- Branch: `fix/task-mission-ownership`
|
||||||
|
- Scope: Fix ownership checks in TasksController/MissionsController and extend gateway ownership tests
|
||||||
|
- Related tracker: worker task only; `docs/TASKS.md` is orchestrator-owned and left unchanged
|
||||||
|
- Budget assumption: no explicit token cap; keep scope limited to requested gateway permission fixes
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Close ownership gaps so task listing/creation and mission creation enforce project/mission ownership and reject cross-user access.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. TasksController `list()` enforces ownership for `projectId` and `missionId`, and does not return cross-user data when neither filter is provided.
|
||||||
|
2. TasksController `create()` rejects unowned `projectId` and `missionId` references.
|
||||||
|
3. MissionsController `create()` rejects unowned `projectId` references.
|
||||||
|
4. Gateway ownership tests cover forbidden task creation and forbidden task listing by unowned project.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Inspect current controller and ownership test patterns.
|
||||||
|
2. Add failing permission tests first.
|
||||||
|
3. Patch controller methods with existing ownership helpers.
|
||||||
|
4. Run targeted gateway tests, then gateway typecheck/lint/full test.
|
||||||
|
5. Perform independent review, record evidence, then complete the requested git/PR workflow.
|
||||||
|
|
||||||
|
## TDD Notes
|
||||||
|
|
||||||
|
- Required: yes. This is auth/permission logic and a bugfix.
|
||||||
|
- Strategy: add failing tests in `resource-ownership.test.ts`, verify red, then implement minimal controller changes.
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- `pnpm --filter @mosaic/gateway test -- src/__tests__/resource-ownership.test.ts`
|
||||||
|
- Red: failed with 2 expected permission-path failures before controller changes.
|
||||||
|
- Green: passed after wiring ownership checks and adding owned-task filtering coverage.
|
||||||
|
- `pnpm --filter @mosaic/gateway typecheck`
|
||||||
|
- Pass on 2026-03-13 after fixing parameter ordering and mission project nullability.
|
||||||
|
- `pnpm --filter @mosaic/gateway lint`
|
||||||
|
- Pass on 2026-03-13.
|
||||||
|
- `pnpm --filter @mosaic/gateway test`
|
||||||
|
- Pass on 2026-03-13 with 3 test files and 23 tests passing.
|
||||||
|
- `pnpm format:check`
|
||||||
|
- Pass on 2026-03-13.
|
||||||
|
|
||||||
|
## Review Log
|
||||||
|
|
||||||
|
- Manual review: checked for auth regressions, cross-user list leakage, and dashboard behavior impact; kept unfiltered task list functional by filtering to owned projects/missions instead of returning an empty list.
|
||||||
|
- Automated review: `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted` running/re-run for independent review evidence.
|
||||||
|
|
||||||
|
## Risks / Blockers
|
||||||
|
|
||||||
|
- Repository-wide Mosaic instructions require merge/issue closure, but the user explicitly instructed PR-only and no merge; follow the user instruction.
|
||||||
|
- `docs/TASKS.md` is orchestrator-owned and will not be edited from this worker task.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { betterAuth } from 'better-auth';
|
import { betterAuth } from 'better-auth';
|
||||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||||
|
import { genericOAuth } from 'better-auth/plugins';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaic/db';
|
||||||
|
|
||||||
export interface AuthConfig {
|
export interface AuthConfig {
|
||||||
@@ -10,6 +11,33 @@ export interface AuthConfig {
|
|||||||
|
|
||||||
export function createAuth(config: AuthConfig) {
|
export function createAuth(config: AuthConfig) {
|
||||||
const { db, baseURL, secret } = config;
|
const { db, baseURL, secret } = config;
|
||||||
|
const authentikIssuer = process.env['AUTHENTIK_ISSUER'];
|
||||||
|
const authentikClientId = process.env['AUTHENTIK_CLIENT_ID'];
|
||||||
|
const authentikClientSecret = process.env['AUTHENTIK_CLIENT_SECRET'];
|
||||||
|
const plugins = authentikClientId
|
||||||
|
? [
|
||||||
|
genericOAuth({
|
||||||
|
config: [
|
||||||
|
{
|
||||||
|
providerId: 'authentik',
|
||||||
|
clientId: authentikClientId,
|
||||||
|
clientSecret: authentikClientSecret ?? '',
|
||||||
|
discoveryUrl: authentikIssuer
|
||||||
|
? `${authentikIssuer}/.well-known/openid-configuration`
|
||||||
|
: undefined,
|
||||||
|
authorizationUrl: authentikIssuer
|
||||||
|
? `${authentikIssuer}/application/o/authorize/`
|
||||||
|
: undefined,
|
||||||
|
tokenUrl: authentikIssuer ? `${authentikIssuer}/application/o/token/` : undefined,
|
||||||
|
userInfoUrl: authentikIssuer
|
||||||
|
? `${authentikIssuer}/application/o/userinfo/`
|
||||||
|
: undefined,
|
||||||
|
scopes: ['openid', 'email', 'profile'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return betterAuth({
|
return betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
@@ -36,6 +64,7 @@ export function createAuth(config: AuthConfig) {
|
|||||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||||
updateAge: 60 * 60 * 24, // refresh daily
|
updateAge: 60 * 60 * 24, // refresh daily
|
||||||
},
|
},
|
||||||
|
plugins,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
plugins/telegram/README.md
Normal file
23
plugins/telegram/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# @mosaic/telegram-plugin
|
||||||
|
|
||||||
|
`@mosaic/telegram-plugin` connects a Telegram bot to the Mosaic gateway chat namespace so Telegram chats can participate in the same conversation flow as the web, TUI, and Discord channels.
|
||||||
|
|
||||||
|
## Required Environment Variables
|
||||||
|
|
||||||
|
- `TELEGRAM_BOT_TOKEN`: Bot token issued by BotFather
|
||||||
|
- `TELEGRAM_GATEWAY_URL`: Base URL for the Mosaic gateway, for example `http://localhost:3000`
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
- Launches a Telegram bot with `telegraf`
|
||||||
|
- Connects to `${TELEGRAM_GATEWAY_URL}/chat` with `socket.io-client`
|
||||||
|
- Maps Telegram `chat.id` values to Mosaic `conversationId` values
|
||||||
|
- Forwards inbound Telegram text messages to the gateway as user messages
|
||||||
|
- Buffers `agent:start` / `agent:text` / `agent:end` socket events and sends the completed response back to the Telegram chat
|
||||||
|
|
||||||
|
## Getting a Bot Token
|
||||||
|
|
||||||
|
1. Open Telegram and start a chat with `@BotFather`
|
||||||
|
2. Run `/newbot`
|
||||||
|
3. Follow the prompts to name the bot and choose a username
|
||||||
|
4. Copy the generated token and assign it to `TELEGRAM_BOT_TOKEN`
|
||||||
@@ -18,5 +18,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^2.0.0"
|
"vitest": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io-client": "^4.8.0",
|
||||||
|
"telegraf": "^4.16.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,187 @@
|
|||||||
export const VERSION = '0.0.0';
|
import { Telegraf } from 'telegraf';
|
||||||
|
import { io, type Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
interface TelegramPluginConfig {
|
||||||
|
token: string;
|
||||||
|
gatewayUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelegramUser {
|
||||||
|
is_bot?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelegramChat {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelegramTextMessage {
|
||||||
|
chat: TelegramChat;
|
||||||
|
from?: TelegramUser;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TelegramPlugin {
|
||||||
|
readonly name = 'telegram';
|
||||||
|
|
||||||
|
private bot: Telegraf;
|
||||||
|
private socket: Socket | null = null;
|
||||||
|
private config: TelegramPluginConfig;
|
||||||
|
/** Map Telegram chat ID → Mosaic conversation ID */
|
||||||
|
private chatConversations = new Map<string, string>();
|
||||||
|
/** Track in-flight responses to avoid duplicate streaming */
|
||||||
|
private pendingResponses = new Map<string, string>();
|
||||||
|
|
||||||
|
constructor(config: TelegramPluginConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.bot = new Telegraf(this.config.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
// Connect to gateway WebSocket
|
||||||
|
this.socket = io(`${this.config.gatewayUrl}/chat`, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('connect', () => {
|
||||||
|
console.log('[telegram] Connected to gateway');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('disconnect', (reason: string) => {
|
||||||
|
console.error(`[telegram] Disconnected from gateway: ${reason}`);
|
||||||
|
this.pendingResponses.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('connect_error', (err: Error) => {
|
||||||
|
console.error(`[telegram] Gateway connection error: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle streaming text from gateway
|
||||||
|
this.socket.on('agent:text', (data: { conversationId: string; text: string }) => {
|
||||||
|
const pending = this.pendingResponses.get(data.conversationId);
|
||||||
|
if (pending !== undefined) {
|
||||||
|
this.pendingResponses.set(data.conversationId, pending + data.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When agent finishes, send the accumulated response
|
||||||
|
this.socket.on('agent:end', (data: { conversationId: string }) => {
|
||||||
|
const text = this.pendingResponses.get(data.conversationId);
|
||||||
|
if (text) {
|
||||||
|
this.pendingResponses.delete(data.conversationId);
|
||||||
|
this.sendToTelegram(data.conversationId, text).catch((err) => {
|
||||||
|
console.error(`[telegram] Error sending response for ${data.conversationId}:`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('agent:start', (data: { conversationId: string }) => {
|
||||||
|
this.pendingResponses.set(data.conversationId, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up Telegram message handler
|
||||||
|
this.bot.on('message', (ctx) => {
|
||||||
|
const message = this.getTextMessage(ctx.message);
|
||||||
|
if (message) {
|
||||||
|
this.handleTelegramMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.bot.launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
this.bot.stop('SIGTERM');
|
||||||
|
this.socket?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTelegramMessage(message: TelegramTextMessage): void {
|
||||||
|
// Ignore bot messages
|
||||||
|
if (message.from?.is_bot) return;
|
||||||
|
|
||||||
|
const content = message.text.trim();
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
// Get or create conversation for this Telegram chat
|
||||||
|
const chatId = String(message.chat.id);
|
||||||
|
let conversationId = this.chatConversations.get(chatId);
|
||||||
|
if (!conversationId) {
|
||||||
|
conversationId = `telegram-${chatId}`;
|
||||||
|
this.chatConversations.set(chatId, conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to gateway
|
||||||
|
if (!this.socket?.connected) {
|
||||||
|
console.error(`[telegram] Cannot forward message: not connected to gateway. chat=${chatId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.emit('message', {
|
||||||
|
conversationId,
|
||||||
|
content,
|
||||||
|
role: 'user',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTextMessage(message: unknown): TelegramTextMessage | null {
|
||||||
|
if (!message || typeof message !== 'object') return null;
|
||||||
|
|
||||||
|
const candidate = message as Partial<TelegramTextMessage>;
|
||||||
|
if (typeof candidate.text !== 'string') return null;
|
||||||
|
if (!candidate.chat || typeof candidate.chat.id !== 'number') return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat: candidate.chat,
|
||||||
|
from: candidate.from,
|
||||||
|
text: candidate.text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendToTelegram(conversationId: string, text: string): Promise<void> {
|
||||||
|
// Find the Telegram chat for this conversation
|
||||||
|
const chatId = Array.from(this.chatConversations.entries()).find(
|
||||||
|
([, convId]) => convId === conversationId,
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (!chatId) {
|
||||||
|
console.error(`[telegram] No chat found for conversation ${conversationId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk responses for Telegram's 4096-char limit
|
||||||
|
const chunks = this.chunkText(text, 4000);
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
try {
|
||||||
|
await this.bot.telegram.sendMessage(chatId, chunk);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[telegram] Failed to send message to chat ${chatId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private chunkText(text: string, maxLength: number): string[] {
|
||||||
|
if (text.length <= maxLength) return [text];
|
||||||
|
|
||||||
|
const chunks: string[] = [];
|
||||||
|
let remaining = text;
|
||||||
|
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
if (remaining.length <= maxLength) {
|
||||||
|
chunks.push(remaining);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to break at a newline
|
||||||
|
let breakPoint = remaining.lastIndexOf('\n', maxLength);
|
||||||
|
if (breakPoint <= 0) breakPoint = maxLength;
|
||||||
|
|
||||||
|
chunks.push(remaining.slice(0, breakPoint));
|
||||||
|
remaining = remaining.slice(breakPoint).trimStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TelegramPlugin };
|
||||||
|
export type { TelegramPluginConfig };
|
||||||
|
export const VERSION = '0.0.5';
|
||||||
|
|||||||
125
pnpm-lock.yaml
generated
125
pnpm-lock.yaml
generated
@@ -62,12 +62,18 @@ importers:
|
|||||||
'@mosaic/db':
|
'@mosaic/db':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/db
|
version: link:../../packages/db
|
||||||
|
'@mosaic/discord-plugin':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../plugins/discord
|
||||||
'@mosaic/log':
|
'@mosaic/log':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/log
|
version: link:../../packages/log
|
||||||
'@mosaic/memory':
|
'@mosaic/memory':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/memory
|
version: link:../../packages/memory
|
||||||
|
'@mosaic/telegram-plugin':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../plugins/telegram
|
||||||
'@mosaic/types':
|
'@mosaic/types':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/types
|
version: link:../../packages/types
|
||||||
@@ -458,6 +464,13 @@ importers:
|
|||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
||||||
|
|
||||||
plugins/telegram:
|
plugins/telegram:
|
||||||
|
dependencies:
|
||||||
|
socket.io-client:
|
||||||
|
specifier: ^4.8.0
|
||||||
|
version: 4.8.3
|
||||||
|
telegraf:
|
||||||
|
specifier: ^4.16.3
|
||||||
|
version: 4.16.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.0
|
specifier: ^5.8.0
|
||||||
@@ -2738,6 +2751,9 @@ packages:
|
|||||||
'@tailwindcss/postcss@4.2.1':
|
'@tailwindcss/postcss@4.2.1':
|
||||||
resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==}
|
resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==}
|
||||||
|
|
||||||
|
'@telegraf/types@7.1.0':
|
||||||
|
resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==}
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2920,6 +2936,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==}
|
resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==}
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
|
||||||
abstract-logging@2.0.1:
|
abstract-logging@2.0.1:
|
||||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||||
|
|
||||||
@@ -3122,12 +3142,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==}
|
resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==}
|
||||||
engines: {node: '>=20.19.0'}
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
|
buffer-alloc-unsafe@1.1.0:
|
||||||
|
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
|
||||||
|
|
||||||
|
buffer-alloc@1.2.0:
|
||||||
|
resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
|
||||||
|
|
||||||
buffer-crc32@0.2.13:
|
buffer-crc32@0.2.13:
|
||||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1:
|
buffer-equal-constant-time@1.0.1:
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
|
buffer-fill@1.0.0:
|
||||||
|
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@@ -3547,6 +3576,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1:
|
||||||
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
eventemitter3@5.0.4:
|
eventemitter3@5.0.4:
|
||||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
@@ -4162,6 +4195,10 @@ packages:
|
|||||||
socks:
|
socks:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
mri@1.2.0:
|
||||||
|
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -4218,6 +4255,15 @@ packages:
|
|||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
deprecated: Use your platform's native DOMException instead
|
deprecated: Use your platform's native DOMException instead
|
||||||
|
|
||||||
|
node-fetch@2.7.0:
|
||||||
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
encoding: ^0.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
encoding:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-fetch@3.3.2:
|
node-fetch@3.3.2:
|
||||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -4281,6 +4327,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
|
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
p-timeout@4.1.0:
|
||||||
|
resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
pac-proxy-agent@7.2.0:
|
pac-proxy-agent@7.2.0:
|
||||||
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
|
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -4551,6 +4601,9 @@ packages:
|
|||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
safe-compare@1.1.4:
|
||||||
|
resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==}
|
||||||
|
|
||||||
safe-regex2@5.1.0:
|
safe-regex2@5.1.0:
|
||||||
resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==}
|
resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4559,6 +4612,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
sandwich-stream@2.0.2:
|
||||||
|
resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
scheduler@0.23.2:
|
scheduler@0.23.2:
|
||||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||||
|
|
||||||
@@ -4736,6 +4793,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
telegraf@4.16.3:
|
||||||
|
resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==}
|
||||||
|
engines: {node: ^12.20.0 || >=14.13.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -4781,6 +4843,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
|
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
tr46@0.0.3:
|
||||||
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
tr46@5.1.1:
|
tr46@5.1.1:
|
||||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4958,6 +5023,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
webidl-conversions@3.0.1:
|
||||||
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
webidl-conversions@7.0.0:
|
webidl-conversions@7.0.0:
|
||||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -4966,6 +5034,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
whatwg-url@5.0.0:
|
||||||
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -7559,6 +7630,8 @@ snapshots:
|
|||||||
postcss: 8.5.8
|
postcss: 8.5.8
|
||||||
tailwindcss: 4.2.1
|
tailwindcss: 4.2.1
|
||||||
|
|
||||||
|
'@telegraf/types@7.1.0': {}
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -7791,6 +7864,10 @@ snapshots:
|
|||||||
|
|
||||||
'@vladfrangu/async_event_emitter@2.4.7': {}
|
'@vladfrangu/async_event_emitter@2.4.7': {}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
|
||||||
abstract-logging@2.0.1: {}
|
abstract-logging@2.0.1: {}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
@@ -7935,10 +8012,19 @@ snapshots:
|
|||||||
|
|
||||||
bson@7.2.0: {}
|
bson@7.2.0: {}
|
||||||
|
|
||||||
|
buffer-alloc-unsafe@1.1.0: {}
|
||||||
|
|
||||||
|
buffer-alloc@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
buffer-alloc-unsafe: 1.1.0
|
||||||
|
buffer-fill: 1.0.0
|
||||||
|
|
||||||
buffer-crc32@0.2.13: {}
|
buffer-crc32@0.2.13: {}
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
|
buffer-fill@1.0.0: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
cac@6.7.14: {}
|
cac@6.7.14: {}
|
||||||
@@ -8384,6 +8470,8 @@ snapshots:
|
|||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1: {}
|
||||||
|
|
||||||
eventemitter3@5.0.4: {}
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
execa@8.0.1:
|
execa@8.0.1:
|
||||||
@@ -9012,6 +9100,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
socks: 2.8.7
|
socks: 2.8.7
|
||||||
|
|
||||||
|
mri@1.2.0: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
mz@2.7.0:
|
mz@2.7.0:
|
||||||
@@ -9059,6 +9149,10 @@ snapshots:
|
|||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
|
node-fetch@2.7.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
node-fetch@3.3.2:
|
node-fetch@3.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
data-uri-to-buffer: 4.0.1
|
data-uri-to-buffer: 4.0.1
|
||||||
@@ -9118,6 +9212,8 @@ snapshots:
|
|||||||
'@types/retry': 0.12.0
|
'@types/retry': 0.12.0
|
||||||
retry: 0.13.1
|
retry: 0.13.1
|
||||||
|
|
||||||
|
p-timeout@4.1.0: {}
|
||||||
|
|
||||||
pac-proxy-agent@7.2.0:
|
pac-proxy-agent@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||||
@@ -9402,12 +9498,18 @@ snapshots:
|
|||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
|
safe-compare@1.1.4:
|
||||||
|
dependencies:
|
||||||
|
buffer-alloc: 1.2.0
|
||||||
|
|
||||||
safe-regex2@5.1.0:
|
safe-regex2@5.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ret: 0.5.0
|
ret: 0.5.0
|
||||||
|
|
||||||
safe-stable-stringify@2.5.0: {}
|
safe-stable-stringify@2.5.0: {}
|
||||||
|
|
||||||
|
sandwich-stream@2.0.2: {}
|
||||||
|
|
||||||
scheduler@0.23.2:
|
scheduler@0.23.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@@ -9614,6 +9716,20 @@ snapshots:
|
|||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|
||||||
|
telegraf@4.16.3:
|
||||||
|
dependencies:
|
||||||
|
'@telegraf/types': 7.1.0
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
debug: 4.4.3
|
||||||
|
mri: 1.2.0
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
p-timeout: 4.1.0
|
||||||
|
safe-compare: 1.1.4
|
||||||
|
sandwich-stream: 2.0.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
thenify: 3.3.1
|
thenify: 3.3.1
|
||||||
@@ -9653,6 +9769,8 @@ snapshots:
|
|||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
tr46@5.1.1:
|
tr46@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
@@ -9807,6 +9925,8 @@ snapshots:
|
|||||||
|
|
||||||
web-streams-polyfill@3.3.3: {}
|
web-streams-polyfill@3.3.3: {}
|
||||||
|
|
||||||
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
webidl-conversions@7.0.0: {}
|
webidl-conversions@7.0.0: {}
|
||||||
|
|
||||||
whatwg-url@14.2.0:
|
whatwg-url@14.2.0:
|
||||||
@@ -9814,6 +9934,11 @@ snapshots:
|
|||||||
tr46: 5.1.1
|
tr46: 5.1.1
|
||||||
webidl-conversions: 7.0.0
|
webidl-conversions: 7.0.0
|
||||||
|
|
||||||
|
whatwg-url@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
tr46: 0.0.3
|
||||||
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user