Compare commits
1 Commits
feat/gatew
...
fix/idempo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08da9164a |
12
.env.example
12
.env.example
@@ -23,8 +23,8 @@ VALKEY_URL=redis://localhost:6380
|
||||
|
||||
|
||||
# ─── Gateway ─────────────────────────────────────────────────────────────────
|
||||
# TCP port the NestJS/Fastify gateway listens on (default: 14242)
|
||||
GATEWAY_PORT=14242
|
||||
# TCP port the NestJS/Fastify gateway listens on (default: 4000)
|
||||
GATEWAY_PORT=4000
|
||||
|
||||
# Comma-separated list of allowed CORS origins.
|
||||
# Must include the web app origin in production.
|
||||
@@ -37,12 +37,12 @@ GATEWAY_CORS_ORIGIN=http://localhost:3000
|
||||
BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
|
||||
|
||||
# Public base URL of the gateway (used by BetterAuth for callback URLs)
|
||||
BETTER_AUTH_URL=http://localhost:14242
|
||||
BETTER_AUTH_URL=http://localhost:4000
|
||||
|
||||
|
||||
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
|
||||
# Public gateway URL — accessible from the browser, not just the server.
|
||||
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
|
||||
NEXT_PUBLIC_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
|
||||
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
||||
@@ -121,12 +121,12 @@ OTEL_SERVICE_NAME=mosaic-gateway
|
||||
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
|
||||
# DISCORD_BOT_TOKEN=
|
||||
# DISCORD_GUILD_ID=
|
||||
# DISCORD_GATEWAY_URL=http://localhost:14242
|
||||
# DISCORD_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
|
||||
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
|
||||
# TELEGRAM_BOT_TOKEN=
|
||||
# TELEGRAM_GATEWAY_URL=http://localhost:14242
|
||||
# TELEGRAM_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
|
||||
# ─── SSO Providers (add credentials to enable) ───────────────────────────────
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1 +1 @@
|
||||
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/
|
||||
@mosaic:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/
|
||||
|
||||
@@ -15,7 +15,6 @@ steps:
|
||||
image: *node_image
|
||||
commands:
|
||||
- corepack enable
|
||||
- apk add --no-cache python3 make g++
|
||||
- pnpm install --frozen-lockfile
|
||||
|
||||
typecheck:
|
||||
@@ -59,7 +58,7 @@ steps:
|
||||
sleep 1
|
||||
done
|
||||
# Run migrations (DATABASE_URL is set in environment above)
|
||||
- pnpm --filter @mosaicstack/db run db:migrate
|
||||
- pnpm --filter @mosaic/db run db:migrate
|
||||
# Run all tests
|
||||
- pnpm test
|
||||
depends_on:
|
||||
|
||||
@@ -33,62 +33,19 @@ steps:
|
||||
- *enable_pnpm
|
||||
# Configure auth for Gitea npm registry
|
||||
- |
|
||||
echo "//git.mosaicstack.dev/api/packages/mosaicstack/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
|
||||
# Publish non-private packages to Gitea.
|
||||
#
|
||||
# The only publish failure we tolerate is "version already exists" —
|
||||
# that legitimately happens when only some packages were bumped in
|
||||
# the merge. Any other failure (registry 404, auth error, network
|
||||
# error) MUST fail the pipeline loudly: the previous
|
||||
# `|| echo "... continuing"` fallback silently hid a 404 from the
|
||||
# Gitea org rename and caused every @mosaicstack/* publish to fall
|
||||
# on the floor while CI still reported green.
|
||||
- |
|
||||
# Portable sh (Alpine ash) — avoid bashisms like PIPESTATUS.
|
||||
set +e
|
||||
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" publish --no-git-checks --access public >/tmp/publish.log 2>&1
|
||||
EXIT=$?
|
||||
set -e
|
||||
cat /tmp/publish.log
|
||||
if [ "$EXIT" -eq 0 ]; then
|
||||
echo "[publish] all packages published successfully"
|
||||
exit 0
|
||||
fi
|
||||
# Hard registry / auth / network errors → fatal. Match npm's own
|
||||
# error lines specifically to avoid false positives on arbitrary
|
||||
# log text that happens to contain "E404" etc.
|
||||
if grep -qE "npm (error|ERR!) code (E404|E401|ENEEDAUTH|ECONNREFUSED|ETIMEDOUT|ENOTFOUND)" /tmp/publish.log; then
|
||||
echo "[publish] FATAL: registry/auth/network error detected — failing pipeline" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Only tolerate the explicit "version already published" case.
|
||||
# npm returns this as E403 with body "You cannot publish over..."
|
||||
# or EPUBLISHCONFLICT depending on version.
|
||||
if grep -qE "EPUBLISHCONFLICT|You cannot publish over|previously published" /tmp/publish.log; then
|
||||
echo "[publish] some packages already at this version — continuing (non-fatal)"
|
||||
exit 0
|
||||
fi
|
||||
echo "[publish] FATAL: publish failed with unrecognized error — failing pipeline" >&2
|
||||
exit 1
|
||||
echo "//git.mosaicstack.dev/api/packages/mosaic/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
echo "@mosaic:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/" >> ~/.npmrc
|
||||
# Publish all non-private packages (--no-git-checks skips dirty/branch checks in CI)
|
||||
# --filter excludes private apps (gateway, web) and the root
|
||||
- >
|
||||
pnpm --filter "@mosaic/*"
|
||||
--filter "!@mosaic/gateway"
|
||||
--filter "!@mosaic/web"
|
||||
publish --no-git-checks --access public
|
||||
|| echo "[publish] Some packages may already exist at this version — continuing"
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
# TODO: Uncomment when ready to publish to npmjs.org
|
||||
# publish-npmjs:
|
||||
# image: *node_image
|
||||
# environment:
|
||||
# NPM_TOKEN:
|
||||
# from_secret: npmjs_token
|
||||
# commands:
|
||||
# - *enable_pnpm
|
||||
# - apk add --no-cache jq bash
|
||||
# - bash scripts/publish-npmjs.sh
|
||||
# depends_on:
|
||||
# - build
|
||||
# when:
|
||||
# - event: [tag]
|
||||
|
||||
build-gateway:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
@@ -103,12 +60,12 @@ steps:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/mosaic-stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
|
||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:latest"
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaic/mosaic-stack/gateway:latest"
|
||||
fi
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:$CI_COMMIT_TAG"
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaic/mosaic-stack/gateway:$CI_COMMIT_TAG"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
|
||||
depends_on:
|
||||
@@ -128,12 +85,12 @@ steps:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/mosaic-stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:latest"
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaic/mosaic-stack/web:latest"
|
||||
fi
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:$CI_COMMIT_TAG"
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaic/mosaic-stack/web:$CI_COMMIT_TAG"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
|
||||
depends_on:
|
||||
|
||||
12
AGENTS.md
12
AGENTS.md
@@ -21,11 +21,11 @@ Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo
|
||||
| `apps/web` | Next.js dashboard | React 19, Tailwind |
|
||||
| `packages/types` | Shared TypeScript contracts | class-validator |
|
||||
| `packages/db` | Drizzle ORM schema + migrations | drizzle-orm, postgres |
|
||||
| `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db |
|
||||
| `packages/brain` | Data layer (PG-backed) | @mosaicstack/db |
|
||||
| `packages/auth` | BetterAuth configuration | better-auth, @mosaic/db |
|
||||
| `packages/brain` | Data layer (PG-backed) | @mosaic/db |
|
||||
| `packages/queue` | Valkey task queue + MCP | ioredis |
|
||||
| `packages/coord` | Mission coordination | @mosaicstack/queue |
|
||||
| `packages/mosaic` | Unified `mosaic` CLI + TUI | Ink, Pi SDK, commander |
|
||||
| `packages/coord` | Mission coordination | @mosaic/queue |
|
||||
| `packages/cli` | Unified CLI + Pi TUI | Ink, Pi SDK |
|
||||
| `plugins/discord` | Discord channel plugin | discord.js |
|
||||
| `plugins/telegram` | Telegram channel plugin | Telegraf |
|
||||
|
||||
@@ -33,9 +33,9 @@ Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo
|
||||
|
||||
1. Gateway is the single API surface — all clients connect through it
|
||||
2. Pi SDK is ESM-only — gateway and CLI must use ESM
|
||||
3. Socket.IO typed events defined in `@mosaicstack/types` enforce compile-time contracts
|
||||
3. Socket.IO typed events defined in `@mosaic/types` enforce compile-time contracts
|
||||
4. OTEL auto-instrumentation loads before NestJS bootstrap
|
||||
5. BetterAuth manages auth tables; schema defined in `@mosaicstack/db`
|
||||
5. BetterAuth manages auth tables; schema defined in `@mosaic/db`
|
||||
6. Docker Compose provides PG (5433), Valkey (6380), OTEL Collector (4317/4318), Jaeger (16686)
|
||||
7. Explicit `@Inject()` decorators required in NestJS (tsx/esbuild doesn't emit decorator metadata)
|
||||
|
||||
|
||||
10
CLAUDE.md
10
CLAUDE.md
@@ -10,7 +10,7 @@ Self-hosted, multi-user AI agent platform. TypeScript monorepo.
|
||||
- **Web**: Next.js 16 + React 19 (`apps/web`)
|
||||
- **ORM**: Drizzle ORM + PostgreSQL 17 + pgvector (`packages/db`)
|
||||
- **Auth**: BetterAuth (`packages/auth`)
|
||||
- **Agent**: Pi SDK (`packages/agent`, `packages/mosaic`)
|
||||
- **Agent**: Pi SDK (`packages/agent`, `packages/cli`)
|
||||
- **Queue**: Valkey 8 (`packages/queue`)
|
||||
- **Build**: pnpm workspaces + Turborepo
|
||||
- **CI**: Woodpecker CI
|
||||
@@ -26,13 +26,13 @@ pnpm test # Vitest (all packages)
|
||||
pnpm build # Build all packages
|
||||
|
||||
# Database
|
||||
pnpm --filter @mosaicstack/db db:push # Push schema to PG (dev)
|
||||
pnpm --filter @mosaicstack/db db:generate # Generate migrations
|
||||
pnpm --filter @mosaicstack/db db:migrate # Run migrations
|
||||
pnpm --filter @mosaic/db db:push # Push schema to PG (dev)
|
||||
pnpm --filter @mosaic/db db:generate # Generate migrations
|
||||
pnpm --filter @mosaic/db db:migrate # Run migrations
|
||||
|
||||
# Dev
|
||||
docker compose up -d # Start PG, Valkey, OTEL, Jaeger
|
||||
pnpm --filter @mosaicstack/gateway exec tsx src/main.ts # Start gateway
|
||||
pnpm --filter @mosaic/gateway exec tsx src/main.ts # Start gateway
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
14
README.md
14
README.md
@@ -12,10 +12,10 @@ bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/mai
|
||||
|
||||
This installs both components:
|
||||
|
||||
| Component | What | Where |
|
||||
| ----------------------- | ---------------------------------------------------------------- | -------------------- |
|
||||
| **Framework** | Bash launcher, guides, runtime configs, tools, skills | `~/.config/mosaic/` |
|
||||
| **@mosaicstack/mosaic** | Unified `mosaic` CLI — TUI, gateway client, wizard, auto-updater | `~/.npm-global/bin/` |
|
||||
| Component | What | Where |
|
||||
| --------------- | ----------------------------------------------------- | -------------------- |
|
||||
| **Framework** | Bash launcher, guides, runtime configs, tools, skills | `~/.config/mosaic/` |
|
||||
| **@mosaic/cli** | TUI, gateway client, wizard, auto-updater | `~/.npm-global/bin/` |
|
||||
|
||||
After install, set up your agent identity:
|
||||
|
||||
@@ -26,7 +26,7 @@ mosaic init # Interactive wizard
|
||||
### Requirements
|
||||
|
||||
- Node.js ≥ 20
|
||||
- npm (for global @mosaicstack/mosaic install)
|
||||
- npm (for global @mosaic/cli install)
|
||||
- One or more runtimes: [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai), or [Pi](https://github.com/mariozechner/pi-coding-agent)
|
||||
|
||||
## Usage
|
||||
@@ -86,7 +86,7 @@ docker compose up -d
|
||||
pnpm install
|
||||
|
||||
# Run migrations
|
||||
pnpm --filter @mosaicstack/db run db:migrate
|
||||
pnpm --filter @mosaic/db run db:migrate
|
||||
|
||||
# Start all services in dev mode
|
||||
pnpm dev
|
||||
@@ -163,7 +163,7 @@ mosaic-stack/
|
||||
|
||||
- **Gateway is the single API surface** — all clients (TUI, web, Discord, Telegram) connect through it
|
||||
- **ESM everywhere** — `"type": "module"`, `.js` extensions in imports, NodeNext resolution
|
||||
- **Socket.IO typed events** — defined in `@mosaicstack/types`, enforced at compile time
|
||||
- **Socket.IO typed events** — defined in `@mosaic/types`, enforced at compile time
|
||||
- **OTEL auto-instrumentation** — loads before NestJS bootstrap
|
||||
- **Explicit `@Inject()` decorators** — required since tsx/esbuild doesn't emit decorator metadata
|
||||
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
{
|
||||
"name": "@mosaicstack/gateway",
|
||||
"version": "0.0.6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
|
||||
"directory": "apps/gateway"
|
||||
},
|
||||
"name": "@mosaic/gateway",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/main.js",
|
||||
"bin": {
|
||||
"mosaic-gateway": "dist/main.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"registry": "https://git.mosaicstack.dev/api/packages/mosaicstack/npm/",
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx watch src/main.ts",
|
||||
@@ -28,28 +14,26 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@fastify/helmet": "^13.0.2",
|
||||
"@mariozechner/pi-ai": "^0.65.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.65.0",
|
||||
"@mariozechner/pi-ai": "~0.57.1",
|
||||
"@mariozechner/pi-coding-agent": "~0.57.1",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@mosaicstack/auth": "workspace:^",
|
||||
"@mosaicstack/brain": "workspace:^",
|
||||
"@mosaicstack/config": "workspace:^",
|
||||
"@mosaicstack/coord": "workspace:^",
|
||||
"@mosaicstack/db": "workspace:^",
|
||||
"@mosaicstack/discord-plugin": "workspace:^",
|
||||
"@mosaicstack/log": "workspace:^",
|
||||
"@mosaicstack/memory": "workspace:^",
|
||||
"@mosaicstack/queue": "workspace:^",
|
||||
"@mosaicstack/storage": "workspace:^",
|
||||
"@mosaicstack/telegram-plugin": "workspace:^",
|
||||
"@mosaicstack/types": "workspace:^",
|
||||
"@mosaic/auth": "workspace:^",
|
||||
"@mosaic/brain": "workspace:^",
|
||||
"@mosaic/coord": "workspace:^",
|
||||
"@mosaic/db": "workspace:^",
|
||||
"@mosaic/discord-plugin": "workspace:^",
|
||||
"@mosaic/log": "workspace:^",
|
||||
"@mosaic/memory": "workspace:^",
|
||||
"@mosaic/queue": "workspace:^",
|
||||
"@mosaic/telegram-plugin": "workspace:^",
|
||||
"@mosaic/types": "workspace:^",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/platform-fastify": "^11.0.0",
|
||||
"@nestjs/platform-socket.io": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@nestjs/websockets": "^11.0.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.72.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
|
||||
"@opentelemetry/resources": "^2.6.0",
|
||||
|
||||
@@ -12,7 +12,7 @@ import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import type { ConversationHistoryMessage } from '../agent/agent.service.js';
|
||||
import { ConversationsController } from '../conversations/conversations.controller.js';
|
||||
import type { Message } from '@mosaicstack/brain';
|
||||
import type { Message } from '@mosaic/brain';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared test data
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createDb } from '@mosaicstack/db';
|
||||
import { createConversationsRepo } from '@mosaicstack/brain';
|
||||
import { createAgentsRepo } from '@mosaicstack/brain';
|
||||
import { createPreferencesRepo, createInsightsRepo } from '@mosaicstack/memory';
|
||||
import { users, conversations, messages, agents, preferences, insights } from '@mosaicstack/db';
|
||||
import { eq } from '@mosaicstack/db';
|
||||
import type { DbHandle } from '@mosaicstack/db';
|
||||
import { createDb } from '@mosaic/db';
|
||||
import { createConversationsRepo } from '@mosaic/brain';
|
||||
import { createAgentsRepo } from '@mosaic/brain';
|
||||
import { createPreferencesRepo, createInsightsRepo } from '@mosaic/memory';
|
||||
import { users, conversations, messages, agents, preferences, insights } from '@mosaic/db';
|
||||
import { eq } from '@mosaic/db';
|
||||
import type { DbHandle } from '@mosaic/db';
|
||||
|
||||
// ─── Fixed IDs so the afterAll cleanup is deterministic ──────────────────────
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||
import { sql, type Db } from '@mosaicstack/db';
|
||||
import { createQueue } from '@mosaicstack/queue';
|
||||
import { sql, type Db } from '@mosaic/db';
|
||||
import { createQueue } from '@mosaic/queue';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { AgentService } from '../agent/agent.service.js';
|
||||
import { ProviderService } from '../agent/provider.service.js';
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { randomBytes, createHash } from 'node:crypto';
|
||||
import { eq, type Db, adminTokens } from '@mosaicstack/db';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { AdminGuard } from './admin.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
import type {
|
||||
CreateTokenDto,
|
||||
TokenCreatedDto,
|
||||
TokenDto,
|
||||
TokenListDto,
|
||||
} from './admin-tokens.dto.js';
|
||||
|
||||
function hashToken(plaintext: string): string {
|
||||
return createHash('sha256').update(plaintext).digest('hex');
|
||||
}
|
||||
|
||||
function toTokenDto(row: typeof adminTokens.$inferSelect): TokenDto {
|
||||
return {
|
||||
id: row.id,
|
||||
label: row.label,
|
||||
scope: row.scope,
|
||||
expiresAt: row.expiresAt?.toISOString() ?? null,
|
||||
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@Controller('api/admin/tokens')
|
||||
@UseGuards(AdminGuard)
|
||||
export class AdminTokensController {
|
||||
constructor(@Inject(DB) private readonly db: Db) {}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body() dto: CreateTokenDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
): Promise<TokenCreatedDto> {
|
||||
const plaintext = randomBytes(32).toString('hex');
|
||||
const tokenHash = hashToken(plaintext);
|
||||
const id = uuid();
|
||||
|
||||
const expiresAt = dto.expiresInDays
|
||||
? new Date(Date.now() + dto.expiresInDays * 24 * 60 * 60 * 1000)
|
||||
: null;
|
||||
|
||||
const [row] = await this.db
|
||||
.insert(adminTokens)
|
||||
.values({
|
||||
id,
|
||||
userId: user.id,
|
||||
tokenHash,
|
||||
label: dto.label ?? 'CLI token',
|
||||
scope: dto.scope ?? 'admin',
|
||||
expiresAt,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return { ...toTokenDto(row!), plaintext };
|
||||
}
|
||||
|
||||
@Get()
|
||||
async list(@CurrentUser() user: { id: string }): Promise<TokenListDto> {
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(adminTokens)
|
||||
.where(eq(adminTokens.userId, user.id))
|
||||
.orderBy(adminTokens.createdAt);
|
||||
|
||||
return { tokens: rows.map(toTokenDto), total: rows.length };
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async revoke(@Param('id') id: string, @CurrentUser() _user: { id: string }): Promise<void> {
|
||||
await this.db.delete(adminTokens).where(eq(adminTokens.id, id));
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { IsString, IsOptional, IsInt, Min } from 'class-validator';
|
||||
|
||||
export class CreateTokenDto {
|
||||
@IsString()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
scope?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
expiresInDays?: number;
|
||||
}
|
||||
|
||||
export interface TokenDto {
|
||||
id: string;
|
||||
label: string;
|
||||
scope: string;
|
||||
expiresAt: string | null;
|
||||
lastUsedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface TokenCreatedDto extends TokenDto {
|
||||
plaintext: string;
|
||||
}
|
||||
|
||||
export interface TokenListDto {
|
||||
tokens: TokenDto[];
|
||||
total: number;
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { eq, type Db, users as usersTable } from '@mosaicstack/db';
|
||||
import type { Auth } from '@mosaicstack/auth';
|
||||
import { eq, type Db, users as usersTable } from '@mosaic/db';
|
||||
import type { Auth } from '@mosaic/auth';
|
||||
import { AUTH } from '../auth/auth.tokens.js';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { AdminGuard } from './admin.guard.js';
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { fromNodeHeaders } from 'better-auth/node';
|
||||
import type { Auth } from '@mosaicstack/auth';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { eq, adminTokens, users as usersTable } from '@mosaicstack/db';
|
||||
import type { Auth } from '@mosaic/auth';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { eq, users as usersTable } from '@mosaic/db';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { AUTH } from '../auth/auth.tokens.js';
|
||||
import { DB } from '../database/database.module.js';
|
||||
@@ -20,8 +19,6 @@ interface UserWithRole {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
type AuthenticatedRequest = FastifyRequest & { user: unknown; session: unknown };
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard implements CanActivate {
|
||||
constructor(
|
||||
@@ -31,64 +28,8 @@ export class AdminGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
|
||||
// Try bearer token auth first
|
||||
const authHeader = request.raw.headers['authorization'];
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
return this.validateBearerToken(request, authHeader.slice(7));
|
||||
}
|
||||
|
||||
// Fall back to BetterAuth session
|
||||
return this.validateSession(request);
|
||||
}
|
||||
|
||||
private async validateBearerToken(request: FastifyRequest, plaintext: string): Promise<boolean> {
|
||||
const tokenHash = createHash('sha256').update(plaintext).digest('hex');
|
||||
|
||||
const [row] = await this.db
|
||||
.select({
|
||||
tokenId: adminTokens.id,
|
||||
userId: adminTokens.userId,
|
||||
scope: adminTokens.scope,
|
||||
expiresAt: adminTokens.expiresAt,
|
||||
userName: usersTable.name,
|
||||
userEmail: usersTable.email,
|
||||
userRole: usersTable.role,
|
||||
})
|
||||
.from(adminTokens)
|
||||
.innerJoin(usersTable, eq(adminTokens.userId, usersTable.id))
|
||||
.where(eq(adminTokens.tokenHash, tokenHash))
|
||||
.limit(1);
|
||||
|
||||
if (!row) {
|
||||
throw new UnauthorizedException('Invalid API token');
|
||||
}
|
||||
|
||||
if (row.expiresAt && row.expiresAt < new Date()) {
|
||||
throw new UnauthorizedException('API token expired');
|
||||
}
|
||||
|
||||
if (row.userRole !== 'admin') {
|
||||
throw new ForbiddenException('Admin access required');
|
||||
}
|
||||
|
||||
// Update last-used timestamp (fire-and-forget)
|
||||
this.db
|
||||
.update(adminTokens)
|
||||
.set({ lastUsedAt: new Date() })
|
||||
.where(eq(adminTokens.id, row.tokenId))
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
const req = request as AuthenticatedRequest;
|
||||
req.user = { id: row.userId, name: row.userName, email: row.userEmail, role: row.userRole };
|
||||
req.session = { id: `token:${row.tokenId}`, userId: row.userId };
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async validateSession(request: FastifyRequest): Promise<boolean> {
|
||||
const headers = fromNodeHeaders(request.raw.headers);
|
||||
|
||||
const result = await this.auth.api.getSession({ headers });
|
||||
|
||||
if (!result) {
|
||||
@@ -97,6 +38,8 @@ export class AdminGuard implements CanActivate {
|
||||
|
||||
const user = result.user as UserWithRole;
|
||||
|
||||
// Ensure the role field is populated. better-auth should include additionalFields
|
||||
// in the session, but as a fallback, fetch the role from the database if needed.
|
||||
let userRole = user.role;
|
||||
if (!userRole) {
|
||||
const [dbUser] = await this.db
|
||||
@@ -105,6 +48,7 @@ export class AdminGuard implements CanActivate {
|
||||
.where(eq(usersTable.id, user.id))
|
||||
.limit(1);
|
||||
userRole = dbUser?.role ?? 'member';
|
||||
// Update the session user object with the fetched role
|
||||
(user as UserWithRole).role = userRole;
|
||||
}
|
||||
|
||||
@@ -112,9 +56,8 @@ export class AdminGuard implements CanActivate {
|
||||
throw new ForbiddenException('Admin access required');
|
||||
}
|
||||
|
||||
const req = request as AuthenticatedRequest;
|
||||
req.user = result.user;
|
||||
req.session = result.session;
|
||||
(request as FastifyRequest & { user: unknown; session: unknown }).user = result.user;
|
||||
(request as FastifyRequest & { user: unknown; session: unknown }).session = result.session;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { AdminController } from './admin.controller.js';
|
||||
import { AdminHealthController } from './admin-health.controller.js';
|
||||
import { AdminJobsController } from './admin-jobs.controller.js';
|
||||
import { AdminTokensController } from './admin-tokens.controller.js';
|
||||
import { BootstrapController } from './bootstrap.controller.js';
|
||||
import { AdminGuard } from './admin.guard.js';
|
||||
|
||||
@Module({
|
||||
controllers: [
|
||||
AdminController,
|
||||
AdminHealthController,
|
||||
AdminJobsController,
|
||||
AdminTokensController,
|
||||
BootstrapController,
|
||||
],
|
||||
controllers: [AdminController, AdminHealthController, AdminJobsController],
|
||||
providers: [AdminGuard],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Inject,
|
||||
InternalServerErrorException,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { randomBytes, createHash } from 'node:crypto';
|
||||
import { count, eq, type Db, users as usersTable, adminTokens } from '@mosaicstack/db';
|
||||
import type { Auth } from '@mosaicstack/auth';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { AUTH } from '../auth/auth.tokens.js';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import type { BootstrapSetupDto, BootstrapStatusDto, BootstrapResultDto } from './bootstrap.dto.js';
|
||||
|
||||
@Controller('api/bootstrap')
|
||||
export class BootstrapController {
|
||||
constructor(
|
||||
@Inject(AUTH) private readonly auth: Auth,
|
||||
@Inject(DB) private readonly db: Db,
|
||||
) {}
|
||||
|
||||
@Get('status')
|
||||
async status(): Promise<BootstrapStatusDto> {
|
||||
const [result] = await this.db.select({ total: count() }).from(usersTable);
|
||||
return { needsSetup: (result?.total ?? 0) === 0 };
|
||||
}
|
||||
|
||||
@Post('setup')
|
||||
async setup(@Body() dto: BootstrapSetupDto): Promise<BootstrapResultDto> {
|
||||
// Only allow setup when zero users exist
|
||||
const [result] = await this.db.select({ total: count() }).from(usersTable);
|
||||
if ((result?.total ?? 0) > 0) {
|
||||
throw new ForbiddenException('Setup already completed — users exist');
|
||||
}
|
||||
|
||||
// Create admin user via BetterAuth API
|
||||
const authApi = this.auth.api as unknown as {
|
||||
createUser: (opts: {
|
||||
body: { name: string; email: string; password: string; role?: string };
|
||||
}) => Promise<{
|
||||
user: { id: string; name: string; email: string };
|
||||
}>;
|
||||
};
|
||||
|
||||
const created = await authApi.createUser({
|
||||
body: {
|
||||
name: dto.name,
|
||||
email: dto.email,
|
||||
password: dto.password,
|
||||
role: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
// Verify user was created
|
||||
const [user] = await this.db
|
||||
.select()
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.id, created.user.id))
|
||||
.limit(1);
|
||||
|
||||
if (!user) throw new InternalServerErrorException('User created but not found');
|
||||
|
||||
// Ensure role is admin (createUser may not set it via BetterAuth)
|
||||
if (user.role !== 'admin') {
|
||||
await this.db.update(usersTable).set({ role: 'admin' }).where(eq(usersTable.id, user.id));
|
||||
}
|
||||
|
||||
// Generate admin API token
|
||||
const plaintext = randomBytes(32).toString('hex');
|
||||
const tokenHash = createHash('sha256').update(plaintext).digest('hex');
|
||||
const tokenId = uuid();
|
||||
|
||||
const [token] = await this.db
|
||||
.insert(adminTokens)
|
||||
.values({
|
||||
id: tokenId,
|
||||
userId: user.id,
|
||||
tokenHash,
|
||||
label: 'Initial setup token',
|
||||
scope: 'admin',
|
||||
})
|
||||
.returning();
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: 'admin',
|
||||
},
|
||||
token: {
|
||||
id: token!.id,
|
||||
plaintext,
|
||||
label: token!.label,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { IsString, IsEmail, MinLength } from 'class-validator';
|
||||
|
||||
export class BootstrapSetupDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password!: string;
|
||||
}
|
||||
|
||||
export interface BootstrapStatusDto {
|
||||
needsSetup: boolean;
|
||||
}
|
||||
|
||||
export interface BootstrapResultDto {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
token: {
|
||||
id: string;
|
||||
plaintext: string;
|
||||
label: string;
|
||||
};
|
||||
}
|
||||
@@ -62,7 +62,7 @@ function restoreEnv(saved: Map<EnvKey, string | undefined>): void {
|
||||
}
|
||||
|
||||
function makeRegistry(): ModelRegistry {
|
||||
return ModelRegistry.inMemory(AuthStorage.inMemory());
|
||||
return new ModelRegistry(AuthStorage.inMemory());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { RoutingService } from '../routing.service.js';
|
||||
import type { ModelInfo } from '@mosaicstack/types';
|
||||
import type { ModelInfo } from '@mosaic/types';
|
||||
|
||||
const mockModels: ModelInfo[] = [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
IProviderAdapter,
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
} from '@mosaicstack/types';
|
||||
} from '@mosaic/types';
|
||||
|
||||
/**
|
||||
* Anthropic provider adapter.
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
IProviderAdapter,
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
} from '@mosaicstack/types';
|
||||
} from '@mosaic/types';
|
||||
|
||||
/** Embedding models that Ollama ships with out of the box */
|
||||
const OLLAMA_EMBEDDING_MODELS: ReadonlyArray<{
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
IProviderAdapter,
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
} from '@mosaicstack/types';
|
||||
} from '@mosaic/types';
|
||||
|
||||
/**
|
||||
* OpenAI provider adapter.
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
IProviderAdapter,
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
} from '@mosaicstack/types';
|
||||
} from '@mosaic/types';
|
||||
|
||||
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
IProviderAdapter,
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
} from '@mosaicstack/types';
|
||||
} from '@mosaic/types';
|
||||
import { getModelCapability } from '../model-capabilities.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
type AgentSessionEvent,
|
||||
type ToolDefinition,
|
||||
} from '@mariozechner/pi-coding-agent';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Memory } from '@mosaicstack/memory';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { MEMORY } from '../memory/memory.tokens.js';
|
||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ModelCapability } from '@mosaicstack/types';
|
||||
import type { ModelCapability } from '@mosaic/types';
|
||||
|
||||
/**
|
||||
* Comprehensive capability matrix for all target models.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { providerCredentials, eq, and } from '@mosaicstack/db';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { providerCredentials, eq, and } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import type { ProviderCredentialSummaryDto } from './provider-credentials.dto.js';
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
ProviderInfo,
|
||||
} from '@mosaicstack/types';
|
||||
} from '@mosaic/types';
|
||||
import {
|
||||
AnthropicAdapter,
|
||||
OllamaAdapter,
|
||||
@@ -67,7 +67,7 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
this.registry = ModelRegistry.inMemory(authStorage);
|
||||
this.registry = new ModelRegistry(authStorage);
|
||||
|
||||
// Build the default set of adapters that rely on the registry
|
||||
this.adapters = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import type { RoutingCriteria } from '@mosaicstack/types';
|
||||
import type { RoutingCriteria } from '@mosaic/types';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import type { ModelInfo } from '@mosaicstack/types';
|
||||
import type { RoutingCriteria, RoutingResult, CostTier } from '@mosaicstack/types';
|
||||
import type { ModelInfo } from '@mosaic/types';
|
||||
import type { RoutingCriteria, RoutingResult, CostTier } from '@mosaic/types';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
|
||||
/** Per-million-token cost thresholds for tier classification */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { routingRules, type Db, sql } from '@mosaicstack/db';
|
||||
import { routingRules, type Db, sql } from '@mosaic/db';
|
||||
import { DB } from '../../database/database.module.js';
|
||||
import type { RoutingCondition, RoutingAction } from './routing.types.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { routingRules, type Db, and, asc, eq, or } from '@mosaicstack/db';
|
||||
import { routingRules, type Db, and, asc, eq, or } from '@mosaic/db';
|
||||
import { DB } from '../../database/database.module.js';
|
||||
import { ProviderService } from '../provider.service.js';
|
||||
import { classifyTask } from './task-classifier.js';
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaicstack/db';
|
||||
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaic/db';
|
||||
import { DB } from '../../database/database.module.js';
|
||||
import { AuthGuard } from '../../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../../auth/current-user.decorator.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Routing engine types — M4-002 (condition types) and M4-003 (action types).
|
||||
*
|
||||
* These types are re-exported from `@mosaicstack/types` for shared use across packages.
|
||||
* These types are re-exported from `@mosaic/types` for shared use across packages.
|
||||
*/
|
||||
|
||||
// ─── Classification primitives ───────────────────────────────────────────────
|
||||
@@ -23,7 +23,7 @@ export type Domain = 'frontend' | 'backend' | 'devops' | 'docs' | 'general';
|
||||
|
||||
/**
|
||||
* Cost tier for model selection.
|
||||
* Extends the existing `CostTier` in `@mosaicstack/types` with `local` for self-hosted models.
|
||||
* Extends the existing `CostTier` in `@mosaic/types` with `local` for self-hosted models.
|
||||
*/
|
||||
export type CostTier = 'cheap' | 'standard' | 'premium' | 'local';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
|
||||
export function createBrainTools(brain: Brain): ToolDefinition[] {
|
||||
const listProjects: ToolDefinition = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import type { Memory } from '@mosaicstack/memory';
|
||||
import type { EmbeddingProvider } from '@mosaicstack/memory';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import type { EmbeddingProvider } from '@mosaic/memory';
|
||||
|
||||
/**
|
||||
* Create memory tools bound to the session's authenticated userId.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { HealthController } from './health/health.controller.js';
|
||||
import { ConfigModule } from './config/config.module.js';
|
||||
import { DatabaseModule } from './database/database.module.js';
|
||||
import { AuthModule } from './auth/auth.module.js';
|
||||
import { BrainModule } from './brain/brain.module.js';
|
||||
@@ -29,7 +28,6 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
@Module({
|
||||
imports: [
|
||||
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
|
||||
ConfigModule,
|
||||
DatabaseModule,
|
||||
AuthModule,
|
||||
BrainModule,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { toNodeHandler } from 'better-auth/node';
|
||||
import type { Auth } from '@mosaicstack/auth';
|
||||
import type { Auth } from '@mosaic/auth';
|
||||
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { AUTH } from './auth.tokens.js';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { fromNodeHeaders } from 'better-auth/node';
|
||||
import type { Auth } from '@mosaicstack/auth';
|
||||
import type { Auth } from '@mosaic/auth';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { AUTH } from './auth.tokens.js';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { createAuth, type Auth } from '@mosaicstack/auth';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { createAuth, type Auth } from '@mosaic/auth';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { AUTH } from './auth.tokens.js';
|
||||
import { SsoController } from './sso.controller.js';
|
||||
@@ -14,7 +14,7 @@ import { SsoController } from './sso.controller.js';
|
||||
useFactory: (db: Db): Auth =>
|
||||
createAuth({
|
||||
db,
|
||||
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242',
|
||||
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:4000',
|
||||
secret: process.env['BETTER_AUTH_SECRET'],
|
||||
}),
|
||||
inject: [DB],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { buildSsoDiscovery, type SsoProviderDiscovery } from '@mosaicstack/auth';
|
||||
import { buildSsoDiscovery, type SsoProviderDiscovery } from '@mosaic/auth';
|
||||
|
||||
@Controller('api/sso/providers')
|
||||
export class SsoController {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { createBrain, type Brain } from '@mosaicstack/brain';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { createBrain, type Brain } from '@mosaic/brain';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { BRAIN } from './brain.tokens.js';
|
||||
|
||||
|
||||
@@ -11,15 +11,15 @@ import {
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
||||
import type { Auth } from '@mosaicstack/auth';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Auth } from '@mosaic/auth';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import type {
|
||||
SetThinkingPayload,
|
||||
SlashCommandPayload,
|
||||
SystemReloadPayload,
|
||||
RoutingDecisionInfo,
|
||||
AbortPayload,
|
||||
} from '@mosaicstack/types';
|
||||
} from '@mosaic/types';
|
||||
import { AgentService, type ConversationHistoryMessage } from '../agent/agent.service.js';
|
||||
import { AUTH } from '../auth/auth.tokens.js';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CommandExecutorService } from './command-executor.service.js';
|
||||
import type { SlashCommandPayload } from '@mosaicstack/types';
|
||||
import type { SlashCommandPayload } from '@mosaic/types';
|
||||
|
||||
// Minimal mock implementations
|
||||
const mockRegistry = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef, Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
||||
import type { QueueHandle } from '@mosaicstack/queue';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaicstack/types';
|
||||
import type { QueueHandle } from '@mosaic/queue';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaic/types';
|
||||
import { AgentService } from '../agent/agent.service.js';
|
||||
import { ChatGateway } from '../chat/chat.gateway.js';
|
||||
import { SessionGCService } from '../gc/session-gc.service.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CommandRegistryService } from './command-registry.service.js';
|
||||
import type { CommandDef } from '@mosaicstack/types';
|
||||
import type { CommandDef } from '@mosaic/types';
|
||||
|
||||
const mockCmd: CommandDef = {
|
||||
name: 'test',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, type OnModuleInit } from '@nestjs/common';
|
||||
import type { CommandDef, CommandManifest } from '@mosaicstack/types';
|
||||
import type { CommandDef, CommandManifest } from '@mosaic/types';
|
||||
|
||||
@Injectable()
|
||||
export class CommandRegistryService implements OnModuleInit {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CommandRegistryService } from './command-registry.service.js';
|
||||
import { CommandExecutorService } from './command-executor.service.js';
|
||||
import type { SlashCommandPayload } from '@mosaicstack/types';
|
||||
import type { SlashCommandPayload } from '@mosaic/types';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
|
||||
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
||||
import { createQueue, type QueueHandle } from '@mosaic/queue';
|
||||
import { ChatModule } from '../chat/chat.module.js';
|
||||
import { GCModule } from '../gc/gc.module.js';
|
||||
import { ReloadModule } from '../reload/reload.module.js';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { loadConfig, type MosaicConfig } from '@mosaicstack/config';
|
||||
|
||||
export const MOSAIC_CONFIG = 'MOSAIC_CONFIG';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: MOSAIC_CONFIG,
|
||||
useFactory: (): MosaicConfig => loadConfig(),
|
||||
},
|
||||
],
|
||||
exports: [MOSAIC_CONFIG],
|
||||
})
|
||||
export class ConfigModule {}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type MissionStatusSummary,
|
||||
type MissionTask,
|
||||
type TaskDetail,
|
||||
} from '@mosaicstack/coord';
|
||||
} from '@mosaic/coord';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
|
||||
@@ -1,51 +1,28 @@
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Global, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
|
||||
import { createDb, createPgliteDb, type Db, type DbHandle } from '@mosaicstack/db';
|
||||
import { createStorageAdapter, type StorageAdapter } from '@mosaicstack/storage';
|
||||
import type { MosaicConfig } from '@mosaicstack/config';
|
||||
import { MOSAIC_CONFIG } from '../config/config.module.js';
|
||||
import { createDb, type Db, type DbHandle } from '@mosaic/db';
|
||||
|
||||
export const DB_HANDLE = 'DB_HANDLE';
|
||||
export const DB = 'DB';
|
||||
export const STORAGE_ADAPTER = 'STORAGE_ADAPTER';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: DB_HANDLE,
|
||||
useFactory: (config: MosaicConfig): DbHandle => {
|
||||
if (config.tier === 'local') {
|
||||
const dataDir = join(homedir(), '.config', 'mosaic', 'gateway', 'pglite');
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
return createPgliteDb(dataDir);
|
||||
}
|
||||
return createDb(config.storage.type === 'postgres' ? config.storage.url : undefined);
|
||||
},
|
||||
inject: [MOSAIC_CONFIG],
|
||||
useFactory: (): DbHandle => createDb(),
|
||||
},
|
||||
{
|
||||
provide: DB,
|
||||
useFactory: (handle: DbHandle): Db => handle.db,
|
||||
inject: [DB_HANDLE],
|
||||
},
|
||||
{
|
||||
provide: STORAGE_ADAPTER,
|
||||
useFactory: (config: MosaicConfig): StorageAdapter => createStorageAdapter(config.storage),
|
||||
inject: [MOSAIC_CONFIG],
|
||||
},
|
||||
],
|
||||
exports: [DB, STORAGE_ADAPTER],
|
||||
exports: [DB],
|
||||
})
|
||||
export class DatabaseModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DB_HANDLE) private readonly handle: DbHandle,
|
||||
@Inject(STORAGE_ADAPTER) private readonly storageAdapter: StorageAdapter,
|
||||
) {}
|
||||
constructor(@Inject(DB_HANDLE) private readonly handle: DbHandle) {}
|
||||
|
||||
async onApplicationShutdown(): Promise<void> {
|
||||
await Promise.all([this.handle.close(), this.storageAdapter.close()]);
|
||||
await this.handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module, type OnApplicationShutdown, Inject } from '@nestjs/common';
|
||||
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
||||
import { createQueue, type QueueHandle } from '@mosaic/queue';
|
||||
import { SessionGCService } from './session-gc.service.js';
|
||||
import { REDIS } from './gc.tokens.js';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { QueueHandle } from '@mosaicstack/queue';
|
||||
import type { LogService } from '@mosaicstack/log';
|
||||
import type { QueueHandle } from '@mosaic/queue';
|
||||
import type { LogService } from '@mosaic/log';
|
||||
import { SessionGCService } from './session-gc.service.js';
|
||||
|
||||
type MockRedis = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import type { QueueHandle } from '@mosaicstack/queue';
|
||||
import type { LogService } from '@mosaicstack/log';
|
||||
import type { QueueHandle } from '@mosaic/queue';
|
||||
import type { LogService } from '@mosaic/log';
|
||||
import { LOG_SERVICE } from '../log/log.tokens.js';
|
||||
import { REDIS } from './gc.tokens.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Body, Controller, Get, Inject, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import type { LogService } from '@mosaicstack/log';
|
||||
import type { LogService } from '@mosaic/log';
|
||||
import { LOG_SERVICE } from './log.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import type { IngestLogDto, QueryLogsDto } from './log.dto.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { createLogService, type LogService } from '@mosaicstack/log';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { createLogService, type LogService } from '@mosaic/log';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { LOG_SERVICE } from './log.tokens.js';
|
||||
import { LogController } from './log.controller.js';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import type { LogService } from '@mosaicstack/log';
|
||||
import type { Memory } from '@mosaicstack/memory';
|
||||
import type { LogService } from '@mosaic/log';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import { LOG_SERVICE } from './log.tokens.js';
|
||||
import { MEMORY } from '../memory/memory.tokens.js';
|
||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { sql, summarizationJobs } from '@mosaicstack/db';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { sql, summarizationJobs } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
const SUMMARIZATION_PROMPT = `You are a knowledge extraction assistant. Given the following agent interaction logs, extract the key decisions, learnings, and patterns. Output a concise summary (2-4 sentences) that captures the most important information for future reference. Focus on actionable insights, not raw events.
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { config } from 'dotenv';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
// Load .env from daemon config dir (global install / daemon mode).
|
||||
// Loaded first so monorepo .env can override for local dev.
|
||||
const daemonEnv = join(homedir(), '.config', 'mosaic', 'gateway', '.env');
|
||||
if (existsSync(daemonEnv)) config({ path: daemonEnv });
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// Load .env from monorepo root (cwd is apps/gateway when run via pnpm filter)
|
||||
config({ path: resolve(process.cwd(), '../../.env') });
|
||||
@@ -19,7 +11,7 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import helmet from '@fastify/helmet';
|
||||
import { listSsoStartupWarnings } from '@mosaicstack/auth';
|
||||
import { listSsoStartupWarnings } from '@mosaic/auth';
|
||||
import { AppModule } from './app.module.js';
|
||||
import { mountAuthHandler } from './auth/auth.controller.js';
|
||||
import { mountMcpHandler } from './mcp/mcp.controller.js';
|
||||
@@ -59,7 +51,7 @@ async function bootstrap(): Promise<void> {
|
||||
mountAuthHandler(app);
|
||||
mountMcpHandler(app, app.get(McpService));
|
||||
|
||||
const port = Number(process.env['GATEWAY_PORT'] ?? 14242);
|
||||
const port = Number(process.env['GATEWAY_PORT'] ?? 4000);
|
||||
await app.listen(port, '0.0.0.0');
|
||||
logger.log(`Gateway listening on port ${port}`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { fromNodeHeaders } from 'better-auth/node';
|
||||
import type { Auth } from '@mosaicstack/auth';
|
||||
import type { Auth } from '@mosaic/auth';
|
||||
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import type { McpService } from './mcp.service.js';
|
||||
import { AUTH } from '../auth/auth.tokens.js';
|
||||
|
||||
@@ -3,8 +3,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Memory } from '@mosaicstack/memory';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { MEMORY } from '../memory/memory.tokens.js';
|
||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import type { EmbeddingProvider } from '@mosaicstack/memory';
|
||||
import type { EmbeddingProvider } from '@mosaic/memory';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Environment-driven configuration
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Memory } from '@mosaicstack/memory';
|
||||
import type { Memory } from '@mosaic/memory';
|
||||
import { MEMORY } from './memory.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import {
|
||||
createMemory,
|
||||
type Memory,
|
||||
createMemoryAdapter,
|
||||
type MemoryAdapter,
|
||||
type MemoryConfig,
|
||||
} from '@mosaicstack/memory';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import type { StorageAdapter } from '@mosaicstack/storage';
|
||||
import type { MosaicConfig } from '@mosaicstack/config';
|
||||
import { MOSAIC_CONFIG } from '../config/config.module.js';
|
||||
import { DB, STORAGE_ADAPTER } from '../database/database.module.js';
|
||||
import { createMemory, type Memory } from '@mosaic/memory';
|
||||
import type { Db } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { MEMORY } from './memory.tokens.js';
|
||||
import { MemoryController } from './memory.controller.js';
|
||||
import { EmbeddingService } from './embedding.service.js';
|
||||
|
||||
export const MEMORY_ADAPTER = 'MEMORY_ADAPTER';
|
||||
|
||||
function buildMemoryConfig(config: MosaicConfig, storageAdapter: StorageAdapter): MemoryConfig {
|
||||
if (config.memory.type === 'keyword') {
|
||||
return { type: 'keyword', storage: storageAdapter };
|
||||
}
|
||||
return { type: config.memory.type };
|
||||
}
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
@@ -32,15 +14,9 @@ function buildMemoryConfig(config: MosaicConfig, storageAdapter: StorageAdapter)
|
||||
useFactory: (db: Db): Memory => createMemory(db),
|
||||
inject: [DB],
|
||||
},
|
||||
{
|
||||
provide: MEMORY_ADAPTER,
|
||||
useFactory: (config: MosaicConfig, storageAdapter: StorageAdapter): MemoryAdapter =>
|
||||
createMemoryAdapter(buildMemoryConfig(config, storageAdapter)),
|
||||
inject: [MOSAIC_CONFIG, STORAGE_ADAPTER],
|
||||
},
|
||||
EmbeddingService,
|
||||
],
|
||||
controllers: [MemoryController],
|
||||
exports: [MEMORY, MEMORY_ADAPTER, EmbeddingService],
|
||||
exports: [MEMORY, EmbeddingService],
|
||||
})
|
||||
export class MemoryModule {}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
type OnModuleDestroy,
|
||||
type OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { DiscordPlugin } from '@mosaicstack/discord-plugin';
|
||||
import { TelegramPlugin } from '@mosaicstack/telegram-plugin';
|
||||
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';
|
||||
import { PLUGIN_REGISTRY } from './plugin.tokens.js';
|
||||
@@ -48,7 +48,7 @@ class TelegramChannelPluginAdapter implements IChannelPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_GATEWAY_URL = 'http://localhost:14242';
|
||||
const DEFAULT_GATEWAY_URL = 'http://localhost:4000';
|
||||
|
||||
function createPluginRegistry(): IChannelPlugin[] {
|
||||
const plugins: IChannelPlugin[] = [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { PreferencesService, PLATFORM_DEFAULTS, IMMUTABLE_KEYS } from './preferences.service.js';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import type { Db } from '@mosaic/db';
|
||||
|
||||
/**
|
||||
* Build a mock Drizzle DB where the select chain supports:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { eq, and, sql, type Db, preferences as preferencesTable } from '@mosaicstack/db';
|
||||
import { eq, and, sql, type Db, preferences as preferencesTable } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
export const PLATFORM_DEFAULTS: Record<string, unknown> = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
||||
import { createQueue, type QueueHandle } from '@mosaic/queue';
|
||||
|
||||
const SESSION_SYSTEM_KEY = (sessionId: string) => `mosaic:session:${sessionId}:system`;
|
||||
const SESSION_SYSTEM_FRAGMENTS_KEY = (sessionId: string) =>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { createQueueAdapter, type QueueAdapter } from '@mosaicstack/queue';
|
||||
import type { MosaicConfig } from '@mosaicstack/config';
|
||||
import { MOSAIC_CONFIG } from '../config/config.module.js';
|
||||
import { QueueService } from './queue.service.js';
|
||||
|
||||
export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
QueueService,
|
||||
{
|
||||
provide: QUEUE_ADAPTER,
|
||||
useFactory: (config: MosaicConfig): QueueAdapter => createQueueAdapter(config.queue),
|
||||
inject: [MOSAIC_CONFIG],
|
||||
},
|
||||
],
|
||||
exports: [QueueService, QUEUE_ADAPTER],
|
||||
providers: [QueueService],
|
||||
exports: [QueueService],
|
||||
})
|
||||
export class QueueModule {}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import { Queue, Worker, type Job, type ConnectionOptions } from 'bullmq';
|
||||
import type { LogService } from '@mosaicstack/log';
|
||||
import type { LogService } from '@mosaic/log';
|
||||
import { LOG_SERVICE } from '../log/log.tokens.js';
|
||||
import type { JobDto, JobStatus } from './queue-admin.dto.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, HttpCode, HttpStatus, Inject, Post, UseGuards } from '@nestjs/common';
|
||||
import type { SystemReloadPayload } from '@mosaicstack/types';
|
||||
import type { SystemReloadPayload } from '@mosaic/types';
|
||||
import { AdminGuard } from '../admin/admin.guard.js';
|
||||
import { ChatGateway } from '../chat/chat.gateway.js';
|
||||
import { ReloadService } from './reload.service.js';
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type OnApplicationBootstrap,
|
||||
type OnApplicationShutdown,
|
||||
} from '@nestjs/common';
|
||||
import type { SystemReloadPayload } from '@mosaicstack/types';
|
||||
import type { SystemReloadPayload } from '@mosaic/types';
|
||||
import { CommandRegistryService } from '../commands/command-registry.service.js';
|
||||
import { isMosaicPlugin } from './mosaic-plugin.interface.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { eq, type Db, skills } from '@mosaicstack/db';
|
||||
import { eq, type Db, skills } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
type Skill = typeof skills.$inferSelect;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import type { Brain } from '@mosaicstack/brain';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { PluginService } from '../plugin/plugin.service.js';
|
||||
import { WorkspaceService } from './workspace.service.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaicstack/db';
|
||||
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
"rootDir": "../..",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@mosaicstack/auth": ["../../packages/auth/src/index.ts"],
|
||||
"@mosaicstack/brain": ["../../packages/brain/src/index.ts"],
|
||||
"@mosaicstack/coord": ["../../packages/coord/src/index.ts"],
|
||||
"@mosaicstack/db": ["../../packages/db/src/index.ts"],
|
||||
"@mosaicstack/log": ["../../packages/log/src/index.ts"],
|
||||
"@mosaicstack/memory": ["../../packages/memory/src/index.ts"],
|
||||
"@mosaicstack/types": ["../../packages/types/src/index.ts"],
|
||||
"@mosaicstack/discord-plugin": ["../../plugins/discord/src/index.ts"],
|
||||
"@mosaicstack/telegram-plugin": ["../../plugins/telegram/src/index.ts"]
|
||||
"@mosaic/auth": ["../../packages/auth/src/index.ts"],
|
||||
"@mosaic/brain": ["../../packages/brain/src/index.ts"],
|
||||
"@mosaic/coord": ["../../packages/coord/src/index.ts"],
|
||||
"@mosaic/db": ["../../packages/db/src/index.ts"],
|
||||
"@mosaic/log": ["../../packages/log/src/index.ts"],
|
||||
"@mosaic/memory": ["../../packages/memory/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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
transpilePackages: ['@mosaicstack/design-tokens'],
|
||||
transpilePackages: ['@mosaic/design-tokens'],
|
||||
|
||||
// Enable gzip/brotli compression for all responses.
|
||||
compress: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@mosaicstack/web",
|
||||
"name": "@mosaic/web",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -12,7 +12,7 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/design-tokens": "workspace:^",
|
||||
"@mosaic/design-tokens": "workspace:^",
|
||||
"better-auth": "^1.5.5",
|
||||
"clsx": "^2.1.0",
|
||||
"next": "^16.0.0",
|
||||
|
||||
@@ -5,9 +5,9 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
*
|
||||
* Assumes:
|
||||
* - Next.js web app running on http://localhost:3000
|
||||
* - NestJS gateway running on http://localhost:14242
|
||||
* - NestJS gateway running on http://localhost:4000
|
||||
*
|
||||
* Run with: pnpm --filter @mosaicstack/web test:e2e
|
||||
* Run with: pnpm --filter @mosaic/web test:e2e
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
|
||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
||||
|
||||
export interface ApiRequestInit extends Omit<RequestInit, 'body'> {
|
||||
body?: unknown;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createAuthClient } from 'better-auth/react';
|
||||
import { adminClient, genericOAuthClient } from 'better-auth/client/plugins';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242',
|
||||
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000',
|
||||
plugins: [adminClient(), genericOAuthClient()],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
|
||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
|
||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
||||
|
||||
let socket: Socket | null = null;
|
||||
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
# Mission Manifest — CLI Unification & E2E First-Run
|
||||
# Mission Manifest — Harness Foundation
|
||||
|
||||
> Persistent document tracking full mission scope, status, and session history.
|
||||
> Updated by the orchestrator at each phase transition and milestone completion.
|
||||
|
||||
## Mission
|
||||
|
||||
**ID:** cli-unification-20260404
|
||||
**Statement:** Transform the Mosaic CLI from a partially-duplicated, manually-assembled experience into a single cohesive entry point that installs, configures, and controls the entire Mosaic system. Every Mosaic package gets first-class CLI surface. The first-run experience works end-to-end with no manual stitching. Gateway token recovery is possible without the web UI. Opt-in telemetry uses the published telemetry clients.
|
||||
**Phase:** Execution
|
||||
**Current Milestone:** cu-m03 / cu-m04 / cu-m05 (parallel-eligible)
|
||||
**Progress:** 2 / 8 milestones
|
||||
**Status:** active
|
||||
**Last Updated:** 2026-04-04
|
||||
**ID:** harness-20260321
|
||||
**Statement:** Transform Mosaic Stack from a functional demo into a real multi-provider, task-routing AI harness. Persist all conversations, integrate frontier LLM providers (Anthropic, OpenAI, OpenRouter, Z.ai, Ollama), build granular task-aware agent routing, harden agent sessions, replace cron with BullMQ, and design the channel protocol for future Matrix/remote integration.
|
||||
**Phase:** Complete
|
||||
**Current Milestone:** All milestones done
|
||||
**Progress:** 7 / 7 milestones
|
||||
**Status:** complete
|
||||
**Last Updated:** 2026-03-22 UTC
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] AC-1: Fresh machine `bash <(curl …install.sh)` → single command lands on a working authenticated gateway with a usable admin token; no secondary manual wizards required
|
||||
- [ ] AC-2: `mosaic --help` lists every sub-package as a top-level command and is alphabetized for readability
|
||||
- [ ] AC-3: `mosaic auth`, `mosaic brain`, `mosaic forge`, `mosaic log`, `mosaic macp`, `mosaic memory`, `mosaic queue`, `mosaic storage`, `mosaic telemetry` each expose at least one working subcommand that exercises the underlying package
|
||||
- [ ] AC-4: Gateway admin token can be rotated or recovered from the CLI alone — operator is never stranded because the web UI is inaccessible
|
||||
- [ ] AC-5: `mosaic telemetry` uses the published `@mosaicstack/telemetry-client-js` (from the Gitea npm registry); local OTEL stays for wide-event logging / post-mortems; remote upload is opt-in and disabled by default
|
||||
- [ ] AC-6: Install → wizard → gateway install → TUI verification flow is a single cohesive path with clear state transitions and no dead ends
|
||||
- [ ] AC-7: `@mosaicstack/mosaic` is the sole `mosaic` binary owner; `@mosaicstack/cli` is gone from the repo and all docs
|
||||
- [ ] AC-8: All milestones ship as merged PRs with green CI, closed issues, and updated release notes
|
||||
- [x] AC-1: Send messages in TUI → restart TUI → resume conversation → agent has full history and context
|
||||
- [x] AC-2: Route a coding task to Claude Opus 4.6, a simple question to Haiku, a summarization to GLM-5 — all via granular routing rules
|
||||
- [x] AC-3: Two users exist, User A's memory searches never return User B's data
|
||||
- [x] AC-4: `/model claude-sonnet-4-6` in TUI switches the active model for subsequent messages
|
||||
- [x] AC-5: `/agent coding-agent` in TUI switches to a different agent with different system prompt and tools
|
||||
- [x] AC-6: BullMQ jobs execute on schedule, failures retry with backoff, admin can inspect via `/api/admin/jobs`
|
||||
- [x] AC-7: Channel protocol document exists with Matrix integration points defined, reviewed, and approved
|
||||
- [x] AC-8: Embeddings run on Ollama local models (no external API dependency for vector operations)
|
||||
- [x] AC-9: All five providers (Anthropic, OpenAI, OpenRouter, Z.ai, Ollama) connect, list models, and complete chat requests
|
||||
- [x] AC-10: Routing transparency — TUI displays which model was selected and the routing reason for each response
|
||||
|
||||
## Milestones
|
||||
|
||||
| # | ID | Name | Status | Branch | Issue | Started | Completed |
|
||||
| --- | ------ | ------------------------------------------------------------------------ | ----------- | ---------------------------------- | ----- | ---------- | ---------- |
|
||||
| 1 | cu-m01 | Kill legacy @mosaicstack/cli package | done | chore/remove-cli-package-duplicate | #398 | 2026-04-04 | 2026-04-04 |
|
||||
| 2 | cu-m02 | Archive stale mission state + scaffold new mission | done | docs/mission-cli-unification | #399 | 2026-04-04 | 2026-04-04 |
|
||||
| 3 | cu-m03 | Fix gateway bootstrap token recovery (server + CLI paths) | not-started | — | — | — | — |
|
||||
| 4 | cu-m04 | Alphabetize + group `mosaic --help` output | not-started | — | — | — | — |
|
||||
| 5 | cu-m05 | Sub-package CLI surface (auth/brain/forge/log/macp/memory/queue/storage) | not-started | — | — | — | — |
|
||||
| 6 | cu-m06 | `mosaic telemetry` — local OTEL + opt-in remote upload | not-started | — | — | — | — |
|
||||
| 7 | cu-m07 | Unified first-run UX (install.sh → wizard → gateway → TUI) | not-started | — | — | — | — |
|
||||
| 8 | cu-m08 | Docs refresh + release tag | not-started | — | — | — | — |
|
||||
| # | ID | Name | Status | Branch | Issue | Started | Completed |
|
||||
| --- | ------ | ---------------------------------- | ------ | ------ | --------- | ---------- | ---------- |
|
||||
| 1 | ms-166 | Conversation Persistence & Context | done | — | #224–#231 | 2026-03-21 | 2026-03-21 |
|
||||
| 2 | ms-167 | Security & Isolation | done | — | #232–#239 | 2026-03-21 | 2026-03-21 |
|
||||
| 3 | ms-168 | Provider Integration | done | — | #240–#251 | 2026-03-21 | 2026-03-22 |
|
||||
| 4 | ms-169 | Agent Routing Engine | done | — | #252–#264 | 2026-03-22 | 2026-03-22 |
|
||||
| 5 | ms-170 | Agent Session Hardening | done | — | #265–#272 | 2026-03-22 | 2026-03-22 |
|
||||
| 6 | ms-171 | Job Queue Foundation | done | — | #273–#280 | 2026-03-22 | 2026-03-22 |
|
||||
| 7 | ms-172 | Channel Protocol Design | done | — | #281–#288 | 2026-03-22 | 2026-03-22 |
|
||||
|
||||
## Deployment
|
||||
|
||||
| Target | URL | Method |
|
||||
| -------------------- | --------- | ----------------------------------------------- |
|
||||
| Local tier (default) | localhost | `mosaic gateway install` — pglite + local queue |
|
||||
| Team tier | any host | `mosaic gateway install` — PG + Valkey |
|
||||
| Docker Compose (dev) | localhost | `docker compose up` for PG/Valkey/OTEL/Jaeger |
|
||||
| Target | URL | Method |
|
||||
| -------------------- | --------- | -------------------------- |
|
||||
| Docker Compose (dev) | localhost | docker compose up |
|
||||
| Production | TBD | Docker Swarm via Portainer |
|
||||
|
||||
## Coordination
|
||||
|
||||
- **Primary Agent:** claude-opus-4-6[1m]
|
||||
- **Sibling Agents:** sonnet (standard implementation), haiku (status/explore/verify), codex (coding-heavy tasks)
|
||||
- **Shared Contracts:** `docs/PRD.md` (existing v0.1.0 PRD — still the long-term target), this manifest, `docs/TASKS.md`, `docs/scratchpads/cli-unification-20260404.md`
|
||||
- **Primary Agent:** claude-opus-4-6
|
||||
- **Sibling Agents:** sonnet (workers), haiku (verification)
|
||||
- **Shared Contracts:** docs/PRD-Harness_Foundation.md, docs/TASKS.md
|
||||
|
||||
## Token Budget
|
||||
|
||||
| Metric | Value |
|
||||
| ------ | ------ |
|
||||
| Budget | TBD |
|
||||
| Used | ~80K |
|
||||
| Budget | — |
|
||||
| Used | ~2.5M |
|
||||
| Mode | normal |
|
||||
|
||||
## Session History
|
||||
|
||||
| Session | Runtime | Started | Duration | Ended Reason | Last Task |
|
||||
| ------- | --------------- | ---------- | --------- | ------------ | ------------------------------------------------------------ |
|
||||
| 1 | claude-opus-4-6 | 2026-04-04 | in-flight | — | cu-m01 + cu-m02 merged (#398, #399); open questions resolved |
|
||||
| Session | Runtime | Started | Duration | Ended Reason | Last Task |
|
||||
| ------- | --------------- | ---------- | -------- | ------------ | ----------------- |
|
||||
| 1 | claude-opus-4-6 | 2026-03-21 | ~6h | complete | M7-008 — all done |
|
||||
|
||||
## Scratchpad
|
||||
|
||||
Path: `docs/scratchpads/cli-unification-20260404.md`
|
||||
Path: `docs/scratchpads/harness-20260321.md`
|
||||
|
||||
@@ -153,7 +153,7 @@ for any `<Image>` components added in the future.
|
||||
|
||||
```bash
|
||||
# Run the DB migration (requires a live DB)
|
||||
pnpm --filter @mosaicstack/db exec drizzle-kit migrate
|
||||
pnpm --filter @mosaic/db exec drizzle-kit migrate
|
||||
|
||||
# Or, in Docker/Swarm — migrations run automatically on gateway startup
|
||||
# via runMigrations() in packages/db/src/migrate.ts
|
||||
|
||||
@@ -57,7 +57,7 @@ Multi-panel layout with keyboard navigation.
|
||||
|
||||
- **Ink 5** (React for CLI) — already in deps
|
||||
- **Component architecture** — break monolithic `app.tsx` into composable components
|
||||
- **Typed Socket.IO events** — leverage `@mosaicstack/types` `ServerToClientEvents` / `ClientToServerEvents`
|
||||
- **Typed Socket.IO events** — leverage `@mosaic/types` `ServerToClientEvents` / `ClientToServerEvents`
|
||||
- **Local state only** (Wave 1) — cwd/branch read from `process.cwd()` and `git` at startup
|
||||
- **Gateway metadata** (future) — extend socket handshake or add REST endpoint for model info, token usage
|
||||
|
||||
|
||||
164
docs/PRD.md
164
docs/PRD.md
@@ -8,7 +8,7 @@
|
||||
- **Best-Guess Mode:** true
|
||||
- Repo (target): `git.mosaicstack.dev/mosaic/mosaic-stack`
|
||||
- Baseline: `~/src/jarvis-old` (jarvis v0.2.0)
|
||||
- Package source: `~/src/mosaic-mono-v0` (@mosaicstack/\* packages)
|
||||
- Package source: `~/src/mosaic-mono-v0` (@mosaic/\* packages)
|
||||
- Agent harness: [pi](https://github.com/badlogic/pi-mono) (v0.57.1)
|
||||
- Remote control reference: [OpenClaw](https://github.com/openclaw/openclaw) (upstream, canonical)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and Next.js frontend. It handles chat, projects, tasks, and LLM routing but lacks orchestration depth, agent coordination, shared memory, and remote access. The Mosaic framework (`~/.config/mosaic`) provides agent guides, shell-based orchestration tools, and quality rails — but these are loose scripts, not an integrated platform. The `@mosaicstack/*` packages in mosaic-mono-v0 began consolidating these into TypeScript packages (brain, queue, coord, cli, prdy, quality-rails) but have no UI, no auth, and no agent runtime integration.
|
||||
Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and Next.js frontend. It handles chat, projects, tasks, and LLM routing but lacks orchestration depth, agent coordination, shared memory, and remote access. The Mosaic framework (`~/.config/mosaic`) provides agent guides, shell-based orchestration tools, and quality rails — but these are loose scripts, not an integrated platform. The `@mosaic/*` packages in mosaic-mono-v0 began consolidating these into TypeScript packages (brain, queue, coord, cli, prdy, quality-rails) but have no UI, no auth, and no agent runtime integration.
|
||||
|
||||
**The gap:** Three codebases with overlapping concerns, no unified runtime, no remote control surface (Discord/Telegram), no gateway orchestrator, and a Python backend that doesn't align with the target TypeScript-everywhere stack.
|
||||
|
||||
@@ -32,7 +32,7 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
||||
4. **Gateway orchestrator** — Central routing layer that dispatches tasks to appropriate agents based on capability, cost, and context
|
||||
5. **Shared memory** — PostgreSQL canonical store + vector DB for semantic search + tiered log summarization to prevent context creep
|
||||
6. **Multi-user with SSO** — BetterAuth with Authentik/WorkOS/Keycloak SSO, RBAC for family/team/business use
|
||||
7. **Full @mosaicstack/\* package integration** — brain, queue, coord, mosaic, prdy, quality-rails, cli all integrated
|
||||
7. **Full @mosaic/\* package integration** — brain, queue, coord, mosaic, prdy, quality-rails, cli all integrated
|
||||
8. **Extensible** — MCP capability, skill import interface, plugin architecture for LLM providers and remote channels
|
||||
|
||||
---
|
||||
@@ -44,7 +44,7 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
||||
1. Chat/conversation UI (web) — carry forward from jarvis-old, rewrite frontend to work with new backend
|
||||
2. Pi TUI integration — terminal-based agent interaction using Pi SDK
|
||||
3. Web dashboard — settings, task management, projects, PRDs, missions, agent status
|
||||
4. Gateway orchestrator (`@mosaicstack/gateway`) — central dispatch for agent tasks with routing logic
|
||||
4. Gateway orchestrator (`@mosaic/gateway`) — central dispatch for agent tasks with routing logic
|
||||
5. Task management — CRUD, kanban, mission-scoped tasks, dependency tracking
|
||||
6. Project management — projects, milestones, PRDs linked to missions
|
||||
7. Shared memory system — learned preferences, behaviors, defaults; tiered storage with summarization
|
||||
@@ -55,13 +55,13 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
||||
12. Agent routing — task-based model/provider selection (cost/capability matrix)
|
||||
13. MCP capability — server and client, tool registration
|
||||
14. Skill import interface — browse, install, manage agent skills
|
||||
15. `@mosaicstack/brain` — structured data layer (migrated to PG + vector DB backend)
|
||||
16. `@mosaicstack/queue` — Valkey-backed task queue with MCP tools
|
||||
17. `@mosaicstack/coord` — mission coordination engine
|
||||
18. `@mosaicstack/mosaic` — install wizard / bootstrap
|
||||
19. `@mosaicstack/prdy` — PRD wizard
|
||||
20. `@mosaicstack/quality-rails` — code quality scaffolder
|
||||
21. `@mosaicstack/cli` — unified `mosaic` CLI
|
||||
15. `@mosaic/brain` — structured data layer (migrated to PG + vector DB backend)
|
||||
16. `@mosaic/queue` — Valkey-backed task queue with MCP tools
|
||||
17. `@mosaic/coord` — mission coordination engine
|
||||
18. `@mosaic/mosaic` — install wizard / bootstrap
|
||||
19. `@mosaic/prdy` — PRD wizard
|
||||
20. `@mosaic/quality-rails` — code quality scaffolder
|
||||
21. `@mosaic/cli` — unified `mosaic` CLI
|
||||
22. Docker Compose deployment + bare-metal capability
|
||||
23. Agent log service — ingest, parse, tier, summarize agent interaction logs
|
||||
|
||||
@@ -94,14 +94,14 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
||||
│ └──────────────┴───────┬───────┴────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────▼──────────┐ │
|
||||
│ │ @mosaicstack/gateway │ ← Central Orchestrator│
|
||||
│ │ @mosaic/gateway │ ← Central Orchestrator│
|
||||
│ │ (NestJS+Fastify) │ │
|
||||
│ └────┬────┬────┬─────┘ │
|
||||
│ │ │ │ │
|
||||
│ ┌──────────────┤ │ ├──────────────┐ │
|
||||
│ │ │ │ │ │ │
|
||||
│ ┌───────▼──────┐ ┌────▼────▼──┐ │ ┌───────────▼────────┐ │
|
||||
│ │ @mosaicstack/brain│ │ @mosaicstack/ │ │ │ Agent Pool │ │
|
||||
│ │ @mosaic/brain│ │ @mosaic/ │ │ │ Agent Pool │ │
|
||||
│ │ (Data Layer) │ │ queue │ │ │ (Pi SDK sessions) │ │
|
||||
│ └───────┬──────┘ └────────────┘ │ │ - Anthropic │ │
|
||||
│ │ │ │ - Codex │ │
|
||||
@@ -111,12 +111,12 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
||||
│ └──────────────┴───────────┘ │ │ - llama.cpp │ │
|
||||
│ │ └────────────────────┘ │
|
||||
│ ┌─────────────▼──────┐ │
|
||||
│ │ @mosaicstack/coord │ │
|
||||
│ │ @mosaic/coord │ │
|
||||
│ │ Mission lifecycle │ │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ @mosaicstack/cli │ │ @mosaicstack/prdy │ │ @mosaicstack/ │ │
|
||||
│ │ @mosaic/cli │ │ @mosaic/prdy │ │ @mosaic/ │ │
|
||||
│ │ │ │ │ │ quality-rails │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
@@ -130,20 +130,20 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
||||
|
||||
| Layer | Technology | Rationale |
|
||||
| ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| **Web Frontend** | Next.js 16 + React 19 + Tailwind CSS | SSR, RSC; design tokens from @mosaicstack/design-tokens (mosaic-stack-website) |
|
||||
| **Web Frontend** | Next.js 16 + React 19 + Tailwind CSS | SSR, RSC; design tokens from @mosaic/design-tokens (mosaic-stack-website) |
|
||||
| **API / Gateway** | NestJS + Fastify adapter | Module system, DI, guards/interceptors for complex gateway; Fastify performance underneath |
|
||||
| **Agent Runtime** | Pi SDK (embedded) | Extensible harness with tools, skills, session management |
|
||||
| **TUI** | Pi interactive mode | Native terminal agent interaction |
|
||||
| **Auth** | BetterAuth + SSO adapters | Multi-user RBAC with Authentik/WorkOS/Keycloak |
|
||||
| **Database** | PostgreSQL 17 + pgvector | Canonical store; pgvector for embedding search |
|
||||
| **Vector DB** | pgvector + VectorStore interface | pgvector for v0.1.0; `VectorStore` abstraction in @mosaicstack/memory makes Qdrant a drop-in later |
|
||||
| **Cache / Queue** | Valkey 8 | Redis-compatible; proven in @mosaicstack/queue |
|
||||
| **Vector DB** | pgvector + VectorStore interface | pgvector for v0.1.0; `VectorStore` abstraction in @mosaic/memory makes Qdrant a drop-in later |
|
||||
| **Cache / Queue** | Valkey 8 | Redis-compatible; proven in @mosaic/queue |
|
||||
| **ORM** | Drizzle ORM | TypeScript-native, lightweight, good migration story |
|
||||
| **Validation** | Zod | Already used across @mosaicstack/\* packages |
|
||||
| **Validation** | Zod | Already used across @mosaic/\* packages |
|
||||
| **Build** | pnpm workspaces + Turborepo | Proven in both jarvis-old and mosaic-mono-v0 |
|
||||
| **Testing** | Vitest + Playwright | Unit/integration via Vitest, E2E via Playwright |
|
||||
| **Remote Control** | Discord.js + Telegraf | Inspired by OpenClaw plugin architecture |
|
||||
| **MCP** | @modelcontextprotocol/sdk | Already used in @mosaicstack/brain and @mosaicstack/queue |
|
||||
| **MCP** | @modelcontextprotocol/sdk | Already used in @mosaic/brain and @mosaic/queue |
|
||||
| **Container** | Docker Compose | Self-hosted; bare-metal also supported |
|
||||
| **CI** | Woodpecker CI | Existing infrastructure at git.mosaicstack.dev |
|
||||
| **Observability** | OpenTelemetry + SigNoz | Wide-event logging from day one; OTEL auto-instrumentation for NestJS/PG/HTTP; SigNoz as all-in-one backend |
|
||||
@@ -158,12 +158,12 @@ The jarvis-old FastAPI backend is not carried forward as code. Its domain logic
|
||||
Instead of a custom LLM provider abstraction (jarvis-old's `BaseLLMProvider`), Pi SDK manages agent sessions. Pi handles model selection, tool calling, context management, and compaction. The gateway dispatches work to Pi sessions configured with appropriate providers.
|
||||
|
||||
**AD-3: Gateway as the central nervous system (NestJS + Fastify adapter)**
|
||||
`@mosaicstack/gateway` is the single API surface. The web app, TUI, Discord, and Telegram all talk to the gateway. The gateway routes to brain (data), queue (coordination), agent pool (LLM work), and coord (mission lifecycle). This replaces the direct FastAPI-to-DB pattern from jarvis-old.
|
||||
`@mosaic/gateway` is the single API surface. The web app, TUI, Discord, and Telegram all talk to the gateway. The gateway routes to brain (data), queue (coordination), agent pool (LLM work), and coord (mission lifecycle). This replaces the direct FastAPI-to-DB pattern from jarvis-old.
|
||||
|
||||
NestJS was chosen over raw Fastify because the gateway is inherently complex — it hosts channel plugins, agent pool management, routing engine, WebSocket hub, MCP server, auth middleware, and integrates brain, queue, memory, and log services. NestJS provides the module system, dependency injection, guards, and interceptors needed to organize this cleanly. NestJS uses Fastify as its HTTP adapter, so Fastify's performance is preserved. This also aligns with the stated stack preference in USER.md ("NestJS API + Next.js web"). @mosaicstack/brain's existing Fastify code migrates naturally into a NestJS module with Fastify adapter.
|
||||
NestJS was chosen over raw Fastify because the gateway is inherently complex — it hosts channel plugins, agent pool management, routing engine, WebSocket hub, MCP server, auth middleware, and integrates brain, queue, memory, and log services. NestJS provides the module system, dependency injection, guards, and interceptors needed to organize this cleanly. NestJS uses Fastify as its HTTP adapter, so Fastify's performance is preserved. This also aligns with the stated stack preference in USER.md ("NestJS API + Next.js web"). @mosaic/brain's existing Fastify code migrates naturally into a NestJS module with Fastify adapter.
|
||||
|
||||
**AD-4: Brain migrates from JSON files to PostgreSQL**
|
||||
`@mosaicstack/brain` currently uses a JSON file store. For Mosaic Stack, brain's data model (tasks, projects, events, agents, missions, tickets) moves to PostgreSQL via Drizzle ORM. Brain's REST + MCP interface is preserved — only the storage backend changes.
|
||||
`@mosaic/brain` currently uses a JSON file store. For Mosaic Stack, brain's data model (tasks, projects, events, agents, missions, tickets) moves to PostgreSQL via Drizzle ORM. Brain's REST + MCP interface is preserved — only the storage backend changes.
|
||||
|
||||
**AD-5: Tiered memory with summarization**
|
||||
Agent interaction logs are ingested into a log service. Raw logs are stored short-term. A summarization pipeline (using a cheap LLM) periodically compresses logs into structured insights stored in the vector DB. This prevents unbounded log growth while preserving searchable context.
|
||||
@@ -189,8 +189,8 @@ The gateway includes a cron scheduler for recurring tasks: log summarization run
|
||||
**AD-12: Web search tool (DuckDuckGo MCP)**
|
||||
Agent sessions include a web search tool for information retrieval. DuckDuckGo via MCP server is the primary option (privacy-respecting, no API key required). Falls back to other search MCP providers if configured. Registered as a standard MCP tool available to all agent sessions.
|
||||
|
||||
**AD-13: Design system from @mosaicstack/design-tokens**
|
||||
The web dashboard uses the Mosaic Stack design system established in `mosaic-stack-website`. The `@mosaicstack/design-tokens` package provides CSS custom properties, Tailwind preset, and TS color/font/radius exports. Dark theme default with light theme support. Fonts: Outfit (sans), Fira Code (mono). Color palette: deep blue-grays with blue/purple/teal accents.
|
||||
**AD-13: Design system from @mosaic/design-tokens**
|
||||
The web dashboard uses the Mosaic Stack design system established in `mosaic-stack-website`. The `@mosaic/design-tokens` package provides CSS custom properties, Tailwind preset, and TS color/font/radius exports. Dark theme default with light theme support. Fonts: Outfit (sans), Fira Code (mono). Color palette: deep blue-grays with blue/purple/teal accents.
|
||||
|
||||
**AD-14: Multi-tier deployment readiness**
|
||||
Code is structured assuming eventual multi-node deployment with dedicated roles (gateway nodes, agent worker nodes, brain/DB nodes). Packages communicate via well-defined APIs (HTTP/WS/MCP), not in-process calls where avoidable. Service boundaries are clean: gateway is stateless (state in PG/Valkey), agent pool can scale independently, brain is a separate service. v0.1.0 runs single-node; the architecture doesn't fight horizontal scaling later.
|
||||
@@ -205,25 +205,25 @@ Code is structured assuming eventual multi-node deployment with dedicated roles
|
||||
mosaic-mono-v1/
|
||||
├── apps/
|
||||
│ ├── web/ Next.js 16 web dashboard
|
||||
│ └── gateway/ @mosaicstack/gateway — NestJS API + WebSocket
|
||||
│ └── gateway/ @mosaic/gateway — NestJS API + WebSocket
|
||||
├── packages/
|
||||
│ ├── types/ @mosaicstack/types — shared type contracts
|
||||
│ ├── brain/ @mosaicstack/brain — data layer (PG-backed)
|
||||
│ ├── queue/ @mosaicstack/queue — Valkey task queue + MCP
|
||||
│ ├── coord/ @mosaicstack/coord — mission coordination
|
||||
│ ├── mosaic/ @mosaicstack/mosaic — install wizard
|
||||
│ ├── prdy/ @mosaicstack/prdy — PRD wizard
|
||||
│ ├── quality-rails/ @mosaicstack/quality-rails — code quality scaffolder
|
||||
│ ├── cli/ @mosaicstack/cli — unified CLI
|
||||
│ ├── auth/ @mosaicstack/auth — BetterAuth config + SSO adapters
|
||||
│ ├── db/ @mosaicstack/db — Drizzle schema, migrations, connection
|
||||
│ ├── agent/ @mosaicstack/agent — Pi SDK integration, agent pool manager
|
||||
│ ├── memory/ @mosaicstack/memory — tiered memory + summarization service
|
||||
│ ├── log/ @mosaicstack/log — agent log ingest + processing
|
||||
│ └── design-tokens/ @mosaicstack/design-tokens — CSS vars, Tailwind preset, colors
|
||||
│ ├── types/ @mosaic/types — shared type contracts
|
||||
│ ├── brain/ @mosaic/brain — data layer (PG-backed)
|
||||
│ ├── queue/ @mosaic/queue — Valkey task queue + MCP
|
||||
│ ├── coord/ @mosaic/coord — mission coordination
|
||||
│ ├── mosaic/ @mosaic/mosaic — install wizard
|
||||
│ ├── prdy/ @mosaic/prdy — PRD wizard
|
||||
│ ├── quality-rails/ @mosaic/quality-rails — code quality scaffolder
|
||||
│ ├── cli/ @mosaic/cli — unified CLI
|
||||
│ ├── auth/ @mosaic/auth — BetterAuth config + SSO adapters
|
||||
│ ├── db/ @mosaic/db — Drizzle schema, migrations, connection
|
||||
│ ├── agent/ @mosaic/agent — Pi SDK integration, agent pool manager
|
||||
│ ├── memory/ @mosaic/memory — tiered memory + summarization service
|
||||
│ ├── log/ @mosaic/log — agent log ingest + processing
|
||||
│ └── design-tokens/ @mosaic/design-tokens — CSS vars, Tailwind preset, colors
|
||||
├── plugins/
|
||||
│ ├── discord/ @mosaicstack/discord-plugin — Discord channel
|
||||
│ └── telegram/ @mosaicstack/telegram-plugin — Telegram channel
|
||||
│ ├── discord/ @mosaic/discord-plugin — Discord channel
|
||||
│ └── telegram/ @mosaic/telegram-plugin — Telegram channel
|
||||
├── docker/
|
||||
│ ├── gateway.Dockerfile
|
||||
│ ├── web.Dockerfile
|
||||
@@ -244,7 +244,7 @@ mosaic-mono-v1/
|
||||
|
||||
### Package Responsibilities
|
||||
|
||||
#### `apps/gateway` — @mosaicstack/gateway (NEW — critical path)
|
||||
#### `apps/gateway` — @mosaic/gateway (NEW — critical path)
|
||||
|
||||
The central nervous system. All clients connect here. Built with NestJS (Fastify adapter).
|
||||
|
||||
@@ -303,7 +303,7 @@ Carried forward from jarvis-old with significant refactoring.
|
||||
- User management (admin RBAC panel)
|
||||
- Auth pages (login, SSO redirect, registration)
|
||||
|
||||
#### `packages/types` — @mosaicstack/types
|
||||
#### `packages/types` — @mosaic/types
|
||||
|
||||
Migrated from mosaic-mono-v0. Extended with:
|
||||
|
||||
@@ -313,7 +313,7 @@ Migrated from mosaic-mono-v0. Extended with:
|
||||
- Memory types (preference, insight, summary)
|
||||
- Plugin channel types (Discord, Telegram message mapping)
|
||||
|
||||
#### `packages/brain` — @mosaicstack/brain
|
||||
#### `packages/brain` — @mosaic/brain
|
||||
|
||||
Migrated from mosaic-mono-v0. **Storage backend changes from JSON to PostgreSQL.**
|
||||
|
||||
@@ -324,7 +324,7 @@ Migrated from mosaic-mono-v0. **Storage backend changes from JSON to PostgreSQL.
|
||||
- New: computed endpoints (today, stale, stats, search, audit) run against PG
|
||||
- New: appreciation collection preserved for family use
|
||||
|
||||
#### `packages/queue` — @mosaicstack/queue
|
||||
#### `packages/queue` — @mosaic/queue
|
||||
|
||||
Migrated from mosaic-mono-v0 with minimal changes.
|
||||
|
||||
@@ -332,7 +332,7 @@ Migrated from mosaic-mono-v0 with minimal changes.
|
||||
- MCP server with 8 tools
|
||||
- Used by gateway for agent task dispatch and coordination
|
||||
|
||||
#### `packages/coord` — @mosaicstack/coord
|
||||
#### `packages/coord` — @mosaic/coord
|
||||
|
||||
Migrated from mosaic-mono-v0.
|
||||
|
||||
@@ -342,7 +342,7 @@ Migrated from mosaic-mono-v0.
|
||||
- Continuation prompt generation
|
||||
- Integration with gateway for mission-driven orchestration
|
||||
|
||||
#### `packages/db` — @mosaicstack/db (NEW)
|
||||
#### `packages/db` — @mosaic/db (NEW)
|
||||
|
||||
Shared database package.
|
||||
|
||||
@@ -351,7 +351,7 @@ Shared database package.
|
||||
- Connection pool configuration
|
||||
- Shared by gateway, brain, auth, memory
|
||||
|
||||
#### `packages/auth` — @mosaicstack/auth (NEW)
|
||||
#### `packages/auth` — @mosaic/auth (NEW)
|
||||
|
||||
Authentication and authorization.
|
||||
|
||||
@@ -361,7 +361,7 @@ Authentication and authorization.
|
||||
- API key generation for brain/MCP access
|
||||
- Session management middleware
|
||||
|
||||
#### `packages/agent` — @mosaicstack/agent (NEW — critical path)
|
||||
#### `packages/agent` — @mosaic/agent (NEW — critical path)
|
||||
|
||||
Pi SDK integration layer.
|
||||
|
||||
@@ -372,7 +372,7 @@ Pi SDK integration layer.
|
||||
- Skill management — loads and configures Pi skills for agent sessions
|
||||
- Session lifecycle — create, monitor, complete, fail, timeout
|
||||
|
||||
#### `packages/memory` — @mosaicstack/memory (NEW)
|
||||
#### `packages/memory` — @mosaic/memory (NEW)
|
||||
|
||||
Tiered memory system.
|
||||
|
||||
@@ -382,7 +382,7 @@ Tiered memory system.
|
||||
- Summarization pipeline — compress raw logs into structured insights
|
||||
- Memory API — used by gateway and agent sessions
|
||||
|
||||
#### `packages/log` — @mosaicstack/log (NEW)
|
||||
#### `packages/log` — @mosaic/log (NEW)
|
||||
|
||||
Agent log service.
|
||||
|
||||
@@ -392,7 +392,7 @@ Agent log service.
|
||||
- Summarization trigger — invokes cheap LLM to compress aging logs
|
||||
- Retention policy — configurable TTLs per tier
|
||||
|
||||
#### `packages/mosaic` — @mosaicstack/mosaic
|
||||
#### `packages/mosaic` — @mosaic/mosaic
|
||||
|
||||
Migrated from mosaic-mono-v0, updated for v1.
|
||||
|
||||
@@ -400,7 +400,7 @@ Migrated from mosaic-mono-v0, updated for v1.
|
||||
- Detects existing installations, offers upgrade path
|
||||
- Configures `~/.config/mosaic/` with guides, tools, runtime configs
|
||||
|
||||
#### `packages/prdy` — @mosaicstack/prdy
|
||||
#### `packages/prdy` — @mosaic/prdy
|
||||
|
||||
Migrated from mosaic-mono-v0.
|
||||
|
||||
@@ -408,7 +408,7 @@ Migrated from mosaic-mono-v0.
|
||||
- Template-based PRD creation with Zod validation
|
||||
- CLI integration via `mosaic prdy`
|
||||
|
||||
#### `packages/quality-rails` — @mosaicstack/quality-rails
|
||||
#### `packages/quality-rails` — @mosaic/quality-rails
|
||||
|
||||
Migrated from mosaic-mono-v0.
|
||||
|
||||
@@ -416,15 +416,15 @@ Migrated from mosaic-mono-v0.
|
||||
- Generates ESLint, tsconfig, Woodpecker, husky, lint-staged configs
|
||||
- Supports project types: monorepo, typescript-node, nextjs
|
||||
|
||||
#### `packages/cli` — @mosaicstack/cli
|
||||
#### `packages/cli` — @mosaic/cli
|
||||
|
||||
Migrated from mosaic-mono-v0, extended.
|
||||
|
||||
- Unified `mosaic` binary
|
||||
- Subcommands: `mosaic coord`, `mosaic prdy`, `mosaic queue`, `mosaic quality`, `mosaic gateway`, `mosaic brain`
|
||||
- Plugin discovery for installed @mosaicstack/\* packages
|
||||
- Plugin discovery for installed @mosaic/\* packages
|
||||
|
||||
#### `plugins/discord` — @mosaicstack/discord-plugin (NEW — high priority)
|
||||
#### `plugins/discord` — @mosaic/discord-plugin (NEW — high priority)
|
||||
|
||||
Discord remote control channel. Architecture inspired by OpenClaw (https://github.com/openclaw/openclaw).
|
||||
|
||||
@@ -436,7 +436,7 @@ Discord remote control channel. Architecture inspired by OpenClaw (https://githu
|
||||
- Bot pairing and permission management (Discord user → Mosaic user mapping)
|
||||
- DM support for private conversations
|
||||
|
||||
#### `plugins/telegram` — @mosaicstack/telegram-plugin (NEW)
|
||||
#### `plugins/telegram` — @mosaic/telegram-plugin (NEW)
|
||||
|
||||
Telegram remote control channel.
|
||||
|
||||
@@ -547,7 +547,7 @@ Telegram remote control channel.
|
||||
- WebSocket hub — real-time updates for chat, agent status, notifications
|
||||
- Rate limiting and request validation
|
||||
|
||||
### FR-3: Agent Pool (@mosaicstack/agent)
|
||||
### FR-3: Agent Pool (@mosaic/agent)
|
||||
|
||||
- Manage concurrent Pi SDK sessions
|
||||
- Provider configuration: API key management, endpoint URLs, model lists
|
||||
@@ -582,7 +582,7 @@ Telegram remote control channel.
|
||||
- Mission CRUD (linked to project and PRD)
|
||||
- Mission tasks with phases, dependencies, ordering
|
||||
- Mission summary with computed progress
|
||||
- Mission coordination via @mosaicstack/coord
|
||||
- Mission coordination via @mosaic/coord
|
||||
- Active mission dashboard in web UI
|
||||
|
||||
### FR-7: Memory System
|
||||
@@ -844,7 +844,7 @@ Telegram remote control channel.
|
||||
- [ ] Database migrations run automatically on first start
|
||||
- [ ] `.env.example` documents all required configuration
|
||||
|
||||
### AC-11: @mosaicstack/\* Packages
|
||||
### AC-11: @mosaic/\* Packages
|
||||
|
||||
- [ ] All 7 migrated packages build, pass tests, and integrate with gateway
|
||||
- [ ] `mosaic` CLI provides subcommands for each package
|
||||
@@ -870,7 +870,7 @@ Telegram remote control channel.
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
| -------------------------------------------------- | ---------- | ------ | ---------------------------------------------------------------------------------------- |
|
||||
| Pi SDK API instability (pre-1.0) | Medium | High | Pin version, abstract behind @mosaicstack/agent interface |
|
||||
| Pi SDK API instability (pre-1.0) | Medium | High | Pin version, abstract behind @mosaic/agent interface |
|
||||
| Brain PG migration complexity | Medium | Medium | Preserve Brain REST/MCP API contract; only storage changes |
|
||||
| Discord plugin complexity (OpenClaw has ~60 files) | Medium | Medium | Start minimal (DM + mention in channel), single-guild only; expand iteratively post-beta |
|
||||
| LLM provider subscription auth varies by provider | Medium | Medium | Abstract behind provider interface; implement per-provider adapters |
|
||||
@@ -882,7 +882,7 @@ Telegram remote control channel.
|
||||
|
||||
| # | Question | Priority | Status |
|
||||
| --- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 1 | Pi SDK version to pin for v0.1.0? | High | ✅ Resolved — Pin `@mariozechner/pi-coding-agent@~0.57.1` (current stable). Abstract behind `@mosaicstack/agent` interface to insulate from breaking changes. Bump deliberately after testing. |
|
||||
| 1 | Pi SDK version to pin for v0.1.0? | High | ✅ Resolved — Pin `@mariozechner/pi-coding-agent@~0.57.1` (current stable). Abstract behind `@mosaic/agent` interface to insulate from breaking changes. Bump deliberately after testing. |
|
||||
| 2 | Authentik vs WorkOS vs Keycloak — which SSO provider to implement first? | Medium | ✅ Resolved — Authentik first (already in Jason's infrastructure) |
|
||||
| 3 | Vector DB: pgvector sufficient or need Qdrant from the start? | Medium | ✅ Resolved — pgvector with VectorStore interface abstraction. Qdrant drops in later if needed. |
|
||||
| 4 | Summarization LLM: which model for log compression? | Medium | ✅ Resolved — Haiku-tier default with structured output guardrails, configurable via routing engine. |
|
||||
@@ -910,9 +910,9 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
|
||||
### Phase 0: Foundation (v0.0.1)
|
||||
|
||||
- Scaffold monorepo (pnpm + turbo + tsconfig + eslint + vitest)
|
||||
- `@mosaicstack/types` — migrate and extend from v0
|
||||
- `@mosaicstack/db` — Drizzle schema, PG connection, migrations
|
||||
- `@mosaicstack/auth` — BetterAuth setup with email/password
|
||||
- `@mosaic/types` — migrate and extend from v0
|
||||
- `@mosaic/db` — Drizzle schema, PG connection, migrations
|
||||
- `@mosaic/auth` — BetterAuth setup with email/password
|
||||
- OTEL foundation — `@opentelemetry/sdk-node` setup, SigNoz in docker-compose, trace propagation wired
|
||||
- Docker Compose (PG 17 + Valkey + SigNoz)
|
||||
- CI pipeline (Woodpecker)
|
||||
@@ -921,19 +921,19 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
|
||||
### Phase 1: Core API (v0.0.2)
|
||||
|
||||
- `apps/gateway` — NestJS server (Fastify adapter), auth middleware, health endpoints
|
||||
- `@mosaicstack/brain` — migrate from v0, swap JSON store for PG via @mosaicstack/db
|
||||
- `@mosaicstack/queue` — migrate from v0 (minimal changes)
|
||||
- `@mosaic/brain` — migrate from v0, swap JSON store for PG via @mosaic/db
|
||||
- `@mosaic/queue` — migrate from v0 (minimal changes)
|
||||
- Gateway routes: conversations, tasks, projects, missions
|
||||
- WebSocket server for chat streaming
|
||||
- Basic agent dispatch (single provider, no routing)
|
||||
|
||||
### Phase 2: Agent Layer (v0.0.3)
|
||||
|
||||
- `@mosaicstack/agent` — Pi SDK integration, agent pool manager
|
||||
- `@mosaic/agent` — Pi SDK integration, agent pool manager
|
||||
- Multi-provider support (Anthropic + Ollama minimum)
|
||||
- Agent routing engine (cost/capability matrix)
|
||||
- Tool registration (brain, queue, memory tools injected into agent sessions)
|
||||
- `@mosaicstack/coord` — migrate from v0, integrate with gateway
|
||||
- `@mosaic/coord` — migrate from v0, integrate with gateway
|
||||
|
||||
### Phase 3: Web Dashboard (v0.0.4)
|
||||
|
||||
@@ -946,25 +946,25 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
|
||||
|
||||
### Phase 4: Memory & Intelligence (v0.0.5)
|
||||
|
||||
- `@mosaicstack/memory` — preference store, insight store, semantic search
|
||||
- `@mosaicstack/log` — log ingest, parsing, tiered storage
|
||||
- `@mosaic/memory` — preference store, insight store, semantic search
|
||||
- `@mosaic/log` — log ingest, parsing, tiered storage
|
||||
- Summarization pipeline
|
||||
- Memory integration into agent sessions
|
||||
- Skill management interface (web UI + CLI)
|
||||
|
||||
### Phase 5: Remote Control (v0.0.6)
|
||||
|
||||
- `@mosaicstack/discord-plugin` — Discord channel plugin
|
||||
- `@mosaicstack/telegram-plugin` — Telegram channel plugin
|
||||
- `@mosaic/discord-plugin` — Discord channel plugin
|
||||
- `@mosaic/telegram-plugin` — Telegram channel plugin
|
||||
- Plugin host in gateway
|
||||
- SSO configuration (Authentik)
|
||||
|
||||
### Phase 6: CLI & Tools (v0.0.7)
|
||||
|
||||
- `@mosaicstack/cli` — unified CLI with all subcommands
|
||||
- `@mosaicstack/prdy` — migrate from v0
|
||||
- `@mosaicstack/quality-rails` — migrate from v0
|
||||
- `@mosaicstack/mosaic` — install wizard updated for v1
|
||||
- `@mosaic/cli` — unified CLI with all subcommands
|
||||
- `@mosaic/prdy` — migrate from v0
|
||||
- `@mosaic/quality-rails` — migrate from v0
|
||||
- `@mosaic/mosaic` — install wizard updated for v1
|
||||
- Pi TUI integration (`mosaic tui`)
|
||||
|
||||
### Phase 7: Polish & Beta (v0.0.8 → v0.1.0)
|
||||
@@ -982,11 +982,11 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
|
||||
|
||||
## Assumptions
|
||||
|
||||
1. RESOLVED: **pgvector is sufficient** for semantic search at v0.1.0 scale (personal/family/team = thousands to low hundreds-of-thousands of vectors). `@mosaicstack/memory` defines a `VectorStore` interface with pgvector as the default adapter. The interface boundary makes Qdrant a drop-in migration if PG resource contention or scale demands it later. Zero additional infrastructure for v0.1.0. Rationale: Reduces ops burden; pgvector HNSW indexes are fast at this scale; interface abstraction costs almost nothing now.
|
||||
1. RESOLVED: **pgvector is sufficient** for semantic search at v0.1.0 scale (personal/family/team = thousands to low hundreds-of-thousands of vectors). `@mosaic/memory` defines a `VectorStore` interface with pgvector as the default adapter. The interface boundary makes Qdrant a drop-in migration if PG resource contention or scale demands it later. Zero additional infrastructure for v0.1.0. Rationale: Reduces ops burden; pgvector HNSW indexes are fast at this scale; interface abstraction costs almost nothing now.
|
||||
|
||||
2. RESOLVED: **Authentik is the first SSO provider** — confirmed, already running in Jason's infrastructure. WorkOS and Keycloak adapters follow in Phase 7.
|
||||
|
||||
3. RESOLVED: **NestJS with Fastify adapter for the gateway.** The gateway's complexity (plugin host, agent pool, routing engine, WebSocket hub, MCP server, auth, brain/queue/memory/log integration) warrants NestJS's module system, DI, and guards. Fastify performance preserved via adapter. Aligns with USER.md stated stack ("NestJS API + Next.js web"). @mosaicstack/brain's Fastify code migrates into a NestJS module.
|
||||
3. RESOLVED: **NestJS with Fastify adapter for the gateway.** The gateway's complexity (plugin host, agent pool, routing engine, WebSocket hub, MCP server, auth, brain/queue/memory/log integration) warrants NestJS's module system, DI, and guards. Fastify performance preserved via adapter. Aligns with USER.md stated stack ("NestJS API + Next.js web"). @mosaic/brain's Fastify code migrates into a NestJS module.
|
||||
|
||||
4. RESOLVED: **OpenTelemetry from Phase 0.** Wide-event logging is required from the start. OTEL auto-instrumentation for NestJS/PG/HTTP via `@opentelemetry/sdk-node`. SigNoz as the all-in-one OTEL backend (single Docker service). Every significant operation emits structured events with rich context. Custom spans for agent dispatch, routing decisions, memory writes. Rationale: Retrofitting observability is painful; baking it in from day one means consistent instrumentation across all services.
|
||||
|
||||
@@ -1002,4 +1002,4 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
|
||||
|
||||
10. ASSUMPTION: **Conversations and messages get their own PG tables** (not stored in brain's entity model). They follow a chat-specific schema with proper foreign keys to users and projects. Rationale: Chat has different access patterns (streaming, pagination, search) than brain entities.
|
||||
|
||||
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaicstack/agent — only configuration management.
|
||||
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaic/agent — only configuration management.
|
||||
|
||||
@@ -108,4 +108,4 @@ The web login page renders provider buttons from `NEXT_PUBLIC_*_ENABLED` flags.
|
||||
|
||||
## Failure mode
|
||||
|
||||
Provider config is optional, but partial config is rejected at startup. If any provider-specific env var is present without the full required set, `@mosaicstack/auth` throws a bootstrap error with the missing keys instead of silently registering a broken provider.
|
||||
Provider config is optional, but partial config is rejected at startup. If any provider-specific env var is present without the full required set, `@mosaic/auth` throws a bootstrap error with the missing keys instead of silently registering a broken provider.
|
||||
|
||||
@@ -91,15 +91,15 @@ packages/cli/src/tui/
|
||||
|
||||
```bash
|
||||
cd /home/jwoltje/src/mosaic-mono-v1-worktrees/tui-improvements
|
||||
pnpm --filter @mosaicstack/cli exec tsx src/cli.ts tui
|
||||
pnpm --filter @mosaic/cli exec tsx src/cli.ts tui
|
||||
# or after build:
|
||||
node packages/cli/dist/cli.js tui --gateway http://localhost:14242
|
||||
node packages/cli/dist/cli.js tui --gateway http://localhost:4000
|
||||
```
|
||||
|
||||
### Quality Gates
|
||||
|
||||
```bash
|
||||
pnpm --filter @mosaicstack/cli typecheck && pnpm --filter @mosaicstack/cli lint
|
||||
pnpm --filter @mosaicstack/gateway typecheck && pnpm --filter @mosaicstack/gateway lint
|
||||
pnpm --filter @mosaicstack/types typecheck
|
||||
pnpm --filter @mosaic/cli typecheck && pnpm --filter @mosaic/cli lint
|
||||
pnpm --filter @mosaic/gateway typecheck && pnpm --filter @mosaic/gateway lint
|
||||
pnpm --filter @mosaic/types typecheck
|
||||
```
|
||||
|
||||
155
docs/TASKS.md
155
docs/TASKS.md
@@ -1,90 +1,73 @@
|
||||
# Tasks — CLI Unification & E2E First-Run
|
||||
# Tasks — Harness Foundation
|
||||
|
||||
> Single-writer: orchestrator only. Workers read but never modify.
|
||||
>
|
||||
> **Mission:** cli-unification-20260404
|
||||
> **Schema:** `| id | status | description | issue | agent | branch | depends_on | estimate | notes |`
|
||||
> **Status values:** `not-started` | `in-progress` | `done` | `blocked` | `failed` | `needs-qa`
|
||||
> **Agent values:** `codex` | `sonnet` | `haiku` | `opus` | `glm-5` | `—` (auto)
|
||||
> **`agent` column values:** `codex` | `sonnet` | `haiku` | `glm-5` | `opus` | `—` (auto/default)
|
||||
|
||||
## Milestone 1 — Kill legacy @mosaicstack/cli (done)
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ------ | ----------------------------------------------------------------- | ----- | ----- | ---------------------------------- | ---------- | -------- | --------------------------- |
|
||||
| CU-01-01 | done | Delete packages/cli directory; update workspace + docs references | #398 | opus | chore/remove-cli-package-duplicate | — | 5K | Merged c39433c3. 6685 LOC−. |
|
||||
|
||||
## Milestone 2 — Archive stale mission + scaffold new mission (done)
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ------ | ------------------------------------------------------------------ | ----- | ----- | ---------------------------- | ---------- | -------- | --------------------------------- |
|
||||
| CU-02-01 | done | Move stale MISSION-MANIFEST / TASKS / PRD-Harness to docs/archive/ | #399 | opus | docs/mission-cli-unification | CU-01-01 | 3K | Harness + storage missions done. |
|
||||
| CU-02-02 | done | Scaffold new MISSION-MANIFEST.md, TASKS.md, scratchpad | #399 | opus | docs/mission-cli-unification | CU-02-01 | 5K | This file + manifest + scratchpad |
|
||||
| CU-02-03 | done | PR review, merge, branch cleanup | #399 | opus | docs/mission-cli-unification | CU-02-02 | 2K | Merged as 6f15a84c |
|
||||
|
||||
## Milestone 3 — Gateway bootstrap token recovery
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ----------- | ---------------------------------------------------------------------------------------------- | ----- | ------ | ------ | ---------- | -------- | ----------------------------- |
|
||||
| CU-03-01 | not-started | Implementation plan for BetterAuth-cookie recovery flow (decision locked 2026-04-04) | — | opus | — | CU-02-03 | 4K | Design locked; plan-only task |
|
||||
| CU-03-02 | not-started | Server: add recovery/rotate endpoint on apps/gateway/src/admin (gated by design from CU-03-01) | — | sonnet | — | CU-03-01 | 12K | |
|
||||
| CU-03-03 | not-started | CLI: `mosaic gateway login` — interactive BetterAuth sign-in, persist session | — | sonnet | — | CU-03-02 | 10K | |
|
||||
| CU-03-04 | not-started | CLI: `mosaic gateway config rotate-token` — mint new admin token via authenticated API | — | sonnet | — | CU-03-03 | 8K | |
|
||||
| CU-03-05 | not-started | CLI: `mosaic gateway config recover-token` — execute the recovery flow from CU-03-01 | — | sonnet | — | CU-03-03 | 10K | |
|
||||
| CU-03-06 | not-started | Install UX: fix the "user exists, no token" dead-end in runInstall bootstrapFirstUser path | — | sonnet | — | CU-03-05 | 8K | |
|
||||
| CU-03-07 | not-started | Tests: integration tests for each recovery path (happy + error) | — | sonnet | — | CU-03-06 | 10K | |
|
||||
| CU-03-08 | not-started | Code review + remediation | — | haiku | — | CU-03-07 | 4K | |
|
||||
|
||||
## Milestone 4 — `mosaic --help` alphabetize + grouping
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------ | ------ | ---------- | -------- | ------------------------------- |
|
||||
| CU-04-01 | not-started | Enable `configureHelp({ sortSubcommands: true })` on root program and each subgroup | — | sonnet | — | CU-02-03 | 3K | |
|
||||
| CU-04-02 | not-started | Group commands into sections (Runtime, Gateway, Framework, Platform) in help output | — | sonnet | — | CU-04-01 | 5K | |
|
||||
| CU-04-03 | not-started | Verify help snapshots render readably; update any docs with stale output | — | haiku | — | CU-04-02 | 3K | |
|
||||
| CU-04-04 | not-started | Top-level `mosaic config` command — `show`, `get <key>`, `set <key> <val>`, `edit`, `path` — wraps packages/mosaic/src/config/config-service.ts (framework/agent config; distinct from `mosaic gateway config`) | — | sonnet | — | CU-02-03 | 10K | New scope (decision 2026-04-04) |
|
||||
| CU-04-05 | not-started | Tests + code review for CU-04-04 | — | haiku | — | CU-04-04 | 4K | |
|
||||
|
||||
## Milestone 5 — Sub-package CLI surface
|
||||
|
||||
> Pattern: each sub-package exports `register<Name>Command(program: Command)` co-located with the library code (proven by `@mosaicstack/quality-rails`). Wire into `packages/mosaic/src/cli.ts`.
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ----------- | --------------------------------------------------------------------------------------------------------- | ----- | ------ | ------ | ---------- | -------- | ------------------- |
|
||||
| CU-05-01 | not-started | `mosaic forge` — subcommands: `run`, `status`, `resume`, `personas list` | — | sonnet | — | CU-02-03 | 18K | User priority |
|
||||
| CU-05-02 | not-started | `mosaic storage` — subcommands: `status`, `tier show`, `tier switch`, `export`, `import`, `migrate` | — | sonnet | — | CU-02-03 | 15K | |
|
||||
| CU-05-03 | not-started | `mosaic queue` — subcommands: `list`, `stats`, `pause/resume`, `jobs tail`, `drain` | — | sonnet | — | CU-02-03 | 12K | |
|
||||
| CU-05-04 | not-started | `mosaic memory` — subcommands: `search`, `stats`, `insights list`, `preferences list` | — | sonnet | — | CU-02-03 | 12K | |
|
||||
| CU-05-05 | not-started | `mosaic brain` — subcommands: `projects list/create`, `missions list`, `tasks list`, `conversations list` | — | sonnet | — | CU-02-03 | 15K | |
|
||||
| CU-05-06 | not-started | `mosaic auth` — subcommands: `users list/create/delete`, `sso list`, `sso test`, `sessions list` | — | sonnet | — | CU-03-03 | 15K | needs gateway login |
|
||||
| CU-05-07 | not-started | `mosaic log` — subcommands: `tail`, `search`, `export`, `level <level>` | — | sonnet | — | CU-02-03 | 10K | |
|
||||
| CU-05-08 | not-started | `mosaic macp` — subcommands: `tasks list`, `submit`, `gate`, `events tail` | — | sonnet | — | CU-02-03 | 12K | |
|
||||
| CU-05-09 | not-started | Wire all eight `register<Name>Command` calls into packages/mosaic/src/cli.ts | — | haiku | — | CU-05-01…8 | 3K | |
|
||||
| CU-05-10 | not-started | Integration test: `mosaic <cmd> --help` exits 0 for every new command | — | haiku | — | CU-05-09 | 5K | |
|
||||
|
||||
## Milestone 6 — `mosaic telemetry`
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ----------- | ------------------------------------------------------------------------------------------------- | ----- | ------ | ------ | ---------- | -------- | ---------------------------------------------- |
|
||||
| CU-06-01 | not-started | Add `@mosaicstack/telemetry-client-js` as dependency of `@mosaicstack/mosaic` from Gitea registry | — | sonnet | — | CU-02-03 | 3K | |
|
||||
| CU-06-02 | not-started | `mosaic telemetry local` — status, tail, Jaeger link (wraps existing apps/gateway/src/tracing.ts) | — | sonnet | — | CU-06-01 | 8K | |
|
||||
| CU-06-03 | not-started | `mosaic telemetry` — status, opt-in, opt-out, test, upload (uses telemetry-client-js) | — | sonnet | — | CU-06-01 | 12K | Dry-run mode when server endpoint not yet live |
|
||||
| CU-06-04 | not-started | Persistent consent state in mosaic config; disabled by default | — | sonnet | — | CU-06-03 | 5K | |
|
||||
| CU-06-05 | not-started | Tests + code review | — | haiku | — | CU-06-04 | 5K | |
|
||||
|
||||
## Milestone 7 — Unified first-run UX
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ----------- | ---------------------------------------------------------------------------------------------- | ----- | ------ | ------ | ---------- | -------- | ----- |
|
||||
| CU-07-01 | not-started | tools/install.sh: after npm install, hand off to `mosaic wizard` then `mosaic gateway install` | — | sonnet | — | CU-03-06 | 10K | |
|
||||
| CU-07-02 | not-started | `mosaic wizard` and `mosaic gateway install` coordination: shared state, no duplicate prompts | — | sonnet | — | CU-07-01 | 12K | |
|
||||
| CU-07-03 | not-started | Post-install verification step: "gateway healthy, tui connects, admin token on file" | — | sonnet | — | CU-07-02 | 8K | |
|
||||
| CU-07-04 | not-started | End-to-end test on a clean container from scratch | — | haiku | — | CU-07-03 | 8K | |
|
||||
|
||||
## Milestone 8 — Docs + release
|
||||
|
||||
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
|
||||
| -------- | ----------- | ---------------------------------------------------------------------- | ----- | ------ | ------ | ---------- | -------- | ----- |
|
||||
| CU-08-01 | not-started | Update README.md with new command tree, install flow, and feature list | — | sonnet | — | CU-07-04 | 8K | |
|
||||
| CU-08-02 | not-started | Update docs/guides/user-guide.md with all new sub-package commands | — | sonnet | — | CU-08-01 | 10K | |
|
||||
| CU-08-03 | not-started | Version bump `@mosaicstack/mosaic`, publish to Gitea registry | — | opus | — | CU-08-02 | 3K | |
|
||||
| CU-08-04 | not-started | Release notes, tag `v0.1.0-rc.N`, publish release on Gitea | — | opus | — | CU-08-03 | 3K | |
|
||||
| id | status | agent | milestone | description | pr | notes |
|
||||
| ------ | ------ | ------ | ------------------ | ------------------------------------------------------------------ | ---- | ----------- |
|
||||
| M1-001 | done | sonnet | M1: Persistence | Wire ChatGateway → ConversationsRepo for user messages | #292 | #224 closed |
|
||||
| M1-002 | done | sonnet | M1: Persistence | Wire agent event relay → ConversationsRepo for assistant responses | #292 | #225 closed |
|
||||
| M1-003 | done | sonnet | M1: Persistence | Store message metadata: model, provider, tokens, tool calls | #292 | #226 closed |
|
||||
| M1-004 | done | sonnet | M1: Persistence | Load message history into Pi session on resume | #301 | #227 closed |
|
||||
| M1-005 | done | sonnet | M1: Persistence | Context window management: summarize when >80% | #301 | #228 closed |
|
||||
| M1-006 | done | sonnet | M1: Persistence | Conversation search endpoint | #299 | #229 closed |
|
||||
| M1-007 | done | sonnet | M1: Persistence | TUI /history command | #297 | #230 closed |
|
||||
| M1-008 | done | sonnet | M1: Persistence | Verify persistence — 20 tests | #304 | #231 closed |
|
||||
| M2-001 | done | sonnet | M2: Security | InsightsRepo userId on searchByEmbedding | #290 | #232 closed |
|
||||
| M2-002 | done | sonnet | M2: Security | InsightsRepo userId on findByUser/decay | #290 | #233 closed |
|
||||
| M2-003 | done | sonnet | M2: Security | PreferencesRepo userId verified | #294 | #234 closed |
|
||||
| M2-004 | done | sonnet | M2: Security | Memory tools userId injection fixed | #294 | #235 closed |
|
||||
| M2-005 | done | sonnet | M2: Security | ConversationsRepo ownership checks | #293 | #236 closed |
|
||||
| M2-006 | done | sonnet | M2: Security | AgentsRepo findAccessible scoped | #293 | #237 closed |
|
||||
| M2-007 | done | sonnet | M2: Security | Cross-user isolation — 28 tests | #305 | #238 closed |
|
||||
| M2-008 | done | sonnet | M2: Security | Valkey SCAN + /gc admin-only | #298 | #239 closed |
|
||||
| M3-001 | done | sonnet | M3: Providers | IProviderAdapter + OllamaAdapter | #306 | #240 closed |
|
||||
| M3-002 | done | sonnet | M3: Providers | AnthropicAdapter | #309 | #241 closed |
|
||||
| M3-003 | done | sonnet | M3: Providers | OpenAIAdapter | #310 | #242 closed |
|
||||
| M3-004 | done | sonnet | M3: Providers | OpenRouterAdapter | #311 | #243 closed |
|
||||
| M3-005 | done | sonnet | M3: Providers | ZaiAdapter (GLM-5) | #314 | #244 closed |
|
||||
| M3-006 | done | sonnet | M3: Providers | Ollama embedding support | #311 | #245 closed |
|
||||
| M3-007 | done | sonnet | M3: Providers | Provider health checks | #308 | #246 closed |
|
||||
| M3-008 | done | sonnet | M3: Providers | Model capability matrix | #303 | #247 closed |
|
||||
| M3-009 | done | sonnet | M3: Providers | EmbeddingService → Ollama default | #308 | #248 closed |
|
||||
| M3-010 | done | sonnet | M3: Providers | OAuth token storage (AES-256-GCM) | #317 | #249 closed |
|
||||
| M3-011 | done | sonnet | M3: Providers | Provider credentials CRUD | #317 | #250 closed |
|
||||
| M3-012 | done | sonnet | M3: Providers | Verify providers — 40 tests | #319 | #251 closed |
|
||||
| M4-001 | done | sonnet | M4: Routing | routing_rules DB schema | #315 | #252 closed |
|
||||
| M4-002 | done | sonnet | M4: Routing | Condition types | #315 | #253 closed |
|
||||
| M4-003 | done | sonnet | M4: Routing | Action types | #315 | #254 closed |
|
||||
| M4-004 | done | sonnet | M4: Routing | Default routing rules (11 seeds) | #316 | #255 closed |
|
||||
| M4-005 | done | sonnet | M4: Routing | Task classifier (60+ tests) | #316 | #256 closed |
|
||||
| M4-006 | done | sonnet | M4: Routing | Routing decision pipeline | #318 | #257 closed |
|
||||
| M4-007 | done | sonnet | M4: Routing | /model override | #323 | #258 closed |
|
||||
| M4-008 | done | sonnet | M4: Routing | Routing transparency in session:info | #323 | #259 closed |
|
||||
| M4-009 | done | sonnet | M4: Routing | Routing rules CRUD API | #320 | #260 closed |
|
||||
| M4-010 | done | sonnet | M4: Routing | Per-user routing overrides | #320 | #261 closed |
|
||||
| M4-011 | done | sonnet | M4: Routing | Agent specialization capabilities | #320 | #262 closed |
|
||||
| M4-012 | done | sonnet | M4: Routing | Routing wired into ChatGateway | #323 | #263 closed |
|
||||
| M4-013 | done | sonnet | M4: Routing | Verify routing — 9 E2E tests | #323 | #264 closed |
|
||||
| M5-001 | done | sonnet | M5: Sessions | Agent config loaded on session create | #323 | #265 closed |
|
||||
| M5-002 | done | sonnet | M5: Sessions | /model command end-to-end | #323 | #266 closed |
|
||||
| M5-003 | done | sonnet | M5: Sessions | /agent command mid-session | #323 | #267 closed |
|
||||
| M5-004 | done | sonnet | M5: Sessions | Session ↔ conversation binding | #321 | #268 closed |
|
||||
| M5-005 | done | sonnet | M5: Sessions | Session info broadcast | #321 | #269 closed |
|
||||
| M5-006 | done | sonnet | M5: Sessions | /agent new from TUI | #321 | #270 closed |
|
||||
| M5-007 | done | sonnet | M5: Sessions | Session metrics | #321 | #271 closed |
|
||||
| M5-008 | done | sonnet | M5: Sessions | Verify sessions — 28 tests | #324 | #272 closed |
|
||||
| M6-001 | done | sonnet | M6: Jobs | BullMQ + Valkey config | #324 | #273 closed |
|
||||
| M6-002 | done | sonnet | M6: Jobs | Queue service with typed jobs | #324 | #274 closed |
|
||||
| M6-003 | done | sonnet | M6: Jobs | Summarization → BullMQ | #324 | #275 closed |
|
||||
| M6-004 | done | sonnet | M6: Jobs | GC → BullMQ | #324 | #276 closed |
|
||||
| M6-005 | done | sonnet | M6: Jobs | Tier management → BullMQ | #324 | #277 closed |
|
||||
| M6-006 | done | sonnet | M6: Jobs | Admin jobs API | #325 | #278 closed |
|
||||
| M6-007 | done | sonnet | M6: Jobs | Job event logging | #325 | #279 closed |
|
||||
| M6-008 | done | sonnet | M6: Jobs | Verify jobs | #324 | #280 closed |
|
||||
| M7-001 | done | sonnet | M7: Channel Design | IChannelAdapter interface | #325 | #281 closed |
|
||||
| M7-002 | done | sonnet | M7: Channel Design | Channel message protocol | #325 | #282 closed |
|
||||
| M7-003 | done | sonnet | M7: Channel Design | Matrix integration design | #326 | #283 closed |
|
||||
| M7-004 | done | sonnet | M7: Channel Design | Conversation multiplexing | #326 | #284 closed |
|
||||
| M7-005 | done | sonnet | M7: Channel Design | Remote auth bridging | #326 | #285 closed |
|
||||
| M7-006 | done | sonnet | M7: Channel Design | Agent-to-agent via Matrix | #326 | #286 closed |
|
||||
| M7-007 | done | sonnet | M7: Channel Design | Multi-user isolation in Matrix | #326 | #287 closed |
|
||||
| M7-008 | done | sonnet | M7: Channel Design | channel-protocol.md published | #326 | #288 closed |
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# Mission Manifest — Harness Foundation
|
||||
|
||||
> Persistent document tracking full mission scope, status, and session history.
|
||||
> Updated by the orchestrator at each phase transition and milestone completion.
|
||||
|
||||
## Mission
|
||||
|
||||
**ID:** harness-20260321
|
||||
**Statement:** Transform Mosaic Stack from a functional demo into a real multi-provider, task-routing AI harness. Persist all conversations, integrate frontier LLM providers (Anthropic, OpenAI, OpenRouter, Z.ai, Ollama), build granular task-aware agent routing, harden agent sessions, replace cron with BullMQ, and design the channel protocol for future Matrix/remote integration.
|
||||
**Phase:** Complete
|
||||
**Current Milestone:** All milestones done
|
||||
**Progress:** 7 / 7 milestones
|
||||
**Status:** complete
|
||||
**Last Updated:** 2026-03-22 UTC
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] AC-1: Send messages in TUI → restart TUI → resume conversation → agent has full history and context
|
||||
- [x] AC-2: Route a coding task to Claude Opus 4.6, a simple question to Haiku, a summarization to GLM-5 — all via granular routing rules
|
||||
- [x] AC-3: Two users exist, User A's memory searches never return User B's data
|
||||
- [x] AC-4: `/model claude-sonnet-4-6` in TUI switches the active model for subsequent messages
|
||||
- [x] AC-5: `/agent coding-agent` in TUI switches to a different agent with different system prompt and tools
|
||||
- [x] AC-6: BullMQ jobs execute on schedule, failures retry with backoff, admin can inspect via `/api/admin/jobs`
|
||||
- [x] AC-7: Channel protocol document exists with Matrix integration points defined, reviewed, and approved
|
||||
- [x] AC-8: Embeddings run on Ollama local models (no external API dependency for vector operations)
|
||||
- [x] AC-9: All five providers (Anthropic, OpenAI, OpenRouter, Z.ai, Ollama) connect, list models, and complete chat requests
|
||||
- [x] AC-10: Routing transparency — TUI displays which model was selected and the routing reason for each response
|
||||
|
||||
## Milestones
|
||||
|
||||
| # | ID | Name | Status | Branch | Issue | Started | Completed |
|
||||
| --- | ------ | ---------------------------------- | ------ | ------ | --------- | ---------- | ---------- |
|
||||
| 1 | ms-166 | Conversation Persistence & Context | done | — | #224–#231 | 2026-03-21 | 2026-03-21 |
|
||||
| 2 | ms-167 | Security & Isolation | done | — | #232–#239 | 2026-03-21 | 2026-03-21 |
|
||||
| 3 | ms-168 | Provider Integration | done | — | #240–#251 | 2026-03-21 | 2026-03-22 |
|
||||
| 4 | ms-169 | Agent Routing Engine | done | — | #252–#264 | 2026-03-22 | 2026-03-22 |
|
||||
| 5 | ms-170 | Agent Session Hardening | done | — | #265–#272 | 2026-03-22 | 2026-03-22 |
|
||||
| 6 | ms-171 | Job Queue Foundation | done | — | #273–#280 | 2026-03-22 | 2026-03-22 |
|
||||
| 7 | ms-172 | Channel Protocol Design | done | — | #281–#288 | 2026-03-22 | 2026-03-22 |
|
||||
|
||||
## Deployment
|
||||
|
||||
| Target | URL | Method |
|
||||
| -------------------- | --------- | -------------------------- |
|
||||
| Docker Compose (dev) | localhost | docker compose up |
|
||||
| Production | TBD | Docker Swarm via Portainer |
|
||||
|
||||
## Coordination
|
||||
|
||||
- **Primary Agent:** claude-opus-4-6
|
||||
- **Sibling Agents:** sonnet (workers), haiku (verification)
|
||||
- **Shared Contracts:** docs/PRD-Harness_Foundation.md, docs/TASKS.md
|
||||
|
||||
## Token Budget
|
||||
|
||||
| Metric | Value |
|
||||
| ------ | ------ |
|
||||
| Budget | — |
|
||||
| Used | ~2.5M |
|
||||
| Mode | normal |
|
||||
|
||||
## Session History
|
||||
|
||||
| Session | Runtime | Started | Duration | Ended Reason | Last Task |
|
||||
| ------- | --------------- | ---------- | -------- | ------------ | ----------------- |
|
||||
| 1 | claude-opus-4-6 | 2026-03-21 | ~6h | complete | M7-008 — all done |
|
||||
|
||||
## Scratchpad
|
||||
|
||||
Path: `docs/scratchpads/harness-20260321.md`
|
||||
@@ -1,30 +0,0 @@
|
||||
# Tasks — Storage Abstraction Retrofit
|
||||
|
||||
> Single-writer: orchestrator only. Workers read but never modify.
|
||||
>
|
||||
> **Mission:** Decouple gateway from hardcoded Postgres/Valkey backends. Introduce interface-driven middleware so the gateway is backend-agnostic. Default to local tier (SQLite + JSON) for zero-dependency installs.
|
||||
>
|
||||
> **`agent` column values:** `codex` | `sonnet` | `haiku` | `glm-5` | `opus` | `—` (auto/default)
|
||||
|
||||
| id | status | agent | description | tokens |
|
||||
| --------- | ----------- | ------ | ---------------------------------------------------------------- | ------ |
|
||||
| SA-P1-001 | done | sonnet | Define QueueAdapter interface in packages/queue/src/types.ts | 3K |
|
||||
| SA-P1-002 | done | sonnet | Define StorageAdapter interface in packages/storage/src/types.ts | 3K |
|
||||
| SA-P1-003 | done | sonnet | Define MemoryAdapter interface in packages/memory/src/types.ts | 3K |
|
||||
| SA-P1-004 | done | sonnet | Create adapter factory pattern + config types | 3K |
|
||||
| SA-P2-001 | done | sonnet | Refactor @mosaicstack/queue: wrap ioredis as BullMQ adapter | 3K |
|
||||
| SA-P2-002 | done | sonnet | Create @mosaicstack/storage: wrap Drizzle as Postgres adapter | 6K |
|
||||
| SA-P2-003 | done | sonnet | Refactor @mosaicstack/memory: extract pgvector adapter | 4K |
|
||||
| SA-P2-004 | done | sonnet | Update gateway modules to use factories + DI tokens | 5K |
|
||||
| SA-P2-005 | done | opus | Verify Phase 2: all tests pass, typecheck clean | — |
|
||||
| SA-P3-001 | done | sonnet | Implement local queue adapter: JSON file persistence | 5K |
|
||||
| SA-P3-002 | done | sonnet | Implement SQLite storage adapter with better-sqlite3 | 8K |
|
||||
| SA-P3-003 | done | sonnet | Implement keyword memory adapter — no vector dependency | 4K |
|
||||
| SA-P3-004 | done | opus | Verify Phase 3: 42 new tests, 347 total passing | — |
|
||||
| SA-P4-001 | done | sonnet | MosaicConfig schema + loader with tier auto-detection | 6K |
|
||||
| SA-P4-002 | done | sonnet | CLI: mosaic gateway init — interactive wizard | 4K |
|
||||
| SA-P4-003 | done | sonnet | CLI: mosaic gateway start/stop/status lifecycle | 5K |
|
||||
| SA-P4-004 | done | opus | Verify Phase 4: 381 tests passing, 40/40 tasks clean | — |
|
||||
| SA-P5-001 | not-started | codex | Migration tooling: mosaic storage export/import | — |
|
||||
| SA-P5-002 | not-started | codex | Docker Compose profiles: local vs team | — |
|
||||
| SA-P5-003 | not-started | codex | Final verification + docs: README, architecture diagram | — |
|
||||
@@ -1,555 +0,0 @@
|
||||
# Storage & Queue Abstraction — Middleware Architecture
|
||||
|
||||
Design
|
||||
Status: Design (retrofit required)
|
||||
date: 2026-04-02
|
||||
context: Agents coupled directly to infrastructure backends, bypassing intended middleware layer
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
Current packages are **direct adapters**, not **middleware**:
|
||||
| Package | Current State | Intended Design |
|
||||
|---------|---------------|-----------------|
|
||||
| `@mosaicstack/queue` | `ioredis` hardcoded | Interface → BullMQ OR local-files |
|
||||
| `@mosaicstack/db` | Drizzle + Postgres hardcoded | Interface → Postgres OR SQLite OR JSON/MD |
|
||||
| `@mosaicstack/memory` | pgvector required | Interface → pgvector OR sqlite-vec OR keyword-search |
|
||||
|
||||
## The gateway and TUI import these packages directly, which means they they're coupled to specific infrastructure. Users cannot run Mosaic Stack without Postgres + Valkey.
|
||||
|
||||
## The Intended Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Gateway / TUI / CLI │
|
||||
│ (agnostic of storage backend, talks to middleware) │
|
||||
└───────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
▼─────────────────┴─────────────────┴─────────────────┘
|
||||
| | | |
|
||||
▼─────────────────┴───────────────────┴─────────────────┘
|
||||
| | | |
|
||||
Queue Storage Memory
|
||||
| | | |
|
||||
┌─────────┬─────────┬─────────┬─────────────────────────────────┐
|
||||
| BullMQ | | Local | | Postgres | SQLite | JSON/MD | pgvector | sqlite-vec | keyword |
|
||||
|(Valkey)| |(files) | | | | | |
|
||||
└─────────┴─────────┴─────────┴─────────────────────────────────┘
|
||||
```
|
||||
|
||||
The gateway imports the interface, not the backend. At startup it reads config and instantiates the correct adapter.
|
||||
|
||||
## The Drift
|
||||
|
||||
```typescript
|
||||
// What should have happened:
|
||||
gateway/queue.service.ts → @mosaicstack/queue (interface) → queue.adapter.ts
|
||||
|
||||
// What actually happened:
|
||||
gateway/queue.service.ts → @mosaicstack/queue → ioredis (hardcoded)
|
||||
```
|
||||
|
||||
## The Current State Analysis
|
||||
|
||||
### `@mosaicstack/queue` (packages/queue/src/queue.ts)
|
||||
|
||||
```typescript
|
||||
import Redis from 'ioredis'; // ← Direct import of backend
|
||||
|
||||
export function createQueue(config?: QueueConfig): QueueHandle {
|
||||
const url = config?.url ?? process.env['VALKEY_URL'] ?? DEFAULT_VALKEY_URL;
|
||||
const redis = new Redis(url, { maxRetriesPerRequest: 3 });
|
||||
// ...queue ops directly on redis...
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** `ioredis` is imported in the package, not the adapter interface. Consumers cannot swap backends.
|
||||
|
||||
### `@mosaicstack/db` (packages/db/src/client.ts)
|
||||
|
||||
```typescript
|
||||
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
|
||||
export function createDb(url?: string): DbHandle {
|
||||
const connectionString = url ?? process.env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL;
|
||||
const sql = postgres(connectionString, { max: 20, idle_timeout: 30, connect_timeout: 5 });
|
||||
const db = drizzle(sql, { schema });
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** Drizzle + Postgres is hardcoded. No SQLite, JSON, or file-based options.
|
||||
|
||||
### `@mosaicstack/memory` (packages/memory/src/memory.ts)
|
||||
|
||||
```typescript
|
||||
import type { Db } from '@mosaicstack/db'; // ← Depends on Drizzle/PG
|
||||
|
||||
export function createMemory(db: Db): Memory {
|
||||
return {
|
||||
preferences: createPreferencesRepo(db),
|
||||
insights: createInsightsRepo(db),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** Memory package is tightly coupled to `@mosaicstack/db` (which is Postgres-only). No alternative storage backends.
|
||||
|
||||
## The Target Interfaces
|
||||
|
||||
### Queue Interface
|
||||
|
||||
```typescript
|
||||
// packages/queue/src/types.ts
|
||||
export interface QueueAdapter {
|
||||
readonly name: string;
|
||||
|
||||
enqueue(queueName: string, payload: TaskPayload): Promise<void>;
|
||||
dequeue(queueName: string): Promise<TaskPayload | null>;
|
||||
length(queueName: string): Promise<number>;
|
||||
publish(channel: string, message: string): Promise<void>;
|
||||
subscribe(channel: string, handler: (message: string) => void): () => void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface TaskPayload {
|
||||
id: string;
|
||||
type: string;
|
||||
data: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface QueueConfig {
|
||||
type: 'bullmq' | 'local';
|
||||
url?: string; // For bullmq: Valkey/Redis URL
|
||||
dataDir?: string; // For local: directory for JSON persistence
|
||||
}
|
||||
```
|
||||
|
||||
### Storage Interface
|
||||
|
||||
```typescript
|
||||
// packages/storage/src/types.ts
|
||||
export interface StorageAdapter {
|
||||
readonly name: string;
|
||||
|
||||
// Entity CRUD
|
||||
create<T>(collection: string, data: O): Promise<T>;
|
||||
read<T>(collection: string, id: string): Promise<T | null>;
|
||||
update<T>(collection: string, id: string, data: Partial<O>): Promise<T | null>;
|
||||
delete(collection: string, id: string): Promise<boolean>;
|
||||
|
||||
// Queries
|
||||
find<T>(collection: string, filter: Record<string, unknown>): Promise<T[]>;
|
||||
findOne<T>(collection: string, filter: Record<string, unknown): Promise<T | null>;
|
||||
|
||||
// Bulk operations
|
||||
createMany<T>(collection: string, items: O[]): Promise<T[]>;
|
||||
updateMany<T>(collection: string, ids: string[], data: Partial<O>): Promise<number>;
|
||||
deleteMany(collection: string, ids: string[]): Promise<number>;
|
||||
|
||||
// Raw queries (for complex queries)
|
||||
query<T>(collection: string, query: string, params?: unknown[]): Promise<T[]>;
|
||||
|
||||
// Transaction support
|
||||
transaction<T>(fn: (tx: StorageTransaction) => Promise<T>): Promise<T>;
|
||||
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface StorageTransaction {
|
||||
commit(): Promise<void>;
|
||||
rollback(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface StorageConfig {
|
||||
type: 'postgres' | 'sqlite' | 'files';
|
||||
url?: string; // For postgres
|
||||
path?: string; // For sqlite/files
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Interface (Vector + Preferences)
|
||||
|
||||
```typescript
|
||||
// packages/memory/src/types.ts
|
||||
export interface MemoryAdapter {
|
||||
readonly name: string;
|
||||
|
||||
// Preferences (key-value storage)
|
||||
getPreference(userId: string, key: string): Promise<unknown | null>;
|
||||
setPreference(userId: string, key: string, value: unknown): Promise<void>;
|
||||
deletePreference(userId: string, key: string): Promise<boolean>;
|
||||
listPreferences(
|
||||
userId: string,
|
||||
category?: string,
|
||||
): Promise<Array<{ key: string; value: unknown }>>;
|
||||
|
||||
// Insights (with optional vector search)
|
||||
storeInsight(insight: NewInsight): Promise<Insight>;
|
||||
getInsight(id: string): Promise<Insight | null>;
|
||||
searchInsights(query: string, limit?: number, filter?: InsightFilter): Promise<SearchResult[]>;
|
||||
deleteInsight(id: string): Promise<boolean>;
|
||||
|
||||
// Embedding provider (optional, null = no vector search)
|
||||
readonly embedder?: EmbeddingProvider | null;
|
||||
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface NewInsight {
|
||||
id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
embedding?: number[]; // If embedder is available
|
||||
source: 'agent' | 'user' | 'summarization' | 'system';
|
||||
category: 'decision' | 'learning' | 'preference' | 'fact' | 'pattern' | 'general';
|
||||
relevanceScore: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
decayedAt?: Date;
|
||||
}
|
||||
|
||||
export interface InsightFilter {
|
||||
userId?: string;
|
||||
category?: string;
|
||||
source?: string;
|
||||
minRelevance?: number;
|
||||
fromDate?: Date;
|
||||
toDate?: Date;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
documentId: string;
|
||||
content: string;
|
||||
distance: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MemoryConfig {
|
||||
type: 'pgvector' | 'sqlite-vec' | 'keyword';
|
||||
storage: StorageAdapter;
|
||||
embedder?: EmbeddingProvider;
|
||||
}
|
||||
|
||||
export interface EmbeddingProvider {
|
||||
embed(text: string): Promise<number[]>;
|
||||
embedBatch(texts: string[]): Promise<number[][]>;
|
||||
readonly dimensions: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Three Tiers
|
||||
|
||||
### Tier 1: Local (Zero Dependencies)
|
||||
|
||||
**Target:** Single user, single machine, no external services
|
||||
|
||||
| Component | Backend | Storage |
|
||||
| --------- | --------------------------------------------- | ------------ |
|
||||
| Queue | In-process + JSON files in `~/.mosaic/queue/` |
|
||||
| Storage | SQLite (better-sqlite3) `~/.mosaic/data.db` |
|
||||
| Memory | Keyword search | SQLite table |
|
||||
| Vector | None | N/A |
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- `better-sqlite3` (bundled)
|
||||
- No Postgres, No Valkey, No pgvector
|
||||
|
||||
**Upgrade path:**
|
||||
|
||||
1. Run `mosaic gateway configure` → select "local" tier
|
||||
2. Gateway starts with SQLite database
|
||||
3. Optional: run `mosaic gateway upgrade --tier team` to migrate to Postgres
|
||||
|
||||
### Tier 2: Team (Postgres + Valkey)
|
||||
|
||||
**Target:** Multiple users, shared server, CI/CD environments
|
||||
|
||||
| Component | Backend | Storage |
|
||||
| --------- | -------------- | ------------------------------ |
|
||||
| Queue | BullMQ | Valkey |
|
||||
| Storage | Postgres | Shared PG instance |
|
||||
| Memory | pgvector | Postgres with vector extension |
|
||||
| Vector | LLM embeddings | Configured provider |
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- PostgreSQL 17+ with pgvector extension
|
||||
- Valkey (Redis-compatible)
|
||||
- LLM provider for embeddings
|
||||
|
||||
**Migration from Local → Team:**
|
||||
|
||||
1. `mosaic gateway backup` → creates dump of SQLite database
|
||||
2. `mosaic gateway upgrade --tier team` → restores to Postgres
|
||||
3. Queue replays from BullMQ (may need manual reconciliation for in-flight jobs)
|
||||
4. Memory embeddings regenerated if vector search was new
|
||||
|
||||
### Tier 3: Enterprise (Clustered)
|
||||
|
||||
**Target:** Large teams, multi-region, high availability
|
||||
|
||||
| Component | Backend | Storage |
|
||||
| --------- | --------------------------- | ----------------------------- |
|
||||
| Queue | BullMQ cluster | Multiple Valkey nodes |
|
||||
| Storage | Postgres cluster | Primary + replicas |
|
||||
| Memory | Dedicated vector DB | Qdrant, Pinecone, or pgvector |
|
||||
| Vector | Dedicated embedding service | Separate microservice |
|
||||
|
||||
## MarkdownDB Integration
|
||||
|
||||
For file-based storage, we use [MarkdownDB](https://markdowndb.com) to parse MD files into queryable data.
|
||||
|
||||
**What it provides:**
|
||||
|
||||
- Parses frontmatter (YAML/JSON/TOML)
|
||||
- Extracts links, tags, metadata
|
||||
- Builds index in JSON or SQLite
|
||||
- Queryable via SQL-like interface
|
||||
|
||||
**Usage in Mosaic:**
|
||||
|
||||
```typescript
|
||||
// Local tier with MD files for documents
|
||||
const storage = createStorageAdapter({
|
||||
type: 'files',
|
||||
path: path.join(mosaicHome, 'docs'),
|
||||
markdowndb: {
|
||||
parseFrontmatter: true,
|
||||
extractLinks: true,
|
||||
indexFile: 'index.json',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Dream Mode — Memory Consolidation
|
||||
|
||||
Automated equivalent to Claude Code's "Dream: Memory Consolidation" cycle
|
||||
|
||||
**Trigger:** Every 24 hours (if 5+ sessions active)
|
||||
|
||||
**Phases:**
|
||||
|
||||
1. **Orient** — What happened, what's the current state
|
||||
- Scan recent session logs
|
||||
- Identify active tasks, missions, conversations
|
||||
- Calculate time window (last 24h)
|
||||
|
||||
2. **Gather** — Pull in relevant context
|
||||
- Load conversations, decisions, agent logs
|
||||
- Extract key interactions and outcomes
|
||||
- Identify patterns and learnings
|
||||
|
||||
3. **Consolidate** — Summarize and compress
|
||||
- Generate summary of the last 24h
|
||||
- Extract key decisions and their rationale
|
||||
- Identify recurring patterns
|
||||
- Compress verbose logs into concise insights
|
||||
|
||||
4. **Prune** — Archive and cleanup
|
||||
- Archive raw session files to dated folders
|
||||
- Delete redundant/temporary data
|
||||
- Update MEMORY.md with consolidated content
|
||||
- Update insight relevance scores
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
// In @mosaicstack/dream (new package)
|
||||
export async function runDreamCycle(config: DreamConfig): Promise<DreamResult> {
|
||||
const memory = await loadMemoryAdapter(config.storage);
|
||||
|
||||
// Orient
|
||||
const sessions = await memory.getRecentSessions(24 * 60 * 60 * 1000);
|
||||
if (sessions.length < 5) return { skipped: true, reason: 'insufficient_sessions' };
|
||||
|
||||
// Gather
|
||||
const context = await gatherContext(memory, sessions);
|
||||
|
||||
// Consolidate
|
||||
const consolidated = await consolidateWithLLM(context, config.llm);
|
||||
|
||||
// Prune
|
||||
await pruneArchivedData(memory, config.retention);
|
||||
|
||||
// Store consolidated insights
|
||||
await memory.storeInsights(consolidated.insights);
|
||||
|
||||
return {
|
||||
sessionsProcessed: sessions.length,
|
||||
insightsCreated: consolidated.insights.length,
|
||||
bytesPruned: consolidated.bytesRemoved,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Retrofit Plan
|
||||
|
||||
### Phase 1: Interface Extraction (2-3 days)
|
||||
|
||||
**Goal:** Define interfaces without changing existing behavior
|
||||
|
||||
1. Create `packages/queue/src/types.ts` with `QueueAdapter` interface
|
||||
2. Create `packages/storage/src/types.ts` with `StorageAdapter` interface
|
||||
3. Create `packages/memory/src/types.ts` with `MemoryAdapter` interface (refactor existing)
|
||||
4. Add adapter registry pattern to each package
|
||||
5. No breaking changes — existing code continues to work
|
||||
|
||||
### Phase 2: Refactor Existing to Adapters (3-5 days)
|
||||
|
||||
**Goal:** Move existing implementations behind adapters
|
||||
|
||||
#### 2.1 Queue Refactor
|
||||
|
||||
1. Rename `packages/queue/src/queue.ts` → `packages/queue/src/adapters/bullmq.ts`
|
||||
2. Create `packages/queue/src/index.ts` to export factory function
|
||||
3. Factory function reads config, instantiates correct adapter
|
||||
4. Update gateway imports to use factory
|
||||
|
||||
#### 2.2 Storage Refactor
|
||||
|
||||
1. Create `packages/storage/` (new package)
|
||||
2. Move Drizzle logic to `packages/storage/src/adapters/postgres.ts`
|
||||
3. Create SQLite adapter in `packages/storage/src/adapters/sqlite.ts`
|
||||
4. Update gateway to use storage factory
|
||||
5. Deprecate direct `@mosaicstack/db` imports
|
||||
|
||||
#### 2.3 Memory Refactor
|
||||
|
||||
1. Extract existing logic to `packages/memory/src/adapters/pgvector.ts`
|
||||
2. Create keyword adapter in `packages/memory/src/adapters/keyword.ts`
|
||||
3. Update vector-store.ts to be adapter-agnostic
|
||||
|
||||
### Phase 3: Local Tier Implementation (2-3 days)
|
||||
|
||||
**Goal:** Zero-dependency baseline
|
||||
|
||||
1. Implement `packages/queue/src/adapters/local.ts` (in-process + JSON persistence)
|
||||
2. Implement `packages/storage/src/adapters/files.ts` (JSON + MD via MarkdownDB)
|
||||
3. Implement `packages/memory/src/adapters/keyword.ts` (TF-IDF search)
|
||||
4. Add `packages/dream/` for consolidation cycle
|
||||
5. Wire up local tier in gateway startup
|
||||
|
||||
### Phase 4: Configuration System (1-2 days)
|
||||
|
||||
**Goal:** Runtime backend selection
|
||||
|
||||
1. Create `packages/config/src/storage.ts` for storage configuration
|
||||
2. Add `mosaic.config.ts` schema with storage tier settings
|
||||
3. Update gateway to read config on startup
|
||||
4. Add `mosaic gateway configure` CLI command
|
||||
5. Add tier migration commands (`mosaic gateway upgrade`)
|
||||
|
||||
### Phase 5: Testing & Documentation (2-3 days)
|
||||
|
||||
1. Unit tests for each adapter
|
||||
2. Integration tests for factory pattern
|
||||
3. Migration tests (local → team)
|
||||
4. Update README and architecture docs
|
||||
5. Add configuration guide
|
||||
|
||||
---
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
### New Files
|
||||
|
||||
```
|
||||
packages/
|
||||
├── config/
|
||||
│ └── src/
|
||||
│ ├── storage.ts # Storage config schema
|
||||
│ └── index.ts
|
||||
├── dream/ # NEW: Dream mode consolidation
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── orient.ts
|
||||
│ │ ├── gather.ts
|
||||
│ │ ├── consolidate.ts
|
||||
│ │ └── prune.ts
|
||||
│ └── package.json
|
||||
├── queue/
|
||||
│ └── src/
|
||||
│ ├── types.ts # NEW: QueueAdapter interface
|
||||
│ ├── index.ts # NEW: Factory function
|
||||
│ └── adapters/
|
||||
│ ├── bullmq.ts # MOVED from queue.ts
|
||||
│ └── local.ts # NEW: In-process adapter
|
||||
├── storage/ # NEW: Storage abstraction
|
||||
│ ├── src/
|
||||
│ │ ├── types.ts # StorageAdapter interface
|
||||
│ │ ├── index.ts # Factory function
|
||||
│ │ └── adapters/
|
||||
│ │ ├── postgres.ts # MOVED from @mosaicstack/db
|
||||
│ │ ├── sqlite.ts # NEW: SQLite adapter
|
||||
│ │ └── files.ts # NEW: JSON/MD adapter
|
||||
│ └── package.json
|
||||
└── memory/
|
||||
└── src/
|
||||
├── types.ts # UPDATED: MemoryAdapter interface
|
||||
├── index.ts # UPDATED: Factory function
|
||||
└── adapters/
|
||||
├── pgvector.ts # EXTRACTED from existing code
|
||||
├── sqlite-vec.ts # NEW: SQLite with vectors
|
||||
└── keyword.ts # NEW: TF-IDF search
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
|
||||
```
|
||||
packages/
|
||||
├── db/ # DEPRECATED: Logic moved to storage adapters
|
||||
├── queue/
|
||||
│ └── src/
|
||||
│ └── queue.ts # → adapters/bullmq.ts
|
||||
├── memory/
|
||||
│ ├── src/
|
||||
│ │ ├── memory.ts # → use factory
|
||||
│ │ ├── insights.ts # → use factory
|
||||
│ │ └── preferences.ts # → use factory
|
||||
│ └── package.json # Remove pgvector from dependencies
|
||||
└── gateway/
|
||||
└── src/
|
||||
├── database/
|
||||
│ └── database.module.ts # Update to use storage factory
|
||||
├── memory/
|
||||
│ └── memory.module.ts # Update to use memory factory
|
||||
└── queue/
|
||||
└── queue.module.ts # Update to use queue factory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
1. **`@mosaicstack/db`** → **`@mosaicstack/storage`** (with migration guide)
|
||||
2. Direct `ioredis` imports → Use `@mosaicstack/queue` factory
|
||||
3. Direct `pgvector` queries → Use `@mosaicstack/memory` factory
|
||||
4. Gateway startup now requires storage config (defaults to local)
|
||||
|
||||
## Non-Breaking Migration Path
|
||||
|
||||
1. Existing deployments with Postgres/Valkey continue to work (default config)
|
||||
2. New deployments can choose local tier
|
||||
3. Migration commands available when ready to upgrade
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Local tier runs with zero external dependencies
|
||||
- [ ] All three tiers (local, team, enterprise) work correctly
|
||||
- [ ] Factory pattern correctly selects backend at runtime
|
||||
- [ ] Migration from local → team preserves all data
|
||||
- [ ] Dream mode consolidates 24h of sessions
|
||||
- [ ] Documentation covers all three tiers and migration paths
|
||||
- [ ] All existing tests pass
|
||||
- [ ] New adapters have >80% coverage
|
||||
@@ -229,11 +229,11 @@ external clients. Authentication requires a valid BetterAuth session (cookie or
|
||||
|
||||
### Gateway
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------- | ------------------------ | ---------------------------------------------- |
|
||||
| `GATEWAY_PORT` | `14242` | Port the gateway listens on |
|
||||
| `GATEWAY_CORS_ORIGIN` | `http://localhost:3000` | Allowed CORS origin for browser clients |
|
||||
| `BETTER_AUTH_URL` | `http://localhost:14242` | Public URL of the gateway (used by BetterAuth) |
|
||||
| Variable | Default | Description |
|
||||
| --------------------- | ----------------------- | ---------------------------------------------- |
|
||||
| `GATEWAY_PORT` | `4000` | Port the gateway listens on |
|
||||
| `GATEWAY_CORS_ORIGIN` | `http://localhost:3000` | Allowed CORS origin for browser clients |
|
||||
| `BETTER_AUTH_URL` | `http://localhost:4000` | Public URL of the gateway (used by BetterAuth) |
|
||||
|
||||
### SSO (Optional)
|
||||
|
||||
@@ -292,13 +292,13 @@ Each OIDC provider requires its client ID, client secret, and issuer URL togethe
|
||||
|
||||
### Plugins
|
||||
|
||||
| Variable | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------- |
|
||||
| `DISCORD_BOT_TOKEN` | Discord bot token (enables Discord plugin) |
|
||||
| `DISCORD_GUILD_ID` | Discord guild/server ID |
|
||||
| `DISCORD_GATEWAY_URL` | Gateway URL for Discord plugin to call (default: `http://localhost:14242`) |
|
||||
| `TELEGRAM_BOT_TOKEN` | Telegram bot token (enables Telegram plugin) |
|
||||
| `TELEGRAM_GATEWAY_URL` | Gateway URL for Telegram plugin to call |
|
||||
| Variable | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------- |
|
||||
| `DISCORD_BOT_TOKEN` | Discord bot token (enables Discord plugin) |
|
||||
| `DISCORD_GUILD_ID` | Discord guild/server ID |
|
||||
| `DISCORD_GATEWAY_URL` | Gateway URL for Discord plugin to call (default: `http://localhost:4000`) |
|
||||
| `TELEGRAM_BOT_TOKEN` | Telegram bot token (enables Telegram plugin) |
|
||||
| `TELEGRAM_GATEWAY_URL` | Gateway URL for Telegram plugin to call |
|
||||
|
||||
### Observability
|
||||
|
||||
@@ -309,9 +309,9 @@ Each OIDC provider requires its client ID, client secret, and issuer URL togethe
|
||||
|
||||
### Web App
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------- | ------------------------ | -------------------------------------- |
|
||||
| `NEXT_PUBLIC_GATEWAY_URL` | `http://localhost:14242` | Gateway URL used by the Next.js client |
|
||||
| Variable | Default | Description |
|
||||
| ------------------------- | ----------------------- | -------------------------------------- |
|
||||
| `NEXT_PUBLIC_GATEWAY_URL` | `http://localhost:4000` | Gateway URL used by the Next.js client |
|
||||
|
||||
### Coordination
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user