Compare commits
177 Commits
v0.2.0
...
feat/frame
| Author | SHA1 | Date | |
|---|---|---|---|
| ec5e1e346c | |||
| b5a53308df | |||
| 1bfd8570d6 | |||
| 312acd8bad | |||
| d08b969918 | |||
| 051de0d8a9 | |||
| bd76df1a50 | |||
| 62b2ce2da1 | |||
| 172bacb30f | |||
| 43667d7349 | |||
| 783884376c | |||
| c08aa6fa46 | |||
| 0ae932ab34 | |||
| a8cd52e88c | |||
| a4c94d9a90 | |||
| cee838d22e | |||
| 732f8a49cf | |||
| be917e2496 | |||
| cd8b1f666d | |||
| 8fa5995bde | |||
| 25cada7735 | |||
| be6553101c | |||
| 417805f330 | |||
| 2472ce52e8 | |||
| 597eb232d7 | |||
| afe997db82 | |||
| b9d464de61 | |||
| 872c124581 | |||
| a531029c5b | |||
| 35ab619bd0 | |||
| 831193cdd8 | |||
| df460d5a49 | |||
| 119ff0eb1b | |||
| 3abd63ea5c | |||
| 641e4604d5 | |||
|
|
9b5ecc0171 | ||
|
|
a00325da0e | ||
| 4ebce3422d | |||
| 751e0ee330 | |||
| 54b2920ef3 | |||
| 5917016509 | |||
| 7b4f1d249d | |||
| 5425f9268e | |||
| febd866098 | |||
| 2446593fff | |||
| 651426cf2e | |||
| cf46f6e0ae | |||
| 6f15a84ccf | |||
| c39433c361 | |||
| 257796ce87 | |||
|
|
2357602f50 | ||
| 1230f6b984 | |||
| 14b775f1b9 | |||
|
|
c7691d9807 | ||
| 9a53d55678 | |||
|
|
31008ef7ff | ||
| 621ab260c0 | |||
| 2b1840214e | |||
|
|
5cfccc2ead | ||
|
|
774b76447d | ||
| 80994bdc8e | |||
| 2e31626f87 | |||
| 255ba46a4d | |||
| 10285933a0 | |||
| 543388e18b | |||
| 07a1f5d594 | |||
|
|
c6fc090c98 | ||
| 9723b6b948 | |||
| c0d0fd44b7 | |||
| 30c0fb1308 | |||
| 26fac4722f | |||
| e3f64c79d9 | |||
| cbd5e8c626 | |||
| 7560c7dee7 | |||
| 982a0e8f83 | |||
| fc7fa11923 | |||
| 86d6c214fe | |||
| 39ccba95d0 | |||
| 202e375f41 | |||
|
|
d0378c5723 | ||
| d6f04a0757 | |||
| afedb8697e | |||
|
|
1274df7ffc | ||
|
|
1b4767bd8b | ||
| 0b0fe10b37 | |||
| acfb31f8f6 | |||
|
|
fd83bd4f2d | ||
|
|
ce3ca1dbd1 | ||
|
|
95e7b071d4 | ||
| d4c5797a65 | |||
| 70a51ba711 | |||
| db8023bdbb | |||
| 9e597ecf87 | |||
| a23c117ea4 | |||
| 0cf80dab8c | |||
|
|
04a80fb9ba | ||
|
|
626adac363 | ||
|
|
35fbd88a1d | ||
| 381b0eed7b | |||
|
|
25383ea645 | ||
|
|
e7db9ddf98 | ||
|
|
7bb878718d | ||
|
|
46a31d4e71 | ||
|
|
e128a7a322 | ||
|
|
27b1898ec6 | ||
|
|
d19ef45bb0 | ||
|
|
5e852df6c3 | ||
|
|
e0eca771c6 | ||
|
|
9d22ef4cc9 | ||
|
|
41961a6980 | ||
|
|
e797676a02 | ||
|
|
05d61e62be | ||
|
|
73043773d8 | ||
| 0be9729e40 | |||
|
|
e83674ac51 | ||
|
|
a6e59bf829 | ||
| e46f0641f6 | |||
|
|
07efaa9580 | ||
|
|
361fece023 | ||
| 80e69016b0 | |||
|
|
e084a88a9d | ||
| 990a88362f | |||
|
|
ea9782b2dc | ||
| 8efbaf100e | |||
|
|
15830e2f2a | ||
| 04db8591af | |||
|
|
785d30e065 | ||
| e57a10913d | |||
| 0d12471868 | |||
| ea371d760d | |||
|
|
3b9104429b | ||
|
|
8a83aed9b1 | ||
|
|
2f68237046 | ||
|
|
45f5b9062e | ||
| 147f5f1bec | |||
|
|
f05b198882 | ||
| d0a484cbb7 | |||
|
|
6e6ee37da0 | ||
| 53199122d8 | |||
|
|
b38cfac760 | ||
| f3cb3e6852 | |||
|
|
e599f5fe38 | ||
| 6357a3fc9c | |||
|
|
92998e6e65 | ||
| 2394a2a0dd | |||
|
|
13934d4879 | ||
| aa80013811 | |||
|
|
2ee7206c3a | ||
| be74ca3cf9 | |||
| 35123b21ce | |||
| 492dc18e14 | |||
|
|
a824a43ed1 | ||
|
|
9b72f0ea14 | ||
|
|
d367f00077 | ||
| 31a5751c6c | |||
| fa43989cd5 | |||
| 1b317e8a0a | |||
| 316807581c | |||
|
|
3321d4575a | ||
|
|
85d4527701 | ||
|
|
47b7509288 | ||
|
|
34fad9da81 | ||
|
|
48be0aa195 | ||
|
|
f544cc65d2 | ||
|
|
41e8f91b2d | ||
|
|
f161e3cb62 | ||
| da41724490 | |||
|
|
281e636e4d | ||
| 87dcd12a65 | |||
|
|
d3fdc4ff54 | ||
| 9690aba0f5 | |||
|
|
10689a30d2 | ||
| 40c068fcbc | |||
|
|
a9340adad7 | ||
| 5cb72e8ca6 | |||
|
|
48323e7d6e | ||
|
|
01259f56cd |
12
.env.example
12
.env.example
@@ -23,8 +23,8 @@ VALKEY_URL=redis://localhost:6380
|
|||||||
|
|
||||||
|
|
||||||
# ─── Gateway ─────────────────────────────────────────────────────────────────
|
# ─── Gateway ─────────────────────────────────────────────────────────────────
|
||||||
# TCP port the NestJS/Fastify gateway listens on (default: 4000)
|
# TCP port the NestJS/Fastify gateway listens on (default: 14242)
|
||||||
GATEWAY_PORT=4000
|
GATEWAY_PORT=14242
|
||||||
|
|
||||||
# Comma-separated list of allowed CORS origins.
|
# Comma-separated list of allowed CORS origins.
|
||||||
# Must include the web app origin in production.
|
# 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
|
BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
|
||||||
|
|
||||||
# Public base URL of the gateway (used by BetterAuth for callback URLs)
|
# Public base URL of the gateway (used by BetterAuth for callback URLs)
|
||||||
BETTER_AUTH_URL=http://localhost:4000
|
BETTER_AUTH_URL=http://localhost:14242
|
||||||
|
|
||||||
|
|
||||||
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
|
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
|
||||||
# Public gateway URL — accessible from the browser, not just the server.
|
# Public gateway URL — accessible from the browser, not just the server.
|
||||||
NEXT_PUBLIC_GATEWAY_URL=http://localhost:4000
|
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
|
||||||
|
|
||||||
|
|
||||||
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
||||||
@@ -121,12 +121,12 @@ OTEL_SERVICE_NAME=mosaic-gateway
|
|||||||
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
|
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
|
||||||
# DISCORD_BOT_TOKEN=
|
# DISCORD_BOT_TOKEN=
|
||||||
# DISCORD_GUILD_ID=
|
# DISCORD_GUILD_ID=
|
||||||
# DISCORD_GATEWAY_URL=http://localhost:4000
|
# DISCORD_GATEWAY_URL=http://localhost:14242
|
||||||
|
|
||||||
|
|
||||||
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
|
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
|
||||||
# TELEGRAM_BOT_TOKEN=
|
# TELEGRAM_BOT_TOKEN=
|
||||||
# TELEGRAM_GATEWAY_URL=http://localhost:4000
|
# TELEGRAM_GATEWAY_URL=http://localhost:14242
|
||||||
|
|
||||||
|
|
||||||
# ─── SSO Providers (add credentials to enable) ───────────────────────────────
|
# ─── SSO Providers (add credentials to enable) ───────────────────────────────
|
||||||
|
|||||||
2
.npmrc
2
.npmrc
@@ -1 +1 @@
|
|||||||
@mosaic:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/
|
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ steps:
|
|||||||
image: *node_image
|
image: *node_image
|
||||||
commands:
|
commands:
|
||||||
- corepack enable
|
- corepack enable
|
||||||
|
- apk add --no-cache python3 make g++
|
||||||
- pnpm install --frozen-lockfile
|
- pnpm install --frozen-lockfile
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
@@ -44,18 +45,30 @@ steps:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://mosaic:mosaic@postgres:5432/mosaic
|
||||||
commands:
|
commands:
|
||||||
- *enable_pnpm
|
- *enable_pnpm
|
||||||
|
# Install postgresql-client for pg_isready
|
||||||
|
- apk add --no-cache postgresql-client
|
||||||
|
# Wait up to 30s for postgres to be ready
|
||||||
|
- |
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
pg_isready -h postgres -p 5432 -U mosaic && break
|
||||||
|
echo "Waiting for postgres ($i/30)..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
# Run migrations (DATABASE_URL is set in environment above)
|
||||||
|
- pnpm --filter @mosaicstack/db run db:migrate
|
||||||
|
# Run all tests
|
||||||
- pnpm test
|
- pnpm test
|
||||||
depends_on:
|
depends_on:
|
||||||
- typecheck
|
- typecheck
|
||||||
|
|
||||||
build:
|
services:
|
||||||
image: *node_image
|
postgres:
|
||||||
commands:
|
image: pgvector/pgvector:pg17
|
||||||
- *enable_pnpm
|
environment:
|
||||||
- pnpm build
|
POSTGRES_USER: mosaic
|
||||||
depends_on:
|
POSTGRES_PASSWORD: mosaic
|
||||||
- lint
|
POSTGRES_DB: mosaic
|
||||||
- format
|
|
||||||
- test
|
|
||||||
|
|||||||
140
.woodpecker/publish.yml
Normal file
140
.woodpecker/publish.yml
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Build, publish npm packages, and push Docker images
|
||||||
|
# Runs only on main branch push/tag
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- &node_image 'node:22-alpine'
|
||||||
|
- &enable_pnpm 'corepack enable'
|
||||||
|
|
||||||
|
when:
|
||||||
|
- branch: [main]
|
||||||
|
event: [push, manual, tag]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
install:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: *node_image
|
||||||
|
commands:
|
||||||
|
- *enable_pnpm
|
||||||
|
- pnpm build
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
image: *node_image
|
||||||
|
environment:
|
||||||
|
NPM_TOKEN:
|
||||||
|
from_secret: gitea_token
|
||||||
|
commands:
|
||||||
|
- *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
|
||||||
|
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:
|
||||||
|
REGISTRY_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
REGISTRY_PASS:
|
||||||
|
from_secret: gitea_password
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
||||||
|
commands:
|
||||||
|
- 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/stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
|
||||||
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:latest"
|
||||||
|
fi
|
||||||
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:$CI_COMMIT_TAG"
|
||||||
|
fi
|
||||||
|
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
build-web:
|
||||||
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
|
environment:
|
||||||
|
REGISTRY_USER:
|
||||||
|
from_secret: gitea_username
|
||||||
|
REGISTRY_PASS:
|
||||||
|
from_secret: gitea_password
|
||||||
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||||
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||||
|
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
||||||
|
commands:
|
||||||
|
- 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/stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
||||||
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
|
||||||
|
fi
|
||||||
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
|
||||||
|
fi
|
||||||
|
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
28
AGENTS.md
28
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 |
|
| `apps/web` | Next.js dashboard | React 19, Tailwind |
|
||||||
| `packages/types` | Shared TypeScript contracts | class-validator |
|
| `packages/types` | Shared TypeScript contracts | class-validator |
|
||||||
| `packages/db` | Drizzle ORM schema + migrations | drizzle-orm, postgres |
|
| `packages/db` | Drizzle ORM schema + migrations | drizzle-orm, postgres |
|
||||||
| `packages/auth` | BetterAuth configuration | better-auth, @mosaic/db |
|
| `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db |
|
||||||
| `packages/brain` | Data layer (PG-backed) | @mosaic/db |
|
| `packages/brain` | Data layer (PG-backed) | @mosaicstack/db |
|
||||||
| `packages/queue` | Valkey task queue + MCP | ioredis |
|
| `packages/queue` | Valkey task queue + MCP | ioredis |
|
||||||
| `packages/coord` | Mission coordination | @mosaic/queue |
|
| `packages/coord` | Mission coordination | @mosaicstack/queue |
|
||||||
| `packages/cli` | Unified CLI + Pi TUI | Ink, Pi SDK |
|
| `packages/mosaic` | Unified `mosaic` CLI + TUI | Ink, Pi SDK, commander |
|
||||||
| `plugins/discord` | Discord channel plugin | discord.js |
|
| `plugins/discord` | Discord channel plugin | discord.js |
|
||||||
| `plugins/telegram` | Telegram channel plugin | Telegraf |
|
| `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
|
1. Gateway is the single API surface — all clients connect through it
|
||||||
2. Pi SDK is ESM-only — gateway and CLI must use ESM
|
2. Pi SDK is ESM-only — gateway and CLI must use ESM
|
||||||
3. Socket.IO typed events defined in `@mosaic/types` enforce compile-time contracts
|
3. Socket.IO typed events defined in `@mosaicstack/types` enforce compile-time contracts
|
||||||
4. OTEL auto-instrumentation loads before NestJS bootstrap
|
4. OTEL auto-instrumentation loads before NestJS bootstrap
|
||||||
5. BetterAuth manages auth tables; schema defined in `@mosaic/db`
|
5. BetterAuth manages auth tables; schema defined in `@mosaicstack/db`
|
||||||
6. Docker Compose provides PG (5433), Valkey (6380), OTEL Collector (4317/4318), Jaeger (16686)
|
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)
|
7. Explicit `@Inject()` decorators required in NestJS (tsx/esbuild doesn't emit decorator metadata)
|
||||||
|
|
||||||
@@ -58,14 +58,14 @@ pnpm typecheck && pnpm lint && pnpm format:check # Quality gates
|
|||||||
|
|
||||||
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
|
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
|
||||||
|
|
||||||
| Value | When to use | Budget |
|
| Value | When to use | Budget |
|
||||||
| -------- | ----------------------------------------------------------- | -------------------------- |
|
| --------- | ----------------------------------------------------------- | -------------------------- |
|
||||||
| `codex` | All coding tasks (default for implementation) | OpenAI credits — preferred |
|
| `codex` | All coding tasks (default for implementation) | OpenAI credits — preferred |
|
||||||
| `glm-5` | Cost-sensitive coding where Codex is unavailable | Z.ai credits |
|
| `glm-5.1` | Cost-sensitive coding where Codex is unavailable | Z.ai credits |
|
||||||
| `haiku` | Review gates, verify tasks, status checks, docs-only | Cheapest Claude tier |
|
| `haiku` | Review gates, verify tasks, status checks, docs-only | Cheapest Claude tier |
|
||||||
| `sonnet` | Complex planning, multi-file reasoning, architecture review | Claude quota |
|
| `sonnet` | Complex planning, multi-file reasoning, architecture review | Claude quota |
|
||||||
| `opus` | Major cross-cutting architecture decisions ONLY | Most expensive — minimize |
|
| `opus` | Major cross-cutting architecture decisions ONLY | Most expensive — minimize |
|
||||||
| `—` | No preference / auto-select cheapest capable | Pipeline decides |
|
| `—` | No preference / auto-select cheapest capable | Pipeline decides |
|
||||||
|
|
||||||
Pipeline crons read this column and spawn accordingly. Workers never modify `docs/TASKS.md` — only the orchestrator writes it.
|
Pipeline crons read this column and spawn accordingly. Workers never modify `docs/TASKS.md` — only the orchestrator writes it.
|
||||||
|
|
||||||
|
|||||||
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`)
|
- **Web**: Next.js 16 + React 19 (`apps/web`)
|
||||||
- **ORM**: Drizzle ORM + PostgreSQL 17 + pgvector (`packages/db`)
|
- **ORM**: Drizzle ORM + PostgreSQL 17 + pgvector (`packages/db`)
|
||||||
- **Auth**: BetterAuth (`packages/auth`)
|
- **Auth**: BetterAuth (`packages/auth`)
|
||||||
- **Agent**: Pi SDK (`packages/agent`, `packages/cli`)
|
- **Agent**: Pi SDK (`packages/agent`, `packages/mosaic`)
|
||||||
- **Queue**: Valkey 8 (`packages/queue`)
|
- **Queue**: Valkey 8 (`packages/queue`)
|
||||||
- **Build**: pnpm workspaces + Turborepo
|
- **Build**: pnpm workspaces + Turborepo
|
||||||
- **CI**: Woodpecker CI
|
- **CI**: Woodpecker CI
|
||||||
@@ -26,13 +26,13 @@ pnpm test # Vitest (all packages)
|
|||||||
pnpm build # Build all packages
|
pnpm build # Build all packages
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
pnpm --filter @mosaic/db db:push # Push schema to PG (dev)
|
pnpm --filter @mosaicstack/db db:push # Push schema to PG (dev)
|
||||||
pnpm --filter @mosaic/db db:generate # Generate migrations
|
pnpm --filter @mosaicstack/db db:generate # Generate migrations
|
||||||
pnpm --filter @mosaic/db db:migrate # Run migrations
|
pnpm --filter @mosaicstack/db db:migrate # Run migrations
|
||||||
|
|
||||||
# Dev
|
# Dev
|
||||||
docker compose up -d # Start PG, Valkey, OTEL, Jaeger
|
docker compose up -d # Start PG, Valkey, OTEL, Jaeger
|
||||||
pnpm --filter @mosaic/gateway exec tsx src/main.ts # Start gateway
|
pnpm --filter @mosaicstack/gateway exec tsx src/main.ts # Start gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|||||||
360
README.md
Normal file
360
README.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# Mosaic Stack
|
||||||
|
|
||||||
|
Self-hosted, multi-user AI agent platform. One config, every runtime, same standards.
|
||||||
|
|
||||||
|
Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi — injecting consistent system prompts, guardrails, skills, and mission context into every session. A NestJS gateway provides the API surface, a Next.js dashboard gives you the UI, and a plugin system connects Discord, Telegram, and more.
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the direct URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL …) --yes # Accept all defaults
|
||||||
|
bash <(curl -fsSL …) --yes --no-auto-launch # Install only, skip wizard
|
||||||
|
```
|
||||||
|
|
||||||
|
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/` |
|
||||||
|
|
||||||
|
After install, the wizard runs automatically or you can invoke it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosaic wizard # Full guided setup (gateway install → verify)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Node.js ≥ 20
|
||||||
|
- npm (for global @mosaicstack/mosaic 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
|
||||||
|
|
||||||
|
### Launching Agent Sessions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosaic pi # Launch Pi with Mosaic injection
|
||||||
|
mosaic claude # Launch Claude Code with Mosaic injection
|
||||||
|
mosaic codex # Launch Codex with Mosaic injection
|
||||||
|
mosaic opencode # Launch OpenCode with Mosaic injection
|
||||||
|
|
||||||
|
mosaic yolo claude # Claude with dangerous-permissions mode
|
||||||
|
mosaic yolo pi # Pi in yolo mode
|
||||||
|
```
|
||||||
|
|
||||||
|
The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md` standards into the runtime, and forwards all arguments.
|
||||||
|
|
||||||
|
### TUI & Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosaic tui # Interactive TUI connected to the gateway
|
||||||
|
mosaic gateway login # Authenticate with a gateway instance
|
||||||
|
mosaic sessions list # List active agent sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gateway Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosaic gateway install # Install and configure the gateway service
|
||||||
|
mosaic gateway verify # Post-install health check
|
||||||
|
mosaic gateway login # Authenticate and store a session token
|
||||||
|
mosaic gateway config rotate-token # Rotate your API token
|
||||||
|
mosaic gateway config recover-token # Recover a token via BetterAuth cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have a gateway account but no token, use `mosaic gateway config recover-token` to retrieve one without recreating your account.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosaic config show # Print full config as JSON
|
||||||
|
mosaic config get <key> # Read a specific key
|
||||||
|
mosaic config set <key> <val># Write a key
|
||||||
|
mosaic config edit # Open config in $EDITOR
|
||||||
|
mosaic config path # Print config file path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosaic doctor # Health audit — detect drift and missing files
|
||||||
|
mosaic sync # Sync skills from canonical source
|
||||||
|
mosaic update # Check for and install CLI updates
|
||||||
|
mosaic wizard # Full guided setup wizard
|
||||||
|
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
|
||||||
|
mosaic coord init # Initialize a new orchestration mission
|
||||||
|
mosaic prdy init # Create a PRD via guided session
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sub-package Commands
|
||||||
|
|
||||||
|
Each Mosaic sub-package exposes its API surface through the unified CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# User management
|
||||||
|
mosaic auth users list
|
||||||
|
mosaic auth users create
|
||||||
|
mosaic auth sso
|
||||||
|
|
||||||
|
# Agent brain (projects, missions, tasks)
|
||||||
|
mosaic brain projects
|
||||||
|
mosaic brain missions
|
||||||
|
mosaic brain tasks
|
||||||
|
mosaic brain conversations
|
||||||
|
|
||||||
|
# Agent forge pipeline
|
||||||
|
mosaic forge run
|
||||||
|
mosaic forge status
|
||||||
|
mosaic forge resume
|
||||||
|
mosaic forge personas
|
||||||
|
|
||||||
|
# Structured logging
|
||||||
|
mosaic log tail
|
||||||
|
mosaic log search
|
||||||
|
mosaic log export
|
||||||
|
mosaic log level
|
||||||
|
|
||||||
|
# MACP protocol
|
||||||
|
mosaic macp tasks
|
||||||
|
mosaic macp submit
|
||||||
|
mosaic macp gate
|
||||||
|
mosaic macp events
|
||||||
|
|
||||||
|
# Agent memory
|
||||||
|
mosaic memory search
|
||||||
|
mosaic memory stats
|
||||||
|
mosaic memory insights
|
||||||
|
mosaic memory preferences
|
||||||
|
|
||||||
|
# Task queue (Valkey)
|
||||||
|
mosaic queue list
|
||||||
|
mosaic queue stats
|
||||||
|
mosaic queue pause
|
||||||
|
mosaic queue resume
|
||||||
|
mosaic queue jobs
|
||||||
|
mosaic queue drain
|
||||||
|
|
||||||
|
# Object storage
|
||||||
|
mosaic storage status
|
||||||
|
mosaic storage tier
|
||||||
|
mosaic storage export
|
||||||
|
mosaic storage import
|
||||||
|
mosaic storage migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telemetry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local observability (OTEL / Jaeger)
|
||||||
|
mosaic telemetry local status
|
||||||
|
mosaic telemetry local tail
|
||||||
|
mosaic telemetry local jaeger
|
||||||
|
|
||||||
|
# Remote telemetry (dry-run by default)
|
||||||
|
mosaic telemetry status
|
||||||
|
mosaic telemetry opt-in
|
||||||
|
mosaic telemetry opt-out
|
||||||
|
mosaic telemetry test
|
||||||
|
mosaic telemetry upload # Dry-run unless opted in
|
||||||
|
```
|
||||||
|
|
||||||
|
Consent state is persisted in config. Remote upload is a no-op until you run `mosaic telemetry opt-in`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js ≥ 20
|
||||||
|
- pnpm 10.6+
|
||||||
|
- Docker & Docker Compose
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@git.mosaicstack.dev:mosaicstack/stack.git
|
||||||
|
cd stack
|
||||||
|
|
||||||
|
# Start infrastructure (Postgres, Valkey, Jaeger)
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
pnpm --filter @mosaicstack/db run db:migrate
|
||||||
|
|
||||||
|
# Start all services in dev mode
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
|
||||||
|
Docker Compose provides:
|
||||||
|
|
||||||
|
| Service | Port | Purpose |
|
||||||
|
| --------------------- | --------- | ---------------------- |
|
||||||
|
| PostgreSQL (pgvector) | 5433 | Primary database |
|
||||||
|
| Valkey | 6380 | Task queue + caching |
|
||||||
|
| Jaeger | 16686 | Distributed tracing UI |
|
||||||
|
| OTEL Collector | 4317/4318 | Telemetry ingestion |
|
||||||
|
|
||||||
|
### Quality Gates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck # TypeScript type checking (all packages)
|
||||||
|
pnpm lint # ESLint (all packages)
|
||||||
|
pnpm test # Vitest (all packages)
|
||||||
|
pnpm format:check # Prettier check
|
||||||
|
pnpm format # Prettier auto-fix
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
Woodpecker CI runs on every push:
|
||||||
|
|
||||||
|
- `pnpm install --frozen-lockfile`
|
||||||
|
- Database migration against a fresh Postgres
|
||||||
|
- `pnpm test` (Turbo-orchestrated across all packages)
|
||||||
|
|
||||||
|
npm packages are published to the Gitea package registry on main merges.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
stack/
|
||||||
|
├── apps/
|
||||||
|
│ ├── gateway/ NestJS API + WebSocket hub (Fastify, Socket.IO, OTEL)
|
||||||
|
│ └── web/ Next.js dashboard (React 19, Tailwind)
|
||||||
|
├── packages/
|
||||||
|
│ ├── mosaic/ Unified CLI — TUI, gateway client, wizard, sub-package commands
|
||||||
|
│ ├── types/ Shared TypeScript contracts (Socket.IO typed events)
|
||||||
|
│ ├── db/ Drizzle ORM schema + migrations (pgvector)
|
||||||
|
│ ├── auth/ BetterAuth configuration
|
||||||
|
│ ├── brain/ Data layer (PG-backed)
|
||||||
|
│ ├── queue/ Valkey task queue + MCP
|
||||||
|
│ ├── coord/ Mission coordination
|
||||||
|
│ ├── forge/ Multi-stage AI pipeline (intake → board → plan → code → review)
|
||||||
|
│ ├── macp/ MACP protocol — credential resolution, gate runner, events
|
||||||
|
│ ├── agent/ Agent session management
|
||||||
|
│ ├── memory/ Agent memory layer
|
||||||
|
│ ├── log/ Structured logging
|
||||||
|
│ ├── prdy/ PRD creation and validation
|
||||||
|
│ ├── quality-rails/ Quality templates (TypeScript, Next.js, monorepo)
|
||||||
|
│ └── design-tokens/ Shared design tokens
|
||||||
|
├── plugins/
|
||||||
|
│ ├── discord/ Discord channel plugin (discord.js)
|
||||||
|
│ ├── telegram/ Telegram channel plugin (Telegraf)
|
||||||
|
│ ├── macp/ OpenClaw MACP runtime plugin
|
||||||
|
│ └── mosaic-framework/ OpenClaw framework injection plugin
|
||||||
|
├── tools/
|
||||||
|
│ └── install.sh Unified installer (framework + npm CLI, --yes / --no-auto-launch)
|
||||||
|
├── scripts/agent/ Agent session lifecycle scripts
|
||||||
|
├── docker-compose.yml Dev infrastructure
|
||||||
|
└── .woodpecker/ CI pipeline configs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Design Decisions
|
||||||
|
|
||||||
|
- **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
|
||||||
|
- **OTEL auto-instrumentation** — loads before NestJS bootstrap
|
||||||
|
- **Explicit `@Inject()` decorators** — required since tsx/esbuild doesn't emit decorator metadata
|
||||||
|
|
||||||
|
### Framework (`~/.config/mosaic/`)
|
||||||
|
|
||||||
|
The framework is the bash-based standards layer installed to every developer machine:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.config/mosaic/
|
||||||
|
├── AGENTS.md ← Central standards (loaded into every runtime)
|
||||||
|
├── SOUL.md ← Agent identity (name, style, guardrails)
|
||||||
|
├── USER.md ← User profile (name, timezone, preferences)
|
||||||
|
├── TOOLS.md ← Machine-level tool reference
|
||||||
|
├── bin/mosaic ← Unified launcher (claude, codex, opencode, pi, yolo)
|
||||||
|
├── guides/ ← E2E delivery, orchestrator protocol, PRD, etc.
|
||||||
|
├── runtime/ ← Per-runtime configs (claude/, codex/, opencode/, pi/)
|
||||||
|
├── skills/ ← Universal skills (synced from agent-skills repo)
|
||||||
|
├── tools/ ← Tool suites (orchestrator, git, quality, prdy, etc.)
|
||||||
|
└── memory/ ← Persistent agent memory (preserved across upgrades)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forge Pipeline
|
||||||
|
|
||||||
|
Forge is a multi-stage AI pipeline for autonomous feature delivery:
|
||||||
|
|
||||||
|
```
|
||||||
|
Intake → Discovery → Board Review → Planning (3 stages) → Coding → Review → Remediation → Test → Deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding), quality gates, and timeouts. The board review uses multiple AI personas (CEO, CTO, CFO, COO + specialists) to evaluate briefs before committing resources.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
Run the installer again — it handles upgrades automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the direct URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosaic update # Check + install CLI updates
|
||||||
|
mosaic update --check # Check only, don't install
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI also performs a background update check on every invocation (cached for 1 hour).
|
||||||
|
|
||||||
|
### Installer Flags
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash tools/install.sh --check # Version check only
|
||||||
|
bash tools/install.sh --framework # Framework only (skip npm CLI)
|
||||||
|
bash tools/install.sh --cli # npm CLI only (skip framework)
|
||||||
|
bash tools/install.sh --ref v1.0 # Install from a specific git ref
|
||||||
|
bash tools/install.sh --yes # Non-interactive, accept all defaults
|
||||||
|
bash tools/install.sh --no-auto-launch # Skip auto-launch of wizard
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a feature branch
|
||||||
|
git checkout -b feat/my-feature
|
||||||
|
|
||||||
|
# Make changes, then verify
|
||||||
|
pnpm typecheck && pnpm lint && pnpm test && pnpm format:check
|
||||||
|
|
||||||
|
# Commit (husky runs lint-staged automatically)
|
||||||
|
git commit -m "feat: description of change"
|
||||||
|
|
||||||
|
# Push and create PR
|
||||||
|
git push -u origin feat/my-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
DTOs go in `*.dto.ts` files at module boundaries. Scratchpads (`docs/scratchpads/`) are mandatory for non-trivial tasks. See `AGENTS.md` for the full standards reference.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Proprietary — all rights reserved.
|
||||||
@@ -1,9 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/gateway",
|
"name": "@mosaicstack/gateway",
|
||||||
"version": "0.0.0",
|
"version": "0.0.6",
|
||||||
"private": true,
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
|
"directory": "apps/gateway"
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/main.js",
|
"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": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsx watch src/main.ts",
|
"dev": "tsx watch src/main.ts",
|
||||||
@@ -14,26 +28,28 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.80.0",
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@fastify/helmet": "^13.0.2",
|
"@fastify/helmet": "^13.0.2",
|
||||||
"@mariozechner/pi-ai": "~0.57.1",
|
"@mariozechner/pi-ai": "^0.65.0",
|
||||||
"@mariozechner/pi-coding-agent": "~0.57.1",
|
"@mariozechner/pi-coding-agent": "^0.65.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
"@mosaic/auth": "workspace:^",
|
"@mosaicstack/auth": "workspace:^",
|
||||||
"@mosaic/brain": "workspace:^",
|
"@mosaicstack/brain": "workspace:^",
|
||||||
"@mosaic/coord": "workspace:^",
|
"@mosaicstack/config": "workspace:^",
|
||||||
"@mosaic/db": "workspace:^",
|
"@mosaicstack/coord": "workspace:^",
|
||||||
"@mosaic/discord-plugin": "workspace:^",
|
"@mosaicstack/db": "workspace:^",
|
||||||
"@mosaic/log": "workspace:^",
|
"@mosaicstack/discord-plugin": "workspace:^",
|
||||||
"@mosaic/memory": "workspace:^",
|
"@mosaicstack/log": "workspace:^",
|
||||||
"@mosaic/queue": "workspace:^",
|
"@mosaicstack/memory": "workspace:^",
|
||||||
"@mosaic/telegram-plugin": "workspace:^",
|
"@mosaicstack/queue": "workspace:^",
|
||||||
"@mosaic/types": "workspace:^",
|
"@mosaicstack/storage": "workspace:^",
|
||||||
|
"@mosaicstack/telegram-plugin": "workspace:^",
|
||||||
|
"@mosaicstack/types": "workspace:^",
|
||||||
"@nestjs/common": "^11.0.0",
|
"@nestjs/common": "^11.0.0",
|
||||||
"@nestjs/core": "^11.0.0",
|
"@nestjs/core": "^11.0.0",
|
||||||
"@nestjs/platform-fastify": "^11.0.0",
|
"@nestjs/platform-fastify": "^11.0.0",
|
||||||
"@nestjs/platform-socket.io": "^11.0.0",
|
"@nestjs/platform-socket.io": "^11.0.0",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/websockets": "^11.0.0",
|
"@nestjs/websockets": "^11.0.0",
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
|
"@opentelemetry/auto-instrumentations-node": "^0.72.0",
|
||||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
|
||||||
"@opentelemetry/resources": "^2.6.0",
|
"@opentelemetry/resources": "^2.6.0",
|
||||||
@@ -56,11 +72,17 @@
|
|||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nestjs/testing": "^11.1.18",
|
||||||
|
"@swc/core": "^1.15.24",
|
||||||
|
"@swc/helpers": "^0.5.21",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@types/supertest": "^7.2.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
"supertest": "^7.2.2",
|
||||||
"tsx": "^4.0.0",
|
"tsx": "^4.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
|
"unplugin-swc": "^1.5.9",
|
||||||
"vitest": "^2.0.0"
|
"vitest": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { BadRequestException, NotFoundException } from '@nestjs/common';
|
|||||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
import type { ConversationHistoryMessage } from '../agent/agent.service.js';
|
import type { ConversationHistoryMessage } from '../agent/agent.service.js';
|
||||||
import { ConversationsController } from '../conversations/conversations.controller.js';
|
import { ConversationsController } from '../conversations/conversations.controller.js';
|
||||||
import type { Message } from '@mosaic/brain';
|
import type { Message } from '@mosaicstack/brain';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Shared test data
|
// Shared test data
|
||||||
|
|||||||
@@ -18,13 +18,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
import { createDb } from '@mosaic/db';
|
import { createDb } from '@mosaicstack/db';
|
||||||
import { createConversationsRepo } from '@mosaic/brain';
|
import { createConversationsRepo } from '@mosaicstack/brain';
|
||||||
import { createAgentsRepo } from '@mosaic/brain';
|
import { createAgentsRepo } from '@mosaicstack/brain';
|
||||||
import { createPreferencesRepo, createInsightsRepo } from '@mosaic/memory';
|
import { createPreferencesRepo, createInsightsRepo } from '@mosaicstack/memory';
|
||||||
import { users, conversations, messages, agents, preferences, insights } from '@mosaic/db';
|
import { users, conversations, messages, agents, preferences, insights } from '@mosaicstack/db';
|
||||||
import { eq } from '@mosaic/db';
|
import { eq } from '@mosaicstack/db';
|
||||||
import type { DbHandle } from '@mosaic/db';
|
import type { DbHandle } from '@mosaicstack/db';
|
||||||
|
|
||||||
// ─── Fixed IDs so the afterAll cleanup is deterministic ──────────────────────
|
// ─── Fixed IDs so the afterAll cleanup is deterministic ──────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||||
import { sql, type Db } from '@mosaic/db';
|
import { sql, type Db } from '@mosaicstack/db';
|
||||||
import { createQueue } from '@mosaic/queue';
|
import { createQueue } from '@mosaicstack/queue';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
import { AgentService } from '../agent/agent.service.js';
|
import { AgentService } from '../agent/agent.service.js';
|
||||||
import { ProviderService } from '../agent/provider.service.js';
|
import { ProviderService } from '../agent/provider.service.js';
|
||||||
|
|||||||
90
apps/gateway/src/admin/admin-tokens.controller.ts
Normal file
90
apps/gateway/src/admin/admin-tokens.controller.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
33
apps/gateway/src/admin/admin-tokens.dto.ts
Normal file
33
apps/gateway/src/admin/admin-tokens.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { eq, type Db, users as usersTable } from '@mosaic/db';
|
import { eq, type Db, users as usersTable } from '@mosaicstack/db';
|
||||||
import type { Auth } from '@mosaic/auth';
|
import type { Auth } from '@mosaicstack/auth';
|
||||||
import { AUTH } from '../auth/auth.tokens.js';
|
import { AUTH } from '../auth/auth.tokens.js';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
import { AdminGuard } from './admin.guard.js';
|
import { AdminGuard } from './admin.guard.js';
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
import { fromNodeHeaders } from 'better-auth/node';
|
import { fromNodeHeaders } from 'better-auth/node';
|
||||||
import type { Auth } from '@mosaic/auth';
|
import type { Auth } from '@mosaicstack/auth';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaicstack/db';
|
||||||
import { eq, users as usersTable } from '@mosaic/db';
|
import { eq, adminTokens, users as usersTable } from '@mosaicstack/db';
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
import { AUTH } from '../auth/auth.tokens.js';
|
import { AUTH } from '../auth/auth.tokens.js';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
@@ -19,6 +20,8 @@ interface UserWithRole {
|
|||||||
role?: string;
|
role?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthenticatedRequest = FastifyRequest & { user: unknown; session: unknown };
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminGuard implements CanActivate {
|
export class AdminGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -28,8 +31,64 @@ export class AdminGuard implements CanActivate {
|
|||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
const headers = fromNodeHeaders(request.raw.headers);
|
|
||||||
|
|
||||||
|
// 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 });
|
const result = await this.auth.api.getSession({ headers });
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -38,8 +97,6 @@ export class AdminGuard implements CanActivate {
|
|||||||
|
|
||||||
const user = result.user as UserWithRole;
|
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;
|
let userRole = user.role;
|
||||||
if (!userRole) {
|
if (!userRole) {
|
||||||
const [dbUser] = await this.db
|
const [dbUser] = await this.db
|
||||||
@@ -48,7 +105,6 @@ export class AdminGuard implements CanActivate {
|
|||||||
.where(eq(usersTable.id, user.id))
|
.where(eq(usersTable.id, user.id))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
userRole = dbUser?.role ?? 'member';
|
userRole = dbUser?.role ?? 'member';
|
||||||
// Update the session user object with the fetched role
|
|
||||||
(user as UserWithRole).role = userRole;
|
(user as UserWithRole).role = userRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +112,9 @@ export class AdminGuard implements CanActivate {
|
|||||||
throw new ForbiddenException('Admin access required');
|
throw new ForbiddenException('Admin access required');
|
||||||
}
|
}
|
||||||
|
|
||||||
(request as FastifyRequest & { user: unknown; session: unknown }).user = result.user;
|
const req = request as AuthenticatedRequest;
|
||||||
(request as FastifyRequest & { user: unknown; session: unknown }).session = result.session;
|
req.user = result.user;
|
||||||
|
req.session = result.session;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AdminController } from './admin.controller.js';
|
import { AdminController } from './admin.controller.js';
|
||||||
import { AdminHealthController } from './admin-health.controller.js';
|
import { AdminHealthController } from './admin-health.controller.js';
|
||||||
import { AdminJobsController } from './admin-jobs.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';
|
import { AdminGuard } from './admin.guard.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AdminController, AdminHealthController, AdminJobsController],
|
controllers: [
|
||||||
|
AdminController,
|
||||||
|
AdminHealthController,
|
||||||
|
AdminJobsController,
|
||||||
|
AdminTokensController,
|
||||||
|
BootstrapController,
|
||||||
|
],
|
||||||
providers: [AdminGuard],
|
providers: [AdminGuard],
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|||||||
102
apps/gateway/src/admin/bootstrap.controller.ts
Normal file
102
apps/gateway/src/admin/bootstrap.controller.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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 { BootstrapSetupDto } from './bootstrap.dto.js';
|
||||||
|
import type { 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/gateway/src/admin/bootstrap.dto.ts
Normal file
31
apps/gateway/src/admin/bootstrap.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
190
apps/gateway/src/admin/bootstrap.e2e.spec.ts
Normal file
190
apps/gateway/src/admin/bootstrap.e2e.spec.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* E2E integration test — POST /api/bootstrap/setup
|
||||||
|
*
|
||||||
|
* Regression guard for the `import type { BootstrapSetupDto }` class-erasure
|
||||||
|
* bug (IUV-M01, issue #436).
|
||||||
|
*
|
||||||
|
* When `BootstrapSetupDto` is imported with `import type`, TypeScript erases
|
||||||
|
* the class at compile time. NestJS then sees `Object` as the `@Body()`
|
||||||
|
* metatype, and ValidationPipe with `whitelist:true + forbidNonWhitelisted:true`
|
||||||
|
* treats every property as non-whitelisted, returning:
|
||||||
|
*
|
||||||
|
* 400 { message: ["property email should not exist", "property password should not exist"] }
|
||||||
|
*
|
||||||
|
* The fix is a plain value import (`import { BootstrapSetupDto }`), which
|
||||||
|
* preserves the class reference so Nest can read the class-validator decorators.
|
||||||
|
*
|
||||||
|
* This test MUST fail if `import type` is re-introduced on `BootstrapSetupDto`.
|
||||||
|
* A controller unit test that constructs ValidationPipe manually won't catch
|
||||||
|
* this — only the real DI binding path exercises the metatype lookup.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'reflect-metadata';
|
||||||
|
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { ValidationPipe, type INestApplication } from '@nestjs/common';
|
||||||
|
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { BootstrapController } from './bootstrap.controller.js';
|
||||||
|
import type { BootstrapResultDto } from './bootstrap.dto.js';
|
||||||
|
|
||||||
|
// ─── Minimal mock dependencies ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We use explicit `@Inject(AUTH)` / `@Inject(DB)` in the controller so we
|
||||||
|
* can provide mock values by token without spinning up the real DB or Auth.
|
||||||
|
*/
|
||||||
|
import { AUTH } from '../auth/auth.tokens.js';
|
||||||
|
import { DB } from '../database/database.module.js';
|
||||||
|
|
||||||
|
const MOCK_USER_ID = 'mock-user-id-001';
|
||||||
|
|
||||||
|
const mockAuth = {
|
||||||
|
api: {
|
||||||
|
createUser: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
user: {
|
||||||
|
id: MOCK_USER_ID,
|
||||||
|
name: 'Admin',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override db.select() so the second query (verify user exists) returns a user.
|
||||||
|
// The bootstrap controller calls select().from() twice:
|
||||||
|
// 1. count() to check zero users → returns [{total: 0}]
|
||||||
|
// 2. select().where().limit() → returns [the created user]
|
||||||
|
let selectCallCount = 0;
|
||||||
|
const mockDbWithUser = {
|
||||||
|
select: () => {
|
||||||
|
selectCallCount++;
|
||||||
|
return {
|
||||||
|
from: () => {
|
||||||
|
if (selectCallCount === 1) {
|
||||||
|
// First call: count — zero users
|
||||||
|
return Promise.resolve([{ total: 0 }]);
|
||||||
|
}
|
||||||
|
// Subsequent calls: return a mock user row
|
||||||
|
return {
|
||||||
|
where: () => ({
|
||||||
|
limit: () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
id: MOCK_USER_ID,
|
||||||
|
name: 'Admin',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update: () => ({
|
||||||
|
set: () => ({
|
||||||
|
where: () => Promise.resolve([]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
insert: () => ({
|
||||||
|
values: () => ({
|
||||||
|
returning: () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
id: 'token-id-001',
|
||||||
|
label: 'Initial setup token',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Test suite ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('POST /api/bootstrap/setup — ValidationPipe DTO binding', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
selectCallCount = 0;
|
||||||
|
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
controllers: [BootstrapController],
|
||||||
|
providers: [
|
||||||
|
{ provide: AUTH, useValue: mockAuth },
|
||||||
|
{ provide: DB, useValue: mockDbWithUser },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
|
||||||
|
|
||||||
|
// Mirror main.ts configuration exactly — this is what reproduced the 400.
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
// Fastify requires waiting for the adapter to be ready
|
||||||
|
await app.getHttpAdapter().getInstance().ready();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 201 (not 400) when a valid {name, email, password} body is sent', async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/api/bootstrap/setup')
|
||||||
|
.send({ name: 'Admin', email: 'admin@example.com', password: 'password123' })
|
||||||
|
.set('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
// Before the fix (import type), Nest ValidationPipe returned 400 with
|
||||||
|
// "property email should not exist" / "property password should not exist"
|
||||||
|
// because the DTO class was erased and every field looked non-whitelisted.
|
||||||
|
expect(res.status).not.toBe(400);
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const body = res.body as BootstrapResultDto;
|
||||||
|
expect(body.user).toBeDefined();
|
||||||
|
expect(body.user.email).toBe('admin@example.com');
|
||||||
|
expect(body.token).toBeDefined();
|
||||||
|
expect(body.token.plaintext).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 400 when extra forbidden properties are sent', async () => {
|
||||||
|
// This proves ValidationPipe IS active and working (forbidNonWhitelisted).
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/api/bootstrap/setup')
|
||||||
|
.send({
|
||||||
|
name: 'Admin',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
extraField: 'should-be-rejected',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 400 when email is invalid', async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/api/bootstrap/setup')
|
||||||
|
.send({ name: 'Admin', email: 'not-an-email', password: 'password123' })
|
||||||
|
.set('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 400 when password is too short', async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/api/bootstrap/setup')
|
||||||
|
.send({ name: 'Admin', email: 'admin@example.com', password: 'short' })
|
||||||
|
.set('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -62,7 +62,7 @@ function restoreEnv(saved: Map<EnvKey, string | undefined>): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeRegistry(): ModelRegistry {
|
function makeRegistry(): ModelRegistry {
|
||||||
return new ModelRegistry(AuthStorage.inMemory());
|
return ModelRegistry.inMemory(AuthStorage.inMemory());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { RoutingService } from '../routing.service.js';
|
import { RoutingService } from '../routing.service.js';
|
||||||
import type { ModelInfo } from '@mosaic/types';
|
import type { ModelInfo } from '@mosaicstack/types';
|
||||||
|
|
||||||
const mockModels: ModelInfo[] = [
|
const mockModels: ModelInfo[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
IProviderAdapter,
|
IProviderAdapter,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
ProviderHealth,
|
ProviderHealth,
|
||||||
} from '@mosaic/types';
|
} from '@mosaicstack/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anthropic provider adapter.
|
* Anthropic provider adapter.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
IProviderAdapter,
|
IProviderAdapter,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
ProviderHealth,
|
ProviderHealth,
|
||||||
} from '@mosaic/types';
|
} from '@mosaicstack/types';
|
||||||
|
|
||||||
/** Embedding models that Ollama ships with out of the box */
|
/** Embedding models that Ollama ships with out of the box */
|
||||||
const OLLAMA_EMBEDDING_MODELS: ReadonlyArray<{
|
const OLLAMA_EMBEDDING_MODELS: ReadonlyArray<{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
IProviderAdapter,
|
IProviderAdapter,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
ProviderHealth,
|
ProviderHealth,
|
||||||
} from '@mosaic/types';
|
} from '@mosaicstack/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenAI provider adapter.
|
* OpenAI provider adapter.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
IProviderAdapter,
|
IProviderAdapter,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
ProviderHealth,
|
ProviderHealth,
|
||||||
} from '@mosaic/types';
|
} from '@mosaicstack/types';
|
||||||
|
|
||||||
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
IProviderAdapter,
|
IProviderAdapter,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
ProviderHealth,
|
ProviderHealth,
|
||||||
} from '@mosaic/types';
|
} from '@mosaicstack/types';
|
||||||
import { getModelCapability } from '../model-capabilities.js';
|
import { getModelCapability } from '../model-capabilities.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
type AgentSessionEvent,
|
type AgentSessionEvent,
|
||||||
type ToolDefinition,
|
type ToolDefinition,
|
||||||
} from '@mariozechner/pi-coding-agent';
|
} from '@mariozechner/pi-coding-agent';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import type { Memory } from '@mosaic/memory';
|
import type { Memory } from '@mosaicstack/memory';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { MEMORY } from '../memory/memory.tokens.js';
|
import { MEMORY } from '../memory/memory.tokens.js';
|
||||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||||
@@ -23,6 +23,7 @@ import { createFileTools } from './tools/file-tools.js';
|
|||||||
import { createGitTools } from './tools/git-tools.js';
|
import { createGitTools } from './tools/git-tools.js';
|
||||||
import { createShellTools } from './tools/shell-tools.js';
|
import { createShellTools } from './tools/shell-tools.js';
|
||||||
import { createWebTools } from './tools/web-tools.js';
|
import { createWebTools } from './tools/web-tools.js';
|
||||||
|
import { createSearchTools } from './tools/search-tools.js';
|
||||||
import type { SessionInfoDto, SessionMetrics } from './session.dto.js';
|
import type { SessionInfoDto, SessionMetrics } from './session.dto.js';
|
||||||
import { SystemOverrideService } from '../preferences/system-override.service.js';
|
import { SystemOverrideService } from '../preferences/system-override.service.js';
|
||||||
import { PreferencesService } from '../preferences/preferences.service.js';
|
import { PreferencesService } from '../preferences/preferences.service.js';
|
||||||
@@ -146,6 +147,7 @@ export class AgentService implements OnModuleDestroy {
|
|||||||
...createGitTools(sandboxDir),
|
...createGitTools(sandboxDir),
|
||||||
...createShellTools(sandboxDir),
|
...createShellTools(sandboxDir),
|
||||||
...createWebTools(),
|
...createWebTools(),
|
||||||
|
...createSearchTools(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ModelCapability } from '@mosaic/types';
|
import type { ModelCapability } from '@mosaicstack/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comprehensive capability matrix for all target models.
|
* Comprehensive capability matrix for all target models.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaicstack/db';
|
||||||
import { providerCredentials, eq, and } from '@mosaic/db';
|
import { providerCredentials, eq, and } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
import type { ProviderCredentialSummaryDto } from './provider-credentials.dto.js';
|
import type { ProviderCredentialSummaryDto } from './provider-credentials.dto.js';
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
ModelInfo,
|
ModelInfo,
|
||||||
ProviderHealth,
|
ProviderHealth,
|
||||||
ProviderInfo,
|
ProviderInfo,
|
||||||
} from '@mosaic/types';
|
} from '@mosaicstack/types';
|
||||||
import {
|
import {
|
||||||
AnthropicAdapter,
|
AnthropicAdapter,
|
||||||
OllamaAdapter,
|
OllamaAdapter,
|
||||||
@@ -67,7 +67,7 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
|
|||||||
|
|
||||||
async onModuleInit(): Promise<void> {
|
async onModuleInit(): Promise<void> {
|
||||||
const authStorage = AuthStorage.inMemory();
|
const authStorage = AuthStorage.inMemory();
|
||||||
this.registry = new ModelRegistry(authStorage);
|
this.registry = ModelRegistry.inMemory(authStorage);
|
||||||
|
|
||||||
// Build the default set of adapters that rely on the registry
|
// Build the default set of adapters that rely on the registry
|
||||||
this.adapters = [
|
this.adapters = [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Inject, Param, Post, UseGuards } from '@nestjs/common';
|
||||||
import type { RoutingCriteria } from '@mosaic/types';
|
import type { RoutingCriteria } from '@mosaicstack/types';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
import { ProviderService } from './provider.service.js';
|
import { ProviderService } from './provider.service.js';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import type { ModelInfo } from '@mosaic/types';
|
import type { ModelInfo } from '@mosaicstack/types';
|
||||||
import type { RoutingCriteria, RoutingResult, CostTier } from '@mosaic/types';
|
import type { RoutingCriteria, RoutingResult, CostTier } from '@mosaicstack/types';
|
||||||
import { ProviderService } from './provider.service.js';
|
import { ProviderService } from './provider.service.js';
|
||||||
|
|
||||||
/** Per-million-token cost thresholds for tier classification */
|
/** Per-million-token cost thresholds for tier classification */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||||
import { routingRules, type Db, sql } from '@mosaic/db';
|
import { routingRules, type Db, sql } from '@mosaicstack/db';
|
||||||
import { DB } from '../../database/database.module.js';
|
import { DB } from '../../database/database.module.js';
|
||||||
import type { RoutingCondition, RoutingAction } from './routing.types.js';
|
import type { RoutingCondition, RoutingAction } from './routing.types.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { routingRules, type Db, and, asc, eq, or } from '@mosaic/db';
|
import { routingRules, type Db, and, asc, eq, or } from '@mosaicstack/db';
|
||||||
import { DB } from '../../database/database.module.js';
|
import { DB } from '../../database/database.module.js';
|
||||||
import { ProviderService } from '../provider.service.js';
|
import { ProviderService } from '../provider.service.js';
|
||||||
import { classifyTask } from './task-classifier.js';
|
import { classifyTask } from './task-classifier.js';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaic/db';
|
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaicstack/db';
|
||||||
import { DB } from '../../database/database.module.js';
|
import { DB } from '../../database/database.module.js';
|
||||||
import { AuthGuard } from '../../auth/auth.guard.js';
|
import { AuthGuard } from '../../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../../auth/current-user.decorator.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).
|
* Routing engine types — M4-002 (condition types) and M4-003 (action types).
|
||||||
*
|
*
|
||||||
* These types are re-exported from `@mosaic/types` for shared use across packages.
|
* These types are re-exported from `@mosaicstack/types` for shared use across packages.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ─── Classification primitives ───────────────────────────────────────────────
|
// ─── Classification primitives ───────────────────────────────────────────────
|
||||||
@@ -23,7 +23,7 @@ export type Domain = 'frontend' | 'backend' | 'devops' | 'docs' | 'general';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cost tier for model selection.
|
* Cost tier for model selection.
|
||||||
* Extends the existing `CostTier` in `@mosaic/types` with `local` for self-hosted models.
|
* Extends the existing `CostTier` in `@mosaicstack/types` with `local` for self-hosted models.
|
||||||
*/
|
*/
|
||||||
export type CostTier = 'cheap' | 'standard' | 'premium' | 'local';
|
export type CostTier = 'cheap' | 'standard' | 'premium' | 'local';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Type } from '@sinclair/typebox';
|
import { Type } from '@sinclair/typebox';
|
||||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
|
|
||||||
export function createBrainTools(brain: Brain): ToolDefinition[] {
|
export function createBrainTools(brain: Brain): ToolDefinition[] {
|
||||||
const listProjects: ToolDefinition = {
|
const listProjects: ToolDefinition = {
|
||||||
|
|||||||
@@ -190,5 +190,169 @@ export function createFileTools(baseDir: string): ToolDefinition[] {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return [readFileTool, writeFileTool, listDirectoryTool];
|
const editFileTool: ToolDefinition = {
|
||||||
|
name: 'fs_edit_file',
|
||||||
|
label: 'Edit File',
|
||||||
|
description:
|
||||||
|
'Make targeted text replacements in a file. Each edit replaces an exact match of oldText with newText. ' +
|
||||||
|
'All edits are matched against the original file content (not incrementally). ' +
|
||||||
|
'Each oldText must be unique in the file and edits must not overlap.',
|
||||||
|
parameters: Type.Object({
|
||||||
|
path: Type.String({
|
||||||
|
description: 'File path (relative to sandbox base or absolute within it)',
|
||||||
|
}),
|
||||||
|
edits: Type.Array(
|
||||||
|
Type.Object({
|
||||||
|
oldText: Type.String({
|
||||||
|
description: 'Exact text to find and replace (must be unique in the file)',
|
||||||
|
}),
|
||||||
|
newText: Type.String({ description: 'Replacement text' }),
|
||||||
|
}),
|
||||||
|
{ description: 'One or more targeted replacements', minItems: 1 },
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async execute(_toolCallId, params) {
|
||||||
|
const { path, edits } = params as {
|
||||||
|
path: string;
|
||||||
|
edits: Array<{ oldText: string; newText: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let safePath: string;
|
||||||
|
try {
|
||||||
|
safePath = guardPath(path, baseDir);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof SandboxEscapeError) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Error: ${err.message}` }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await stat(safePath);
|
||||||
|
if (!info.isFile()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Error: path is not a file: ${path}` }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (info.size > MAX_READ_BYTES) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Error: file too large for editing (${info.size} bytes, limit ${MAX_READ_BYTES} bytes)`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Error reading file: ${String(err)}` }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: string;
|
||||||
|
try {
|
||||||
|
content = await readFile(safePath, { encoding: 'utf8' });
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Error reading file: ${String(err)}` }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all edits before applying any
|
||||||
|
const errors: string[] = [];
|
||||||
|
for (let i = 0; i < edits.length; i++) {
|
||||||
|
const edit = edits[i]!;
|
||||||
|
const occurrences = content.split(edit.oldText).length - 1;
|
||||||
|
if (occurrences === 0) {
|
||||||
|
errors.push(`Edit ${i + 1}: oldText not found in file`);
|
||||||
|
} else if (occurrences > 1) {
|
||||||
|
errors.push(`Edit ${i + 1}: oldText matches ${occurrences} locations (must be unique)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overlapping edits
|
||||||
|
if (errors.length === 0) {
|
||||||
|
const positions = edits.map((edit, i) => ({
|
||||||
|
index: i,
|
||||||
|
start: content.indexOf(edit.oldText),
|
||||||
|
end: content.indexOf(edit.oldText) + edit.oldText.length,
|
||||||
|
}));
|
||||||
|
positions.sort((a, b) => a.start - b.start);
|
||||||
|
for (let i = 1; i < positions.length; i++) {
|
||||||
|
if (positions[i]!.start < positions[i - 1]!.end) {
|
||||||
|
errors.push(
|
||||||
|
`Edits ${positions[i - 1]!.index + 1} and ${positions[i]!.index + 1} overlap`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Edit validation failed:\n${errors.join('\n')}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply edits: process from end to start to preserve positions
|
||||||
|
const positions = edits.map((edit) => ({
|
||||||
|
edit,
|
||||||
|
start: content.indexOf(edit.oldText),
|
||||||
|
}));
|
||||||
|
positions.sort((a, b) => b.start - a.start); // reverse order
|
||||||
|
|
||||||
|
let result = content;
|
||||||
|
for (const { edit } of positions) {
|
||||||
|
result = result.replace(edit.oldText, edit.newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer.byteLength(result, 'utf8') > MAX_WRITE_BYTES) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Error: resulting file too large (limit ${MAX_WRITE_BYTES} bytes)`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile(safePath, result, { encoding: 'utf8' });
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `File edited successfully: ${path} (${edits.length} edit(s) applied)`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Error writing file: ${String(err)}` }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [readFileTool, writeFileTool, listDirectoryTool, editFileTool];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { createBrainTools } from './brain-tools.js';
|
|||||||
export { createCoordTools } from './coord-tools.js';
|
export { createCoordTools } from './coord-tools.js';
|
||||||
export { createFileTools } from './file-tools.js';
|
export { createFileTools } from './file-tools.js';
|
||||||
export { createGitTools } from './git-tools.js';
|
export { createGitTools } from './git-tools.js';
|
||||||
|
export { createSearchTools } from './search-tools.js';
|
||||||
export { createShellTools } from './shell-tools.js';
|
export { createShellTools } from './shell-tools.js';
|
||||||
export { createWebTools } from './web-tools.js';
|
export { createWebTools } from './web-tools.js';
|
||||||
export { createSkillTools } from './skill-tools.js';
|
export { createSkillTools } from './skill-tools.js';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from '@sinclair/typebox';
|
import { Type } from '@sinclair/typebox';
|
||||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||||
import type { Memory } from '@mosaic/memory';
|
import type { Memory } from '@mosaicstack/memory';
|
||||||
import type { EmbeddingProvider } from '@mosaic/memory';
|
import type { EmbeddingProvider } from '@mosaicstack/memory';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create memory tools bound to the session's authenticated userId.
|
* Create memory tools bound to the session's authenticated userId.
|
||||||
|
|||||||
496
apps/gateway/src/agent/tools/search-tools.ts
Normal file
496
apps/gateway/src/agent/tools/search-tools.ts
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
||||||
|
const MAX_RESULTS = 10;
|
||||||
|
const MAX_RESPONSE_BYTES = 256 * 1024; // 256 KB
|
||||||
|
|
||||||
|
// ─── Provider helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
snippet: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResponse {
|
||||||
|
provider: string;
|
||||||
|
query: string;
|
||||||
|
results: SearchResult[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithTimeout(
|
||||||
|
url: string,
|
||||||
|
init: RequestInit,
|
||||||
|
timeoutMs: number,
|
||||||
|
): Promise<Response> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
return await fetch(url, { ...init, signal: controller.signal });
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLimited(response: Response): Promise<string> {
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) return '';
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let total = 0;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
total += value.length;
|
||||||
|
if (total > MAX_RESPONSE_BYTES) {
|
||||||
|
chunks.push(value.subarray(0, MAX_RESPONSE_BYTES - (total - value.length)));
|
||||||
|
reader.cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
chunks.push(value);
|
||||||
|
}
|
||||||
|
const combined = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0));
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
combined.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Brave Search ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function searchBrave(query: string, limit: number): Promise<SearchResponse> {
|
||||||
|
const apiKey = process.env['BRAVE_API_KEY'];
|
||||||
|
if (!apiKey) return { provider: 'brave', query, results: [], error: 'BRAVE_API_KEY not set' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
count: String(Math.min(limit, 20)),
|
||||||
|
});
|
||||||
|
const res = await fetchWithTimeout(
|
||||||
|
`https://api.search.brave.com/res/v1/web/search?${params}`,
|
||||||
|
{ headers: { 'X-Subscription-Token': apiKey, Accept: 'application/json' } },
|
||||||
|
DEFAULT_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text().catch(() => '');
|
||||||
|
return { provider: 'brave', query, results: [], error: `HTTP ${res.status}: ${body}` };
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
web?: { results?: Array<{ title: string; url: string; description: string }> };
|
||||||
|
};
|
||||||
|
const results: SearchResult[] = (data.web?.results ?? []).slice(0, limit).map((r) => ({
|
||||||
|
title: r.title,
|
||||||
|
url: r.url,
|
||||||
|
snippet: r.description,
|
||||||
|
}));
|
||||||
|
return { provider: 'brave', query, results };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
provider: 'brave',
|
||||||
|
query,
|
||||||
|
results: [],
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tavily Search ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function searchTavily(query: string, limit: number): Promise<SearchResponse> {
|
||||||
|
const apiKey = process.env['TAVILY_API_KEY'];
|
||||||
|
if (!apiKey) return { provider: 'tavily', query, results: [], error: 'TAVILY_API_KEY not set' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchWithTimeout(
|
||||||
|
'https://api.tavily.com/search',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
api_key: apiKey,
|
||||||
|
query,
|
||||||
|
max_results: Math.min(limit, 10),
|
||||||
|
include_answer: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
DEFAULT_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text().catch(() => '');
|
||||||
|
return { provider: 'tavily', query, results: [], error: `HTTP ${res.status}: ${body}` };
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
results?: Array<{ title: string; url: string; content: string }>;
|
||||||
|
};
|
||||||
|
const results: SearchResult[] = (data.results ?? []).slice(0, limit).map((r) => ({
|
||||||
|
title: r.title,
|
||||||
|
url: r.url,
|
||||||
|
snippet: r.content,
|
||||||
|
}));
|
||||||
|
return { provider: 'tavily', query, results };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
provider: 'tavily',
|
||||||
|
query,
|
||||||
|
results: [],
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SearXNG (self-hosted) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function searchSearxng(query: string, limit: number): Promise<SearchResponse> {
|
||||||
|
const baseUrl = process.env['SEARXNG_URL'];
|
||||||
|
if (!baseUrl) return { provider: 'searxng', query, results: [], error: 'SEARXNG_URL not set' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
format: 'json',
|
||||||
|
pageno: '1',
|
||||||
|
});
|
||||||
|
const res = await fetchWithTimeout(
|
||||||
|
`${baseUrl.replace(/\/$/, '')}/search?${params}`,
|
||||||
|
{ headers: { Accept: 'application/json' } },
|
||||||
|
DEFAULT_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text().catch(() => '');
|
||||||
|
return { provider: 'searxng', query, results: [], error: `HTTP ${res.status}: ${body}` };
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
results?: Array<{ title: string; url: string; content: string }>;
|
||||||
|
};
|
||||||
|
const results: SearchResult[] = (data.results ?? []).slice(0, limit).map((r) => ({
|
||||||
|
title: r.title,
|
||||||
|
url: r.url,
|
||||||
|
snippet: r.content,
|
||||||
|
}));
|
||||||
|
return { provider: 'searxng', query, results };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
provider: 'searxng',
|
||||||
|
query,
|
||||||
|
results: [],
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DuckDuckGo (lite HTML endpoint) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
async function searchDuckDuckGo(query: string, limit: number): Promise<SearchResponse> {
|
||||||
|
try {
|
||||||
|
// Use the DuckDuckGo Instant Answer API (JSON, free, no key)
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
format: 'json',
|
||||||
|
no_html: '1',
|
||||||
|
skip_disambig: '1',
|
||||||
|
});
|
||||||
|
const res = await fetchWithTimeout(
|
||||||
|
`https://api.duckduckgo.com/?${params}`,
|
||||||
|
{ headers: { Accept: 'application/json' } },
|
||||||
|
DEFAULT_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
return {
|
||||||
|
provider: 'duckduckgo',
|
||||||
|
query,
|
||||||
|
results: [],
|
||||||
|
error: `HTTP ${res.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const text = await readLimited(res);
|
||||||
|
const data = JSON.parse(text) as {
|
||||||
|
AbstractText?: string;
|
||||||
|
AbstractURL?: string;
|
||||||
|
AbstractSource?: string;
|
||||||
|
RelatedTopics?: Array<{
|
||||||
|
Text?: string;
|
||||||
|
FirstURL?: string;
|
||||||
|
Result?: string;
|
||||||
|
Topics?: Array<{ Text?: string; FirstURL?: string }>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const results: SearchResult[] = [];
|
||||||
|
|
||||||
|
// Main abstract result
|
||||||
|
if (data.AbstractText && data.AbstractURL) {
|
||||||
|
results.push({
|
||||||
|
title: data.AbstractSource ?? 'DuckDuckGo Abstract',
|
||||||
|
url: data.AbstractURL,
|
||||||
|
snippet: data.AbstractText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Related topics
|
||||||
|
for (const topic of data.RelatedTopics ?? []) {
|
||||||
|
if (results.length >= limit) break;
|
||||||
|
if (topic.Text && topic.FirstURL) {
|
||||||
|
results.push({
|
||||||
|
title: topic.Text.slice(0, 120),
|
||||||
|
url: topic.FirstURL,
|
||||||
|
snippet: topic.Text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sub-topics
|
||||||
|
for (const sub of topic.Topics ?? []) {
|
||||||
|
if (results.length >= limit) break;
|
||||||
|
if (sub.Text && sub.FirstURL) {
|
||||||
|
results.push({
|
||||||
|
title: sub.Text.slice(0, 120),
|
||||||
|
url: sub.FirstURL,
|
||||||
|
snippet: sub.Text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { provider: 'duckduckgo', query, results: results.slice(0, limit) };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
provider: 'duckduckgo',
|
||||||
|
query,
|
||||||
|
results: [],
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Provider resolution ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SearchProvider = 'brave' | 'tavily' | 'searxng' | 'duckduckgo' | 'auto';
|
||||||
|
|
||||||
|
function getAvailableProviders(): SearchProvider[] {
|
||||||
|
const available: SearchProvider[] = [];
|
||||||
|
if (process.env['BRAVE_API_KEY']) available.push('brave');
|
||||||
|
if (process.env['TAVILY_API_KEY']) available.push('tavily');
|
||||||
|
if (process.env['SEARXNG_URL']) available.push('searxng');
|
||||||
|
// DuckDuckGo is always available (no API key needed)
|
||||||
|
available.push('duckduckgo');
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSearch(
|
||||||
|
provider: SearchProvider,
|
||||||
|
query: string,
|
||||||
|
limit: number,
|
||||||
|
): Promise<SearchResponse> {
|
||||||
|
switch (provider) {
|
||||||
|
case 'brave':
|
||||||
|
return searchBrave(query, limit);
|
||||||
|
case 'tavily':
|
||||||
|
return searchTavily(query, limit);
|
||||||
|
case 'searxng':
|
||||||
|
return searchSearxng(query, limit);
|
||||||
|
case 'duckduckgo':
|
||||||
|
return searchDuckDuckGo(query, limit);
|
||||||
|
case 'auto': {
|
||||||
|
// Try providers in priority order: Brave > Tavily > SearXNG > DuckDuckGo
|
||||||
|
const available = getAvailableProviders();
|
||||||
|
for (const p of available) {
|
||||||
|
const result = await executeSearch(p, query, limit);
|
||||||
|
if (!result.error && result.results.length > 0) return result;
|
||||||
|
}
|
||||||
|
// Fall back to DuckDuckGo if everything failed
|
||||||
|
return searchDuckDuckGo(query, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSearchResults(response: SearchResponse): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`Search provider: ${response.provider}`);
|
||||||
|
lines.push(`Query: "${response.query}"`);
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
lines.push(`Error: ${response.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.results.length === 0) {
|
||||||
|
lines.push('No results found.');
|
||||||
|
} else {
|
||||||
|
lines.push(`Results (${response.results.length}):\n`);
|
||||||
|
for (let i = 0; i < response.results.length; i++) {
|
||||||
|
const r = response.results[i]!;
|
||||||
|
lines.push(`${i + 1}. ${r.title}`);
|
||||||
|
lines.push(` URL: ${r.url}`);
|
||||||
|
lines.push(` ${r.snippet}`);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tool exports ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function createSearchTools(): ToolDefinition[] {
|
||||||
|
const webSearch: ToolDefinition = {
|
||||||
|
name: 'web_search',
|
||||||
|
label: 'Web Search',
|
||||||
|
description:
|
||||||
|
'Search the web using configured search providers. ' +
|
||||||
|
'Supports Brave, Tavily, SearXNG, and DuckDuckGo. ' +
|
||||||
|
'Use "auto" provider to pick the best available. ' +
|
||||||
|
'DuckDuckGo is always available as a fallback (no API key needed).',
|
||||||
|
parameters: Type.Object({
|
||||||
|
query: Type.String({ description: 'Search query' }),
|
||||||
|
provider: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
description:
|
||||||
|
'Search provider: "auto" (default), "brave", "tavily", "searxng", or "duckduckgo"',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
limit: Type.Optional(
|
||||||
|
Type.Number({ description: `Max results to return (default 5, max ${MAX_RESULTS})` }),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async execute(_toolCallId, params) {
|
||||||
|
const { query, provider, limit } = params as {
|
||||||
|
query: string;
|
||||||
|
provider?: string;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const effectiveProvider = (provider ?? 'auto') as SearchProvider;
|
||||||
|
const validProviders = ['auto', 'brave', 'tavily', 'searxng', 'duckduckgo'];
|
||||||
|
if (!validProviders.includes(effectiveProvider)) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Invalid provider "${provider}". Valid: ${validProviders.join(', ')}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveLimit = Math.min(Math.max(limit ?? 5, 1), MAX_RESULTS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await executeSearch(effectiveProvider, query, effectiveLimit);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: formatSearchResults(response) }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const webSearchNews: ToolDefinition = {
|
||||||
|
name: 'web_search_news',
|
||||||
|
label: 'Web Search (News)',
|
||||||
|
description:
|
||||||
|
'Search for recent news articles. Uses Brave News API if available, falls back to standard search with news keywords.',
|
||||||
|
parameters: Type.Object({
|
||||||
|
query: Type.String({ description: 'News search query' }),
|
||||||
|
limit: Type.Optional(
|
||||||
|
Type.Number({ description: `Max results (default 5, max ${MAX_RESULTS})` }),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async execute(_toolCallId, params) {
|
||||||
|
const { query, limit } = params as { query: string; limit?: number };
|
||||||
|
const effectiveLimit = Math.min(Math.max(limit ?? 5, 1), MAX_RESULTS);
|
||||||
|
|
||||||
|
// Try Brave News API first (dedicated news endpoint)
|
||||||
|
const braveKey = process.env['BRAVE_API_KEY'];
|
||||||
|
if (braveKey) {
|
||||||
|
try {
|
||||||
|
const newsParams = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
count: String(effectiveLimit),
|
||||||
|
});
|
||||||
|
const res = await fetchWithTimeout(
|
||||||
|
`https://api.search.brave.com/res/v1/news/search?${newsParams}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-Subscription-Token': braveKey,
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DEFAULT_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
results?: Array<{
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
description: string;
|
||||||
|
age?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
const results: SearchResult[] = (data.results ?? [])
|
||||||
|
.slice(0, effectiveLimit)
|
||||||
|
.map((r) => ({
|
||||||
|
title: r.title + (r.age ? ` (${r.age})` : ''),
|
||||||
|
url: r.url,
|
||||||
|
snippet: r.description,
|
||||||
|
}));
|
||||||
|
const response: SearchResponse = { provider: 'brave-news', query, results };
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: formatSearchResults(response) }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to generic search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: standard search with "news" appended
|
||||||
|
const newsQuery = `${query} news latest`;
|
||||||
|
const response = await executeSearch('auto', newsQuery, effectiveLimit);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: formatSearchResults(response) }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchProviders: ToolDefinition = {
|
||||||
|
name: 'web_search_providers',
|
||||||
|
label: 'List Search Providers',
|
||||||
|
description: 'List the currently available and configured web search providers.',
|
||||||
|
parameters: Type.Object({}),
|
||||||
|
async execute() {
|
||||||
|
const available = getAvailableProviders();
|
||||||
|
const allProviders = [
|
||||||
|
{ name: 'brave', configured: !!process.env['BRAVE_API_KEY'], envVar: 'BRAVE_API_KEY' },
|
||||||
|
{ name: 'tavily', configured: !!process.env['TAVILY_API_KEY'], envVar: 'TAVILY_API_KEY' },
|
||||||
|
{ name: 'searxng', configured: !!process.env['SEARXNG_URL'], envVar: 'SEARXNG_URL' },
|
||||||
|
{ name: 'duckduckgo', configured: true, envVar: '(none — always available)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const lines = ['Search providers:\n'];
|
||||||
|
for (const p of allProviders) {
|
||||||
|
const status = p.configured ? '✓ configured' : '✗ not configured';
|
||||||
|
lines.push(` ${p.name}: ${status} (${p.envVar})`);
|
||||||
|
}
|
||||||
|
lines.push(`\nActive providers for "auto" mode: ${available.join(', ')}`);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: lines.join('\n') }],
|
||||||
|
details: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [webSearch, webSearchNews, searchProviders];
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { APP_GUARD } from '@nestjs/core';
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { HealthController } from './health/health.controller.js';
|
import { HealthController } from './health/health.controller.js';
|
||||||
|
import { ConfigModule } from './config/config.module.js';
|
||||||
import { DatabaseModule } from './database/database.module.js';
|
import { DatabaseModule } from './database/database.module.js';
|
||||||
import { AuthModule } from './auth/auth.module.js';
|
import { AuthModule } from './auth/auth.module.js';
|
||||||
import { BrainModule } from './brain/brain.module.js';
|
import { BrainModule } from './brain/brain.module.js';
|
||||||
@@ -28,6 +29,7 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
|
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
|
||||||
|
ConfigModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
BrainModule,
|
BrainModule,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||||
import { toNodeHandler } from 'better-auth/node';
|
import { toNodeHandler } from 'better-auth/node';
|
||||||
import type { Auth } from '@mosaic/auth';
|
import type { Auth } from '@mosaicstack/auth';
|
||||||
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
import { AUTH } from './auth.tokens.js';
|
import { AUTH } from './auth.tokens.js';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { fromNodeHeaders } from 'better-auth/node';
|
import { fromNodeHeaders } from 'better-auth/node';
|
||||||
import type { Auth } from '@mosaic/auth';
|
import type { Auth } from '@mosaicstack/auth';
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
import { AUTH } from './auth.tokens.js';
|
import { AUTH } from './auth.tokens.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { createAuth, type Auth } from '@mosaic/auth';
|
import { createAuth, type Auth } from '@mosaicstack/auth';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
import { AUTH } from './auth.tokens.js';
|
import { AUTH } from './auth.tokens.js';
|
||||||
import { SsoController } from './sso.controller.js';
|
import { SsoController } from './sso.controller.js';
|
||||||
@@ -14,7 +14,7 @@ import { SsoController } from './sso.controller.js';
|
|||||||
useFactory: (db: Db): Auth =>
|
useFactory: (db: Db): Auth =>
|
||||||
createAuth({
|
createAuth({
|
||||||
db,
|
db,
|
||||||
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:4000',
|
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242',
|
||||||
secret: process.env['BETTER_AUTH_SECRET'],
|
secret: process.env['BETTER_AUTH_SECRET'],
|
||||||
}),
|
}),
|
||||||
inject: [DB],
|
inject: [DB],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { buildSsoDiscovery, type SsoProviderDiscovery } from '@mosaic/auth';
|
import { buildSsoDiscovery, type SsoProviderDiscovery } from '@mosaicstack/auth';
|
||||||
|
|
||||||
@Controller('api/sso/providers')
|
@Controller('api/sso/providers')
|
||||||
export class SsoController {
|
export class SsoController {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { createBrain, type Brain } from '@mosaic/brain';
|
import { createBrain, type Brain } from '@mosaicstack/brain';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
import { BRAIN } from './brain.tokens.js';
|
import { BRAIN } from './brain.tokens.js';
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ import {
|
|||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
||||||
import type { Auth } from '@mosaic/auth';
|
import type { Auth } from '@mosaicstack/auth';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import type {
|
import type {
|
||||||
SetThinkingPayload,
|
SetThinkingPayload,
|
||||||
SlashCommandPayload,
|
SlashCommandPayload,
|
||||||
SystemReloadPayload,
|
SystemReloadPayload,
|
||||||
RoutingDecisionInfo,
|
RoutingDecisionInfo,
|
||||||
} from '@mosaic/types';
|
AbortPayload,
|
||||||
|
} from '@mosaicstack/types';
|
||||||
import { AgentService, type ConversationHistoryMessage } from '../agent/agent.service.js';
|
import { AgentService, type ConversationHistoryMessage } from '../agent/agent.service.js';
|
||||||
import { AUTH } from '../auth/auth.tokens.js';
|
import { AUTH } from '../auth/auth.tokens.js';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
@@ -325,6 +326,38 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('abort')
|
||||||
|
async handleAbort(
|
||||||
|
@ConnectedSocket() client: Socket,
|
||||||
|
@MessageBody() data: AbortPayload,
|
||||||
|
): Promise<void> {
|
||||||
|
const conversationId = data.conversationId;
|
||||||
|
this.logger.log(`Abort requested by ${client.id} for conversation ${conversationId}`);
|
||||||
|
|
||||||
|
const session = this.agentService.getSession(conversationId);
|
||||||
|
if (!session) {
|
||||||
|
client.emit('error', {
|
||||||
|
conversationId,
|
||||||
|
error: 'No active session to abort.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.piSession.abort();
|
||||||
|
this.logger.log(`Agent session ${conversationId} aborted successfully`);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to abort session ${conversationId}`,
|
||||||
|
err instanceof Error ? err.stack : String(err),
|
||||||
|
);
|
||||||
|
client.emit('error', {
|
||||||
|
conversationId,
|
||||||
|
error: 'Failed to abort the agent operation.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SubscribeMessage('command:execute')
|
@SubscribeMessage('command:execute')
|
||||||
async handleCommandExecute(
|
async handleCommandExecute(
|
||||||
@ConnectedSocket() client: Socket,
|
@ConnectedSocket() client: Socket,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { CommandExecutorService } from './command-executor.service.js';
|
import { CommandExecutorService } from './command-executor.service.js';
|
||||||
import type { SlashCommandPayload } from '@mosaic/types';
|
import type { SlashCommandPayload } from '@mosaicstack/types';
|
||||||
|
|
||||||
// Minimal mock implementations
|
// Minimal mock implementations
|
||||||
const mockRegistry = {
|
const mockRegistry = {
|
||||||
@@ -82,6 +82,7 @@ function buildService(): CommandExecutorService {
|
|||||||
mockBrain as never,
|
mockBrain as never,
|
||||||
null,
|
null,
|
||||||
mockChatGateway as never,
|
mockChatGateway as never,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { forwardRef, Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
||||||
import type { QueueHandle } from '@mosaic/queue';
|
import type { QueueHandle } from '@mosaicstack/queue';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaic/types';
|
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaicstack/types';
|
||||||
import { AgentService } from '../agent/agent.service.js';
|
import { AgentService } from '../agent/agent.service.js';
|
||||||
import { ChatGateway } from '../chat/chat.gateway.js';
|
import { ChatGateway } from '../chat/chat.gateway.js';
|
||||||
import { SessionGCService } from '../gc/session-gc.service.js';
|
import { SessionGCService } from '../gc/session-gc.service.js';
|
||||||
import { SystemOverrideService } from '../preferences/system-override.service.js';
|
import { SystemOverrideService } from '../preferences/system-override.service.js';
|
||||||
import { ReloadService } from '../reload/reload.service.js';
|
import { ReloadService } from '../reload/reload.service.js';
|
||||||
|
import { McpClientService } from '../mcp-client/mcp-client.service.js';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { COMMANDS_REDIS } from './commands.tokens.js';
|
import { COMMANDS_REDIS } from './commands.tokens.js';
|
||||||
import { CommandRegistryService } from './command-registry.service.js';
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
@@ -28,6 +29,9 @@ export class CommandExecutorService {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@Inject(forwardRef(() => ChatGateway))
|
@Inject(forwardRef(() => ChatGateway))
|
||||||
private readonly chatGateway: ChatGateway | null,
|
private readonly chatGateway: ChatGateway | null,
|
||||||
|
@Optional()
|
||||||
|
@Inject(McpClientService)
|
||||||
|
private readonly mcpClient: McpClientService | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(payload: SlashCommandPayload, userId: string): Promise<SlashCommandResultPayload> {
|
async execute(payload: SlashCommandPayload, userId: string): Promise<SlashCommandResultPayload> {
|
||||||
@@ -105,6 +109,8 @@ export class CommandExecutorService {
|
|||||||
};
|
};
|
||||||
case 'tools':
|
case 'tools':
|
||||||
return await this.handleTools(conversationId, userId);
|
return await this.handleTools(conversationId, userId);
|
||||||
|
case 'mcp':
|
||||||
|
return await this.handleMcp(args ?? null, conversationId);
|
||||||
case 'reload': {
|
case 'reload': {
|
||||||
if (!this.reloadService) {
|
if (!this.reloadService) {
|
||||||
return {
|
return {
|
||||||
@@ -489,4 +495,92 @@ export class CommandExecutorService {
|
|||||||
conversationId,
|
conversationId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleMcp(
|
||||||
|
args: string | null,
|
||||||
|
conversationId: string,
|
||||||
|
): Promise<SlashCommandResultPayload> {
|
||||||
|
if (!this.mcpClient) {
|
||||||
|
return {
|
||||||
|
command: 'mcp',
|
||||||
|
conversationId,
|
||||||
|
success: false,
|
||||||
|
message: 'MCP client service is not available.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = args?.trim().split(/\s+/)[0] ?? 'status';
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'status':
|
||||||
|
case 'servers': {
|
||||||
|
const statuses = this.mcpClient.getServerStatuses();
|
||||||
|
if (statuses.length === 0) {
|
||||||
|
return {
|
||||||
|
command: 'mcp',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message:
|
||||||
|
'No MCP servers configured. Set MCP_SERVERS env var to connect external tool servers.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const lines = ['MCP Server Status:\n'];
|
||||||
|
for (const s of statuses) {
|
||||||
|
const status = s.connected ? '✓ connected' : '✗ disconnected';
|
||||||
|
lines.push(` ${s.name}: ${status}`);
|
||||||
|
lines.push(` URL: ${s.url}`);
|
||||||
|
lines.push(` Tools: ${s.toolCount}`);
|
||||||
|
if (s.error) lines.push(` Error: ${s.error}`);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
const tools = this.mcpClient.getToolDefinitions();
|
||||||
|
if (tools.length > 0) {
|
||||||
|
lines.push(`Total bridged tools: ${tools.length}`);
|
||||||
|
lines.push(`Tool names: ${tools.map((t) => t.name).join(', ')}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
command: 'mcp',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: lines.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'reconnect': {
|
||||||
|
const serverName = args?.trim().split(/\s+/).slice(1).join(' ');
|
||||||
|
if (!serverName) {
|
||||||
|
return {
|
||||||
|
command: 'mcp',
|
||||||
|
conversationId,
|
||||||
|
success: false,
|
||||||
|
message: 'Usage: /mcp reconnect <server-name>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.mcpClient.reconnectServer(serverName);
|
||||||
|
return {
|
||||||
|
command: 'mcp',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: `MCP server "${serverName}" reconnected successfully.`,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
command: 'mcp',
|
||||||
|
conversationId,
|
||||||
|
success: false,
|
||||||
|
message: `Failed to reconnect MCP server "${serverName}": ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
command: 'mcp',
|
||||||
|
conversationId,
|
||||||
|
success: false,
|
||||||
|
message: `Unknown MCP action: "${action}". Use: /mcp status, /mcp servers, /mcp reconnect <name>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { CommandRegistryService } from './command-registry.service.js';
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
import type { CommandDef } from '@mosaic/types';
|
import type { CommandDef } from '@mosaicstack/types';
|
||||||
|
|
||||||
const mockCmd: CommandDef = {
|
const mockCmd: CommandDef = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, type OnModuleInit } from '@nestjs/common';
|
import { Injectable, type OnModuleInit } from '@nestjs/common';
|
||||||
import type { CommandDef, CommandManifest } from '@mosaic/types';
|
import type { CommandDef, CommandManifest } from '@mosaicstack/types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommandRegistryService implements OnModuleInit {
|
export class CommandRegistryService implements OnModuleInit {
|
||||||
@@ -260,6 +260,23 @@ export class CommandRegistryService implements OnModuleInit {
|
|||||||
execution: 'socket',
|
execution: 'socket',
|
||||||
available: true,
|
available: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'mcp',
|
||||||
|
description: 'Manage MCP server connections (status/reconnect/servers)',
|
||||||
|
aliases: [],
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'action',
|
||||||
|
type: 'enum',
|
||||||
|
optional: true,
|
||||||
|
values: ['status', 'reconnect', 'servers'],
|
||||||
|
description: 'Action: status (default), reconnect <name>, servers',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scope: 'agent',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'reload',
|
name: 'reload',
|
||||||
description: 'Soft-reload gateway plugins and command manifest (admin)',
|
description: 'Soft-reload gateway plugins and command manifest (admin)',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { CommandRegistryService } from './command-registry.service.js';
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
import { CommandExecutorService } from './command-executor.service.js';
|
import { CommandExecutorService } from './command-executor.service.js';
|
||||||
import type { SlashCommandPayload } from '@mosaic/types';
|
import type { SlashCommandPayload } from '@mosaicstack/types';
|
||||||
|
|
||||||
// ─── Mocks ───────────────────────────────────────────────────────────────────
|
// ─── Mocks ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ function buildExecutor(registry: CommandRegistryService): CommandExecutorService
|
|||||||
mockBrain as never,
|
mockBrain as never,
|
||||||
null, // reloadService (optional)
|
null, // reloadService (optional)
|
||||||
null, // chatGateway (optional)
|
null, // chatGateway (optional)
|
||||||
|
null, // mcpClient (optional)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { forwardRef, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
|
import { forwardRef, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { createQueue, type QueueHandle } from '@mosaic/queue';
|
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
||||||
import { ChatModule } from '../chat/chat.module.js';
|
import { ChatModule } from '../chat/chat.module.js';
|
||||||
import { GCModule } from '../gc/gc.module.js';
|
import { GCModule } from '../gc/gc.module.js';
|
||||||
import { ReloadModule } from '../reload/reload.module.js';
|
import { ReloadModule } from '../reload/reload.module.js';
|
||||||
|
|||||||
16
apps/gateway/src/config/config.module.ts
Normal file
16
apps/gateway/src/config/config.module.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type MissionStatusSummary,
|
type MissionStatusSummary,
|
||||||
type MissionTask,
|
type MissionTask,
|
||||||
type TaskDetail,
|
type TaskDetail,
|
||||||
} from '@mosaic/coord';
|
} from '@mosaicstack/coord';
|
||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,51 @@
|
|||||||
|
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 { Global, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { createDb, type Db, type DbHandle } from '@mosaic/db';
|
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';
|
||||||
|
|
||||||
export const DB_HANDLE = 'DB_HANDLE';
|
export const DB_HANDLE = 'DB_HANDLE';
|
||||||
export const DB = 'DB';
|
export const DB = 'DB';
|
||||||
|
export const STORAGE_ADAPTER = 'STORAGE_ADAPTER';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: DB_HANDLE,
|
provide: DB_HANDLE,
|
||||||
useFactory: (): DbHandle => createDb(),
|
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],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: DB,
|
provide: DB,
|
||||||
useFactory: (handle: DbHandle): Db => handle.db,
|
useFactory: (handle: DbHandle): Db => handle.db,
|
||||||
inject: [DB_HANDLE],
|
inject: [DB_HANDLE],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: STORAGE_ADAPTER,
|
||||||
|
useFactory: (config: MosaicConfig): StorageAdapter => createStorageAdapter(config.storage),
|
||||||
|
inject: [MOSAIC_CONFIG],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
exports: [DB],
|
exports: [DB, STORAGE_ADAPTER],
|
||||||
})
|
})
|
||||||
export class DatabaseModule implements OnApplicationShutdown {
|
export class DatabaseModule implements OnApplicationShutdown {
|
||||||
constructor(@Inject(DB_HANDLE) private readonly handle: DbHandle) {}
|
constructor(
|
||||||
|
@Inject(DB_HANDLE) private readonly handle: DbHandle,
|
||||||
|
@Inject(STORAGE_ADAPTER) private readonly storageAdapter: StorageAdapter,
|
||||||
|
) {}
|
||||||
|
|
||||||
async onApplicationShutdown(): Promise<void> {
|
async onApplicationShutdown(): Promise<void> {
|
||||||
await this.handle.close();
|
await Promise.all([this.handle.close(), this.storageAdapter.close()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Module, type OnApplicationShutdown, Inject } from '@nestjs/common';
|
import { Module, type OnApplicationShutdown, Inject } from '@nestjs/common';
|
||||||
import { createQueue, type QueueHandle } from '@mosaic/queue';
|
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
||||||
import { SessionGCService } from './session-gc.service.js';
|
import { SessionGCService } from './session-gc.service.js';
|
||||||
import { REDIS } from './gc.tokens.js';
|
import { REDIS } from './gc.tokens.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import type { QueueHandle } from '@mosaic/queue';
|
import type { QueueHandle } from '@mosaicstack/queue';
|
||||||
import type { LogService } from '@mosaic/log';
|
import type { LogService } from '@mosaicstack/log';
|
||||||
import { SessionGCService } from './session-gc.service.js';
|
import { SessionGCService } from './session-gc.service.js';
|
||||||
|
|
||||||
type MockRedis = {
|
type MockRedis = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||||
import type { QueueHandle } from '@mosaic/queue';
|
import type { QueueHandle } from '@mosaicstack/queue';
|
||||||
import type { LogService } from '@mosaic/log';
|
import type { LogService } from '@mosaicstack/log';
|
||||||
import { LOG_SERVICE } from '../log/log.tokens.js';
|
import { LOG_SERVICE } from '../log/log.tokens.js';
|
||||||
import { REDIS } from './gc.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 { Body, Controller, Get, Inject, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
import type { LogService } from '@mosaic/log';
|
import type { LogService } from '@mosaicstack/log';
|
||||||
import { LOG_SERVICE } from './log.tokens.js';
|
import { LOG_SERVICE } from './log.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import type { IngestLogDto, QueryLogsDto } from './log.dto.js';
|
import type { IngestLogDto, QueryLogsDto } from './log.dto.js';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { createLogService, type LogService } from '@mosaic/log';
|
import { createLogService, type LogService } from '@mosaicstack/log';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
import { LOG_SERVICE } from './log.tokens.js';
|
import { LOG_SERVICE } from './log.tokens.js';
|
||||||
import { LogController } from './log.controller.js';
|
import { LogController } from './log.controller.js';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import type { LogService } from '@mosaic/log';
|
import type { LogService } from '@mosaicstack/log';
|
||||||
import type { Memory } from '@mosaic/memory';
|
import type { Memory } from '@mosaicstack/memory';
|
||||||
import { LOG_SERVICE } from './log.tokens.js';
|
import { LOG_SERVICE } from './log.tokens.js';
|
||||||
import { MEMORY } from '../memory/memory.tokens.js';
|
import { MEMORY } from '../memory/memory.tokens.js';
|
||||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaicstack/db';
|
||||||
import { sql, summarizationJobs } from '@mosaic/db';
|
import { sql, summarizationJobs } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
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.
|
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,5 +1,13 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
import { resolve } from 'node:path';
|
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 });
|
||||||
|
|
||||||
// Load .env from monorepo root (cwd is apps/gateway when run via pnpm filter)
|
// Load .env from monorepo root (cwd is apps/gateway when run via pnpm filter)
|
||||||
config({ path: resolve(process.cwd(), '../../.env') });
|
config({ path: resolve(process.cwd(), '../../.env') });
|
||||||
@@ -11,7 +19,7 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||||
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
import helmet from '@fastify/helmet';
|
import helmet from '@fastify/helmet';
|
||||||
import { listSsoStartupWarnings } from '@mosaic/auth';
|
import { listSsoStartupWarnings } from '@mosaicstack/auth';
|
||||||
import { AppModule } from './app.module.js';
|
import { AppModule } from './app.module.js';
|
||||||
import { mountAuthHandler } from './auth/auth.controller.js';
|
import { mountAuthHandler } from './auth/auth.controller.js';
|
||||||
import { mountMcpHandler } from './mcp/mcp.controller.js';
|
import { mountMcpHandler } from './mcp/mcp.controller.js';
|
||||||
@@ -51,7 +59,7 @@ async function bootstrap(): Promise<void> {
|
|||||||
mountAuthHandler(app);
|
mountAuthHandler(app);
|
||||||
mountMcpHandler(app, app.get(McpService));
|
mountMcpHandler(app, app.get(McpService));
|
||||||
|
|
||||||
const port = Number(process.env['GATEWAY_PORT'] ?? 4000);
|
const port = Number(process.env['GATEWAY_PORT'] ?? 14242);
|
||||||
await app.listen(port, '0.0.0.0');
|
await app.listen(port, '0.0.0.0');
|
||||||
logger.log(`Gateway listening on port ${port}`);
|
logger.log(`Gateway listening on port ${port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { fromNodeHeaders } from 'better-auth/node';
|
import { fromNodeHeaders } from 'better-auth/node';
|
||||||
import type { Auth } from '@mosaic/auth';
|
import type { Auth } from '@mosaicstack/auth';
|
||||||
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
import type { McpService } from './mcp.service.js';
|
import type { McpService } from './mcp.service.js';
|
||||||
import { AUTH } from '../auth/auth.tokens.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 { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import type { Memory } from '@mosaic/memory';
|
import type { Memory } from '@mosaicstack/memory';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { MEMORY } from '../memory/memory.tokens.js';
|
import { MEMORY } from '../memory/memory.tokens.js';
|
||||||
import { EmbeddingService } from '../memory/embedding.service.js';
|
import { EmbeddingService } from '../memory/embedding.service.js';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import type { EmbeddingProvider } from '@mosaic/memory';
|
import type { EmbeddingProvider } from '@mosaicstack/memory';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Environment-driven configuration
|
// Environment-driven configuration
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { Memory } from '@mosaic/memory';
|
import type { Memory } from '@mosaicstack/memory';
|
||||||
import { MEMORY } from './memory.tokens.js';
|
import { MEMORY } from './memory.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { createMemory, type Memory } from '@mosaic/memory';
|
import {
|
||||||
import type { Db } from '@mosaic/db';
|
createMemory,
|
||||||
import { DB } from '../database/database.module.js';
|
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 { MEMORY } from './memory.tokens.js';
|
import { MEMORY } from './memory.tokens.js';
|
||||||
import { MemoryController } from './memory.controller.js';
|
import { MemoryController } from './memory.controller.js';
|
||||||
import { EmbeddingService } from './embedding.service.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()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -14,9 +32,15 @@ import { EmbeddingService } from './embedding.service.js';
|
|||||||
useFactory: (db: Db): Memory => createMemory(db),
|
useFactory: (db: Db): Memory => createMemory(db),
|
||||||
inject: [DB],
|
inject: [DB],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MEMORY_ADAPTER,
|
||||||
|
useFactory: (config: MosaicConfig, storageAdapter: StorageAdapter): MemoryAdapter =>
|
||||||
|
createMemoryAdapter(buildMemoryConfig(config, storageAdapter)),
|
||||||
|
inject: [MOSAIC_CONFIG, STORAGE_ADAPTER],
|
||||||
|
},
|
||||||
EmbeddingService,
|
EmbeddingService,
|
||||||
],
|
],
|
||||||
controllers: [MemoryController],
|
controllers: [MemoryController],
|
||||||
exports: [MEMORY, EmbeddingService],
|
exports: [MEMORY, MEMORY_ADAPTER, EmbeddingService],
|
||||||
})
|
})
|
||||||
export class MemoryModule {}
|
export class MemoryModule {}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
type OnModuleDestroy,
|
type OnModuleDestroy,
|
||||||
type OnModuleInit,
|
type OnModuleInit,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { DiscordPlugin } from '@mosaic/discord-plugin';
|
import { DiscordPlugin } from '@mosaicstack/discord-plugin';
|
||||||
import { TelegramPlugin } from '@mosaic/telegram-plugin';
|
import { TelegramPlugin } from '@mosaicstack/telegram-plugin';
|
||||||
import { PluginService } from './plugin.service.js';
|
import { PluginService } from './plugin.service.js';
|
||||||
import type { IChannelPlugin } from './plugin.interface.js';
|
import type { IChannelPlugin } from './plugin.interface.js';
|
||||||
import { PLUGIN_REGISTRY } from './plugin.tokens.js';
|
import { PLUGIN_REGISTRY } from './plugin.tokens.js';
|
||||||
@@ -48,7 +48,7 @@ class TelegramChannelPluginAdapter implements IChannelPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_GATEWAY_URL = 'http://localhost:4000';
|
const DEFAULT_GATEWAY_URL = 'http://localhost:14242';
|
||||||
|
|
||||||
function createPluginRegistry(): IChannelPlugin[] {
|
function createPluginRegistry(): IChannelPlugin[] {
|
||||||
const plugins: IChannelPlugin[] = [];
|
const plugins: IChannelPlugin[] = [];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { PreferencesService, PLATFORM_DEFAULTS, IMMUTABLE_KEYS } from './preferences.service.js';
|
import { PreferencesService, PLATFORM_DEFAULTS, IMMUTABLE_KEYS } from './preferences.service.js';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaicstack/db';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a mock Drizzle DB where the select chain supports:
|
* Build a mock Drizzle DB where the select chain supports:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { eq, and, sql, type Db, preferences as preferencesTable } from '@mosaic/db';
|
import { eq, and, sql, type Db, preferences as preferencesTable } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
|
|
||||||
export const PLATFORM_DEFAULTS: Record<string, unknown> = {
|
export const PLATFORM_DEFAULTS: Record<string, unknown> = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { createQueue, type QueueHandle } from '@mosaic/queue';
|
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
|
||||||
|
|
||||||
const SESSION_SYSTEM_KEY = (sessionId: string) => `mosaic:session:${sessionId}:system`;
|
const SESSION_SYSTEM_KEY = (sessionId: string) => `mosaic:session:${sessionId}:system`;
|
||||||
const SESSION_SYSTEM_FRAGMENTS_KEY = (sessionId: string) =>
|
const SESSION_SYSTEM_FRAGMENTS_KEY = (sessionId: string) =>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
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';
|
import { QueueService } from './queue.service.js';
|
||||||
|
|
||||||
|
export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [QueueService],
|
providers: [
|
||||||
exports: [QueueService],
|
QueueService,
|
||||||
|
{
|
||||||
|
provide: QUEUE_ADAPTER,
|
||||||
|
useFactory: (config: MosaicConfig): QueueAdapter => createQueueAdapter(config.queue),
|
||||||
|
inject: [MOSAIC_CONFIG],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [QueueService, QUEUE_ADAPTER],
|
||||||
})
|
})
|
||||||
export class QueueModule {}
|
export class QueueModule {}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type OnModuleDestroy,
|
type OnModuleDestroy,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Queue, Worker, type Job, type ConnectionOptions } from 'bullmq';
|
import { Queue, Worker, type Job, type ConnectionOptions } from 'bullmq';
|
||||||
import type { LogService } from '@mosaic/log';
|
import type { LogService } from '@mosaicstack/log';
|
||||||
import { LOG_SERVICE } from '../log/log.tokens.js';
|
import { LOG_SERVICE } from '../log/log.tokens.js';
|
||||||
import type { JobDto, JobStatus } from './queue-admin.dto.js';
|
import type { JobDto, JobStatus } from './queue-admin.dto.js';
|
||||||
|
|
||||||
@@ -51,16 +51,42 @@ export interface QueueHealthStatus {
|
|||||||
// Constants
|
// Constants
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const QUEUE_SUMMARIZATION = 'mosaic:summarization';
|
export const QUEUE_SUMMARIZATION = 'mosaic-summarization';
|
||||||
export const QUEUE_GC = 'mosaic:gc';
|
export const QUEUE_GC = 'mosaic-gc';
|
||||||
export const QUEUE_TIER_MANAGEMENT = 'mosaic:tier-management';
|
export const QUEUE_TIER_MANAGEMENT = 'mosaic-tier-management';
|
||||||
|
|
||||||
const DEFAULT_VALKEY_URL = 'redis://localhost:6380';
|
const DEFAULT_VALKEY_URL = 'redis://localhost:6380';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a Redis URL string into a BullMQ-compatible ConnectionOptions object.
|
||||||
|
*
|
||||||
|
* BullMQ v5 does `Object.assign({ port: 6379, host: '127.0.0.1' }, opts)` in
|
||||||
|
* its RedisConnection constructor. If opts is a URL string, Object.assign only
|
||||||
|
* copies character-index properties and the defaults survive — so 6379 wins.
|
||||||
|
* We must parse the URL ourselves and return a plain RedisOptions object.
|
||||||
|
*/
|
||||||
function getConnection(): ConnectionOptions {
|
function getConnection(): ConnectionOptions {
|
||||||
const url = process.env['VALKEY_URL'] ?? DEFAULT_VALKEY_URL;
|
const url = process.env['VALKEY_URL'] ?? DEFAULT_VALKEY_URL;
|
||||||
// BullMQ ConnectionOptions accepts a URL string (ioredis-compatible)
|
try {
|
||||||
return url as unknown as ConnectionOptions;
|
const parsed = new URL(url);
|
||||||
|
const opts: ConnectionOptions = {
|
||||||
|
host: parsed.hostname || '127.0.0.1',
|
||||||
|
port: parsed.port ? parseInt(parsed.port, 10) : 6380,
|
||||||
|
};
|
||||||
|
if (parsed.password) {
|
||||||
|
(opts as Record<string, unknown>)['password'] = decodeURIComponent(parsed.password);
|
||||||
|
}
|
||||||
|
if (parsed.pathname && parsed.pathname.length > 1) {
|
||||||
|
const db = parseInt(parsed.pathname.slice(1), 10);
|
||||||
|
if (!isNaN(db)) {
|
||||||
|
(opts as Record<string, unknown>)['db'] = db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
} catch {
|
||||||
|
// Fallback: hope the value is already a host string ioredis understands
|
||||||
|
return { host: '127.0.0.1', port: 6380 } as ConnectionOptions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, HttpCode, HttpStatus, Inject, Post, UseGuards } from '@nestjs/common';
|
import { Controller, HttpCode, HttpStatus, Inject, Post, UseGuards } from '@nestjs/common';
|
||||||
import type { SystemReloadPayload } from '@mosaic/types';
|
import type { SystemReloadPayload } from '@mosaicstack/types';
|
||||||
import { AdminGuard } from '../admin/admin.guard.js';
|
import { AdminGuard } from '../admin/admin.guard.js';
|
||||||
import { ChatGateway } from '../chat/chat.gateway.js';
|
import { ChatGateway } from '../chat/chat.gateway.js';
|
||||||
import { ReloadService } from './reload.service.js';
|
import { ReloadService } from './reload.service.js';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type OnApplicationBootstrap,
|
type OnApplicationBootstrap,
|
||||||
type OnApplicationShutdown,
|
type OnApplicationShutdown,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { SystemReloadPayload } from '@mosaic/types';
|
import type { SystemReloadPayload } from '@mosaicstack/types';
|
||||||
import { CommandRegistryService } from '../commands/command-registry.service.js';
|
import { CommandRegistryService } from '../commands/command-registry.service.js';
|
||||||
import { isMosaicPlugin } from './mosaic-plugin.interface.js';
|
import { isMosaicPlugin } from './mosaic-plugin.interface.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { eq, type Db, skills } from '@mosaic/db';
|
import { eq, type Db, skills } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
|
|
||||||
type Skill = typeof skills.$inferSelect;
|
type Skill = typeof skills.$inferSelect;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaicstack/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { PluginService } from '../plugin/plugin.service.js';
|
import { PluginService } from '../plugin/plugin.service.js';
|
||||||
import { WorkspaceService } from './workspace.service.js';
|
import { WorkspaceService } from './workspace.service.js';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaic/db';
|
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
"rootDir": "../..",
|
"rootDir": "../..",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@mosaic/auth": ["../../packages/auth/src/index.ts"],
|
"@mosaicstack/auth": ["../../packages/auth/src/index.ts"],
|
||||||
"@mosaic/brain": ["../../packages/brain/src/index.ts"],
|
"@mosaicstack/brain": ["../../packages/brain/src/index.ts"],
|
||||||
"@mosaic/coord": ["../../packages/coord/src/index.ts"],
|
"@mosaicstack/coord": ["../../packages/coord/src/index.ts"],
|
||||||
"@mosaic/db": ["../../packages/db/src/index.ts"],
|
"@mosaicstack/db": ["../../packages/db/src/index.ts"],
|
||||||
"@mosaic/log": ["../../packages/log/src/index.ts"],
|
"@mosaicstack/log": ["../../packages/log/src/index.ts"],
|
||||||
"@mosaic/memory": ["../../packages/memory/src/index.ts"],
|
"@mosaicstack/memory": ["../../packages/memory/src/index.ts"],
|
||||||
"@mosaic/types": ["../../packages/types/src/index.ts"],
|
"@mosaicstack/types": ["../../packages/types/src/index.ts"],
|
||||||
"@mosaic/discord-plugin": ["../../plugins/discord/src/index.ts"],
|
"@mosaicstack/discord-plugin": ["../../plugins/discord/src/index.ts"],
|
||||||
"@mosaic/telegram-plugin": ["../../plugins/telegram/src/index.ts"]
|
"@mosaicstack/telegram-plugin": ["../../plugins/telegram/src/index.ts"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import swc from 'unplugin-swc';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -5,4 +6,22 @@ export default defineConfig({
|
|||||||
globals: true,
|
globals: true,
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
swc.vite({
|
||||||
|
jsc: {
|
||||||
|
parser: {
|
||||||
|
syntax: 'typescript',
|
||||||
|
decorators: true,
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
decoratorMetadata: true,
|
||||||
|
legacyDecorator: true,
|
||||||
|
},
|
||||||
|
target: 'es2022',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
type: 'nodenext',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { NextConfig } from 'next';
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
transpilePackages: ['@mosaic/design-tokens'],
|
transpilePackages: ['@mosaicstack/design-tokens'],
|
||||||
|
|
||||||
// Enable gzip/brotli compression for all responses.
|
// Enable gzip/brotli compression for all responses.
|
||||||
compress: true,
|
compress: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/web",
|
"name": "@mosaicstack/web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mosaic/design-tokens": "workspace:^",
|
"@mosaicstack/design-tokens": "workspace:^",
|
||||||
"better-auth": "^1.5.5",
|
"better-auth": "^1.5.5",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"next": "^16.0.0",
|
"next": "^16.0.0",
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
*
|
*
|
||||||
* Assumes:
|
* Assumes:
|
||||||
* - Next.js web app running on http://localhost:3000
|
* - Next.js web app running on http://localhost:3000
|
||||||
* - NestJS gateway running on http://localhost:4000
|
* - NestJS gateway running on http://localhost:14242
|
||||||
*
|
*
|
||||||
* Run with: pnpm --filter @mosaic/web test:e2e
|
* Run with: pnpm --filter @mosaicstack/web test:e2e
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
|
|||||||
0
apps/web/public/.gitkeep
Normal file
0
apps/web/public/.gitkeep
Normal file
@@ -1,4 +1,4 @@
|
|||||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
|
||||||
|
|
||||||
export interface ApiRequestInit extends Omit<RequestInit, 'body'> {
|
export interface ApiRequestInit extends Omit<RequestInit, 'body'> {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createAuthClient } from 'better-auth/react';
|
|||||||
import { adminClient, genericOAuthClient } from 'better-auth/client/plugins';
|
import { adminClient, genericOAuthClient } from 'better-auth/client/plugins';
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000',
|
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242',
|
||||||
plugins: [adminClient(), genericOAuthClient()],
|
plugins: [adminClient(), genericOAuthClient()],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { io, type Socket } from 'socket.io-client';
|
import { io, type Socket } from 'socket.io-client';
|
||||||
|
|
||||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
|
||||||
|
|
||||||
let socket: Socket | null = null;
|
let socket: Socket | null = null;
|
||||||
|
|
||||||
|
|||||||
231
briefs/monorepo-consolidation.md
Normal file
231
briefs/monorepo-consolidation.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# Brief: Monorepo Consolidation — mosaic/stack → mosaic/mosaic-stack
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
Architecture consolidation — merge the mosaic/stack repo (Forge pipeline, MACP protocol, framework tools) into mosaic/mosaic-stack (Harness Foundation platform). Two repos doing related work that need to converge.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**mosaic/stack** (OLD) contains:
|
||||||
|
|
||||||
|
- Forge progressive refinement pipeline (stages, agents, personas, rails, debate protocol, brief classification)
|
||||||
|
- MACP protocol (JSON schemas, deterministic Python controller, dispatcher, event system, gate runner)
|
||||||
|
- Credential resolver (Python — OC config, mosaic files, ambient env, JSON5 parser)
|
||||||
|
- OC framework plugin (injects Mosaic rails into all agent sessions)
|
||||||
|
- Profiles (runtime-neutral context packs for tech stacks and domains)
|
||||||
|
- Stage adapter (Forge→MACP bridge)
|
||||||
|
- Board tasks (multi-agent board evaluation)
|
||||||
|
- OpenBrain specialist memory (learning capture/recall)
|
||||||
|
- 17 guides, 5 universal skills
|
||||||
|
|
||||||
|
**mosaic/mosaic-stack** (NEW) contains:
|
||||||
|
|
||||||
|
- Harness Foundation platform (NestJS gateway, Next.js web, Drizzle ORM, Pi SDK runtime)
|
||||||
|
- 5 provider adapters, task classifier, routing rules, model capability matrix
|
||||||
|
- MACP OC plugin (ACP runtime backend with Pi bridge)
|
||||||
|
- TS coord package (mission runner, tasks file manager, status tracker — 1635 lines)
|
||||||
|
- BullMQ job queue, OTEL telemetry, channel plugins (Discord, Telegram)
|
||||||
|
- CLI with TUI, 65/65 tasks done, v0.2.0
|
||||||
|
|
||||||
|
**Decision:** NEW repo is the base. All unique work from OLD gets ported into NEW as packages.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Work Package 1: Forge Pipeline Package (`packages/forge`)
|
||||||
|
|
||||||
|
Port the entire Forge progressive refinement pipeline as a TypeScript package.
|
||||||
|
|
||||||
|
**From OLD:**
|
||||||
|
|
||||||
|
- `forge/pipeline/stages/*.md` — 11 stage definitions
|
||||||
|
- `forge/pipeline/agents/{board,generalists,specialists,cross-cutting}/*.md` — all persona definitions
|
||||||
|
- `forge/pipeline/rails/*.md` — debate protocol, dynamic composition, worker rails
|
||||||
|
- `forge/pipeline/gates/` — gate reviewer definitions
|
||||||
|
- `forge/pipeline/orchestrator/run-structure.md` — file-based observability spec
|
||||||
|
- `forge/templates/` — brief and PRD templates
|
||||||
|
- `forge/pipeline/orchestrator/board_tasks.py` → rewrite in TS
|
||||||
|
- `forge/pipeline/orchestrator/stage_adapter.py` → rewrite in TS
|
||||||
|
- `forge/pipeline/orchestrator/pipeline_runner.py` → rewrite in TS
|
||||||
|
- `forge/forge` CLI (Python) → rewrite in TS, integrate with `packages/cli`
|
||||||
|
|
||||||
|
**Package structure:**
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/forge/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Public API
|
||||||
|
│ ├── pipeline-runner.ts # Orchestrates full pipeline run
|
||||||
|
│ ├── stage-adapter.ts # Maps stages to MACP/coord tasks
|
||||||
|
│ ├── board-tasks.ts # Multi-agent board evaluation task generator
|
||||||
|
│ ├── brief-classifier.ts # strategic/technical/hotfix classification
|
||||||
|
│ ├── types.ts # Stage specs, run manifest, gate results
|
||||||
|
│ └── constants.ts # Stage sequence, timeouts, labels
|
||||||
|
├── pipeline/
|
||||||
|
│ ├── stages/ # .md stage definitions (copied)
|
||||||
|
│ ├── agents/ # .md persona definitions (copied)
|
||||||
|
│ │ ├── board/
|
||||||
|
│ │ ├── cross-cutting/
|
||||||
|
│ │ ├── generalists/
|
||||||
|
│ │ └── specialists/
|
||||||
|
│ │ ├── language/
|
||||||
|
│ │ └── domain/
|
||||||
|
│ ├── rails/ # .md rails (copied)
|
||||||
|
│ ├── gates/ # .md gate definitions (copied)
|
||||||
|
│ └── templates/ # brief + PRD templates (copied)
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key design decisions:**
|
||||||
|
|
||||||
|
- Pipeline markdown assets are runtime data, not compiled — ship as-is in the package
|
||||||
|
- `pipeline-runner.ts` calls into `packages/coord` for task execution (not a separate controller)
|
||||||
|
- Stage adapter generates coord-compatible tasks, not MACP JSON directly
|
||||||
|
- Board tasks use `depends_on_policy: "all_terminal"` for synthesis
|
||||||
|
- Per-stage timeouts from `STAGE_TIMEOUTS` map
|
||||||
|
- Brief classifier supports CLI flag, YAML frontmatter, and keyword auto-detection
|
||||||
|
- Run output goes to project-scoped `.forge/runs/{run-id}/` (not inside the Forge package)
|
||||||
|
|
||||||
|
**Persona override system (new):**
|
||||||
|
|
||||||
|
- Base personas ship with the package (read-only)
|
||||||
|
- Project-level overrides in `.forge/personas/{role}.md` extend (not replace) base personas
|
||||||
|
- Board composition configurable via `.forge/config.yaml`:
|
||||||
|
```yaml
|
||||||
|
board:
|
||||||
|
additional_members:
|
||||||
|
- compliance-officer.md
|
||||||
|
skip_members: []
|
||||||
|
specialists:
|
||||||
|
always_include:
|
||||||
|
- proxmox-expert
|
||||||
|
```
|
||||||
|
- OpenBrain integration for cross-run specialist memory (when enabled)
|
||||||
|
|
||||||
|
### Work Package 2: MACP Protocol Package (`packages/macp`)
|
||||||
|
|
||||||
|
Port the MACP protocol layer, event system, and gate runner as a TypeScript package.
|
||||||
|
|
||||||
|
**From OLD:**
|
||||||
|
|
||||||
|
- `tools/macp/protocol/task.schema.json` — task JSON schema
|
||||||
|
- `tools/macp/protocol/` — event schemas
|
||||||
|
- `tools/macp/controller/gate_runner.py` → rewrite in TS as `gate-runner.ts`
|
||||||
|
- `tools/macp/events/` — event watcher, webhook adapter, Discord formatter → rewrite in TS
|
||||||
|
- `tools/macp/dispatcher/credential_resolver.py` → rewrite in TS as `credential-resolver.ts`
|
||||||
|
- `tools/macp/memory/learning_capture.py` + `learning_recall.py` → rewrite in TS
|
||||||
|
|
||||||
|
**Package structure:**
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/macp/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Public API
|
||||||
|
│ ├── types.ts # Task, event, result, gate types
|
||||||
|
│ ├── schemas/ # JSON schemas (copied)
|
||||||
|
│ ├── gate-runner.ts # Mechanical + AI review quality gates
|
||||||
|
│ ├── credential-resolver.ts # Provider credential resolution (mosaic files, OC config, ambient)
|
||||||
|
│ ├── event-emitter.ts # Append events to ndjson, structured event types
|
||||||
|
│ ├── event-watcher.ts # Poll events.ndjson with cursor persistence
|
||||||
|
│ ├── webhook-adapter.ts # POST events to configurable URL
|
||||||
|
│ ├── discord-formatter.ts # Human-readable event messages
|
||||||
|
│ └── learning.ts # OpenBrain capture + recall
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Integration with existing packages:**
|
||||||
|
|
||||||
|
- `packages/coord` uses `packages/macp` for event emission, gate running, and credential resolution
|
||||||
|
- `plugins/macp` uses `packages/macp` for protocol types and credential resolution
|
||||||
|
- `packages/forge` uses `packages/macp` gate types for stage gates
|
||||||
|
|
||||||
|
### Work Package 3: OC Framework Plugin (`plugins/mosaic-framework`)
|
||||||
|
|
||||||
|
Port the OC framework plugin that injects Mosaic rails into all agent sessions.
|
||||||
|
|
||||||
|
**From OLD:**
|
||||||
|
|
||||||
|
- `oc-plugins/mosaic-framework/index.ts` — `before_agent_start` + `subagent_spawning` hooks
|
||||||
|
- `oc-plugins/mosaic-framework/openclaw.plugin.json`
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins/mosaic-framework/
|
||||||
|
├── src/
|
||||||
|
│ └── index.ts # Plugin hooks
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**This is separate from `plugins/macp`:**
|
||||||
|
|
||||||
|
- `mosaic-framework` = injects Mosaic rails/contracts into every OC session (passive enforcement)
|
||||||
|
- `macp` = provides an ACP runtime backend for MACP task execution (active runtime)
|
||||||
|
|
||||||
|
### Work Package 4: Profiles + Guides + Skills
|
||||||
|
|
||||||
|
Port reference content as a documentation/config package or top-level directories.
|
||||||
|
|
||||||
|
**From OLD:**
|
||||||
|
|
||||||
|
- `profiles/domains/*.json` — HIPAA, fintech, crypto context packs
|
||||||
|
- `profiles/tech-stacks/*.json` — NestJS, Next.js, FastAPI, React conventions
|
||||||
|
- `profiles/workflows/*.json` — API development, frontend component, testing workflows
|
||||||
|
- `guides/*.md` — 17 guides (auth, backend, QA, orchestrator, PRD, etc.)
|
||||||
|
- `skills-universal/` — jarvis, macp, mosaic-standards, prd, setup-cicd skills
|
||||||
|
|
||||||
|
**Destination:**
|
||||||
|
|
||||||
|
```
|
||||||
|
profiles/ # Top-level (same as OLD)
|
||||||
|
guides/ # Top-level (same as OLD)
|
||||||
|
skills/ # Top-level (renamed from skills-universal)
|
||||||
|
```
|
||||||
|
|
||||||
|
These are runtime-neutral assets consumed by any agent or profile loader — they don't belong in a compiled package.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Rewriting the NestJS orchestrator app from OLD (`apps/orchestrator/`) — its functionality is subsumed by `packages/coord` + `apps/gateway`
|
||||||
|
- Porting the FastAPI coordinator from OLD (`apps/coordinator/`) — its functionality (webhook receiver, issue parser, quality orchestrator) is handled by `packages/coord` + `apps/gateway` in the new architecture
|
||||||
|
- Porting the Prisma schema or OLD's `apps/api` — Drizzle migration is complete
|
||||||
|
- Old Docker Compose configs (Traefik, Matrix, OpenBao) — NEW has its own infra setup
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. `packages/forge` exists with all 11 stage definitions, all persona markdowns, all rails, and TS implementations of pipeline-runner, stage-adapter, board-tasks, and brief-classifier
|
||||||
|
2. `packages/macp` exists with gate-runner, credential-resolver, event system, and learning capture/recall — all in TypeScript
|
||||||
|
3. `plugins/mosaic-framework` exists and registers OC hooks for rails injection
|
||||||
|
4. Profiles, guides, and skills are present at top-level
|
||||||
|
5. `packages/forge` integrates with `packages/coord` for task execution
|
||||||
|
6. `packages/macp` credential-resolver is used by `plugins/macp` Pi bridge
|
||||||
|
7. All existing tests pass (no regressions)
|
||||||
|
8. New packages have test coverage ≥85%
|
||||||
|
9. `pnpm lint && pnpm typecheck && pnpm build` passes
|
||||||
|
10. `.forge/runs/` project-scoped output directory works for at least one test run
|
||||||
|
|
||||||
|
## Technical Constraints
|
||||||
|
|
||||||
|
- All new code is ESM with NodeNext module resolution
|
||||||
|
- No Python in the new repo — everything rewrites to TypeScript
|
||||||
|
- Pipeline markdown assets (stages, personas, rails) are shipped as package data, not compiled
|
||||||
|
- Credential resolver must support: mosaic credential files, OC config (JSON5), ambient environment — same resolution order as the Python version
|
||||||
|
- Must preserve `depends_on_policy` semantics (all, any, all_terminal)
|
||||||
|
- Per-stage timeouts must be preserved
|
||||||
|
- JSON5 stripping must use the placeholder-extraction approach (not naive regex on string content)
|
||||||
|
|
||||||
|
## Estimated Complexity
|
||||||
|
|
||||||
|
High — crosses 4 work packages with protocol porting, TS rewrites, and integration wiring. Each work package is independently shippable.
|
||||||
|
|
||||||
|
**Suggested execution order:**
|
||||||
|
|
||||||
|
1. WP4 (profiles/guides/skills) — pure copy, no code, fast win
|
||||||
|
2. WP2 (packages/macp) — protocol foundation, needed by WP1 and WP3
|
||||||
|
3. WP1 (packages/forge) — the big one, depends on WP2
|
||||||
|
4. WP3 (plugins/mosaic-framework) — OC integration, can parallel with WP1
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `packages/coord` must be stable (it is — WP1 integrates with it)
|
||||||
|
- `plugins/macp` must be stable (it is — WP2 provides types/credentials to it)
|
||||||
|
- Pi SDK (`@mariozechner/pi-agent-core`) already in the dependency tree
|
||||||
@@ -1,70 +1,73 @@
|
|||||||
# Mission Manifest — Harness Foundation
|
# Mission Manifest — Install UX v2
|
||||||
|
|
||||||
> Persistent document tracking full mission scope, status, and session history.
|
> Persistent document tracking full mission scope, status, and session history.
|
||||||
> Updated by the orchestrator at each phase transition and milestone completion.
|
> Updated by the orchestrator at each phase transition and milestone completion.
|
||||||
|
|
||||||
## Mission
|
## Mission
|
||||||
|
|
||||||
**ID:** harness-20260321
|
**ID:** install-ux-v2-20260405
|
||||||
**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.
|
**Statement:** The install-ux-hardening mission shipped the plumbing (uninstall, masked password, hooks consent, unified flow, headless path), but the first real end-to-end run surfaced a critical regression and a collection of UX failings that make the wizard feel neither quick nor intelligent. This mission closes the bootstrap regression as a hotfix, then rethinks the first-run experience around a provider-first, intent-driven flow with a drill-down main menu and a genuinely fast quick-start.
|
||||||
**Phase:** Complete
|
**Phase:** Execution
|
||||||
**Current Milestone:** All milestones done
|
**Current Milestone:** IUV-M03
|
||||||
**Progress:** 7 / 7 milestones
|
**Progress:** 2 / 3 milestones
|
||||||
**Status:** complete
|
**Status:** active
|
||||||
**Last Updated:** 2026-03-22 UTC
|
**Last Updated:** 2026-04-05 (IUV-M02 complete — CORS/FQDN + skill installer rework)
|
||||||
|
**Parent Mission:** [install-ux-hardening-20260405](./archive/missions/install-ux-hardening-20260405/MISSION-MANIFEST.md) (complete — `mosaic-v0.0.25`)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Real-run testing of `@mosaicstack/mosaic@0.0.25` uncovered:
|
||||||
|
|
||||||
|
1. **Critical:** admin bootstrap fails with HTTP 400 `property email should not exist` — `bootstrap.controller.ts` uses `import type { BootstrapSetupDto }`, erasing the class at runtime. Nest's `@Body()` falls back to plain `Object` metatype, and ValidationPipe with `forbidNonWhitelisted` rejects every property. One-character fix (drop the `type` keyword), but it blocks the happy path of the release that just shipped.
|
||||||
|
2. The wizard reports `✔ Wizard complete` and `✔ Done` _after_ the bootstrap 400 — failure only propagates in headless mode (`wizard.ts:147`).
|
||||||
|
3. The gateway port prompt does not prefill `14242` in the input buffer.
|
||||||
|
4. `"What is Mosaic?"` intro copy does not mention Pi SDK (the actual agent runtime behind Claude/Codex/OpenCode).
|
||||||
|
5. CORS origin prompt is confusing — the user should be able to supply an FQDN/hostname and have the system derive the CORS value.
|
||||||
|
6. Skill / additional feature install section is unusable in practice.
|
||||||
|
7. Quick-start asks far too many questions to be meaningfully "quick".
|
||||||
|
8. No drill-down main menu — everything is a linear interrogation.
|
||||||
|
9. Provider setup happens late and without intelligence. An OpenClaw-style provider-first flow would let the user describe what they want in natural language, have the agent expound on it, and have the agent choose its own name based on that intent.
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
- [x] AC-1: Send messages in TUI → restart TUI → resume conversation → agent has full history and context
|
- [x] AC-1: Admin bootstrap completes successfully end-to-end on a fresh install (DTO value import, no forbidNonWhitelisted regression); covered by an integration or e2e test that exercises the real DTO binding. _(PR #440)_
|
||||||
- [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-2: Wizard fails loudly (non-zero exit, clear error) when the bootstrap stage returns `completed: false`, in both interactive and headless modes. No more silent `✔ Wizard complete` after a 400. _(PR #440)_
|
||||||
- [x] AC-3: Two users exist, User A's memory searches never return User B's data
|
- [x] AC-3: Gateway port prompt prefills `14242` in the input field (user can press Enter to accept). _(PR #440)_
|
||||||
- [x] AC-4: `/model claude-sonnet-4-6` in TUI switches the active model for subsequent messages
|
- [x] AC-4: `"What is Mosaic?"` intro copy mentions Pi SDK as the underlying agent runtime. _(PR #440)_
|
||||||
- [x] AC-5: `/agent coding-agent` in TUI switches to a different agent with different system prompt and tools
|
- [x] AC-5: Release `mosaic-v0.0.26` tagged and published to the Gitea npm registry, unblocking the 0.0.25 happy path. _(tag: mosaic-v0.0.26, registry: 0.0.26 live)_
|
||||||
- [x] AC-6: BullMQ jobs execute on schedule, failures retry with backoff, admin can inspect via `/api/admin/jobs`
|
- [ ] AC-6: CORS origin prompt replaced with FQDN/hostname input; CORS string is derived from that.
|
||||||
- [x] AC-7: Channel protocol document exists with Matrix integration points defined, reviewed, and approved
|
- [ ] AC-7: Skill / additional feature install section is reworked until it is actually usable end-to-end (worker defines the concrete failure modes during diagnosis).
|
||||||
- [x] AC-8: Embeddings run on Ollama local models (no external API dependency for vector operations)
|
- [ ] AC-8: First-run flow has a drill-down main menu with at least `Plugins` (Recommended / Custom), `Providers`, and the other top-level configuration groups. Linear interrogation is gone.
|
||||||
- [x] AC-9: All five providers (Anthropic, OpenAI, OpenRouter, Z.ai, Ollama) connect, list models, and complete chat requests
|
- [ ] AC-9: `Quick Start` path completes with a minimal, curated set of questions (target: under 90 seconds for a returning user; define the exact baseline during design).
|
||||||
- [x] AC-10: Routing transparency — TUI displays which model was selected and the routing reason for each response
|
- [ ] AC-10: Provider setup happens first, driven by a natural-language intake prompt. The agent expounds on the user's intent and chooses its own name based on that intent (OpenClaw-style). Naming is confirmable / overridable.
|
||||||
|
- [ ] AC-11: All milestones ship as merged PRs with green CI and closed issues.
|
||||||
|
|
||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
| # | ID | Name | Status | Branch | Issue | Started | Completed |
|
| # | ID | Name | Status | Branch | Issue | Started | Completed |
|
||||||
| --- | ------ | ---------------------------------- | ------ | ------ | --------- | ---------- | ---------- |
|
| --- | ------- | ------------------------------------------------------------ | ----------- | ---------------------- | ----- | ---------- | ---------- |
|
||||||
| 1 | ms-166 | Conversation Persistence & Context | done | — | #224–#231 | 2026-03-21 | 2026-03-21 |
|
| 1 | IUV-M01 | Hotfix: bootstrap DTO + wizard failure + port prefill + copy | complete | fix/bootstrap-hotfix | #436 | 2026-04-05 | 2026-04-05 |
|
||||||
| 2 | ms-167 | Security & Isolation | done | — | #232–#239 | 2026-03-21 | 2026-03-21 |
|
| 2 | IUV-M02 | UX polish: CORS/FQDN, skill installer rework | complete | feat/install-ux-polish | #437 | 2026-04-05 | 2026-04-05 |
|
||||||
| 3 | ms-168 | Provider Integration | done | — | #240–#251 | 2026-03-21 | 2026-03-22 |
|
| 3 | IUV-M03 | Provider-first intelligent flow + drill-down main menu | not-started | feat/install-ux-intent | #438 | — | — |
|
||||||
| 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
|
## Subagent Delegation Plan
|
||||||
|
|
||||||
| Target | URL | Method |
|
| Milestone | Recommended Tier | Rationale |
|
||||||
| -------------------- | --------- | -------------------------- |
|
| --------- | ---------------- | --------------------------------------------------------------------- |
|
||||||
| Docker Compose (dev) | localhost | docker compose up |
|
| IUV-M01 | sonnet | Tight bug cluster with known fix sites + small release cycle |
|
||||||
| Production | TBD | Docker Swarm via Portainer |
|
| IUV-M02 | sonnet | UX rework, moderate surface, diagnostic-heavy for the skill installer |
|
||||||
|
| IUV-M03 | opus | Architectural redesign of first-run flow, state machine + LLM intake |
|
||||||
|
|
||||||
## Coordination
|
## Risks
|
||||||
|
|
||||||
- **Primary Agent:** claude-opus-4-6
|
- **Hotfix regression surface** — the `import type` → `import` fix on the DTO class is one character but needs an integration test that binds the real DTO, not just a controller unit test, to prevent the same class-erasure regression from sneaking back in.
|
||||||
- **Sibling Agents:** sonnet (workers), haiku (verification)
|
- **LLM-driven intake latency / offline** — M03's provider-first intent flow assumes an available LLM call to expound on user input and choose a name. Offline installs need a deterministic fallback.
|
||||||
- **Shared Contracts:** docs/PRD-Harness_Foundation.md, docs/TASKS.md
|
- **Menu vs. linear back-compat** — M03 changes the top-level flow shape; existing `tools/install.sh --yes` + env-var headless path must continue to work.
|
||||||
|
- **Scope creep in M03** — "redesign the wizard" can absorb arbitrary work. Keep it bounded with explicit non-goals.
|
||||||
|
|
||||||
## Token Budget
|
## Out of Scope
|
||||||
|
|
||||||
| Metric | Value |
|
- Migrating the wizard to a GUI / web UI (still terminal-first)
|
||||||
| ------ | ------ |
|
- Replacing the Gitea registry or the Woodpecker publish pipeline
|
||||||
| Budget | — |
|
- Multi-tenant / multi-user onboarding (still single-admin bootstrap)
|
||||||
| Used | ~2.5M |
|
- Reworking `mosaic uninstall` (M01 of the parent mission — stable)
|
||||||
| 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`
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ for any `<Image>` components added in the future.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run the DB migration (requires a live DB)
|
# Run the DB migration (requires a live DB)
|
||||||
pnpm --filter @mosaic/db exec drizzle-kit migrate
|
pnpm --filter @mosaicstack/db exec drizzle-kit migrate
|
||||||
|
|
||||||
# Or, in Docker/Swarm — migrations run automatically on gateway startup
|
# Or, in Docker/Swarm — migrations run automatically on gateway startup
|
||||||
# via runMigrations() in packages/db/src/migrate.ts
|
# 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
|
- **Ink 5** (React for CLI) — already in deps
|
||||||
- **Component architecture** — break monolithic `app.tsx` into composable components
|
- **Component architecture** — break monolithic `app.tsx` into composable components
|
||||||
- **Typed Socket.IO events** — leverage `@mosaic/types` `ServerToClientEvents` / `ClientToServerEvents`
|
- **Typed Socket.IO events** — leverage `@mosaicstack/types` `ServerToClientEvents` / `ClientToServerEvents`
|
||||||
- **Local state only** (Wave 1) — cwd/branch read from `process.cwd()` and `git` at startup
|
- **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
|
- **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
|
- **Best-Guess Mode:** true
|
||||||
- Repo (target): `git.mosaicstack.dev/mosaic/mosaic-stack`
|
- Repo (target): `git.mosaicstack.dev/mosaic/mosaic-stack`
|
||||||
- Baseline: `~/src/jarvis-old` (jarvis v0.2.0)
|
- Baseline: `~/src/jarvis-old` (jarvis v0.2.0)
|
||||||
- Package source: `~/src/mosaic-mono-v0` (@mosaic/\* packages)
|
- Package source: `~/src/mosaic-mono-v0` (@mosaicstack/\* packages)
|
||||||
- Agent harness: [pi](https://github.com/badlogic/pi-mono) (v0.57.1)
|
- Agent harness: [pi](https://github.com/badlogic/pi-mono) (v0.57.1)
|
||||||
- Remote control reference: [OpenClaw](https://github.com/openclaw/openclaw) (upstream, canonical)
|
- Remote control reference: [OpenClaw](https://github.com/openclaw/openclaw) (upstream, canonical)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## Problem Statement
|
## 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 `@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.
|
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.
|
||||||
|
|
||||||
**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.
|
**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
|
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
|
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
|
6. **Multi-user with SSO** — BetterAuth with Authentik/WorkOS/Keycloak SSO, RBAC for family/team/business use
|
||||||
7. **Full @mosaic/\* package integration** — brain, queue, coord, mosaic, prdy, quality-rails, cli all integrated
|
7. **Full @mosaicstack/\* 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
|
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
|
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
|
2. Pi TUI integration — terminal-based agent interaction using Pi SDK
|
||||||
3. Web dashboard — settings, task management, projects, PRDs, missions, agent status
|
3. Web dashboard — settings, task management, projects, PRDs, missions, agent status
|
||||||
4. Gateway orchestrator (`@mosaic/gateway`) — central dispatch for agent tasks with routing logic
|
4. Gateway orchestrator (`@mosaicstack/gateway`) — central dispatch for agent tasks with routing logic
|
||||||
5. Task management — CRUD, kanban, mission-scoped tasks, dependency tracking
|
5. Task management — CRUD, kanban, mission-scoped tasks, dependency tracking
|
||||||
6. Project management — projects, milestones, PRDs linked to missions
|
6. Project management — projects, milestones, PRDs linked to missions
|
||||||
7. Shared memory system — learned preferences, behaviors, defaults; tiered storage with summarization
|
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)
|
12. Agent routing — task-based model/provider selection (cost/capability matrix)
|
||||||
13. MCP capability — server and client, tool registration
|
13. MCP capability — server and client, tool registration
|
||||||
14. Skill import interface — browse, install, manage agent skills
|
14. Skill import interface — browse, install, manage agent skills
|
||||||
15. `@mosaic/brain` — structured data layer (migrated to PG + vector DB backend)
|
15. `@mosaicstack/brain` — structured data layer (migrated to PG + vector DB backend)
|
||||||
16. `@mosaic/queue` — Valkey-backed task queue with MCP tools
|
16. `@mosaicstack/queue` — Valkey-backed task queue with MCP tools
|
||||||
17. `@mosaic/coord` — mission coordination engine
|
17. `@mosaicstack/coord` — mission coordination engine
|
||||||
18. `@mosaic/mosaic` — install wizard / bootstrap
|
18. `@mosaicstack/mosaic` — install wizard / bootstrap
|
||||||
19. `@mosaic/prdy` — PRD wizard
|
19. `@mosaicstack/prdy` — PRD wizard
|
||||||
20. `@mosaic/quality-rails` — code quality scaffolder
|
20. `@mosaicstack/quality-rails` — code quality scaffolder
|
||||||
21. `@mosaic/cli` — unified `mosaic` CLI
|
21. `@mosaicstack/cli` — unified `mosaic` CLI
|
||||||
22. Docker Compose deployment + bare-metal capability
|
22. Docker Compose deployment + bare-metal capability
|
||||||
23. Agent log service — ingest, parse, tier, summarize agent interaction logs
|
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
|
|||||||
│ └──────────────┴───────┬───────┴────────────────┘ │
|
│ └──────────────┴───────┬───────┴────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ┌─────────▼──────────┐ │
|
│ ┌─────────▼──────────┐ │
|
||||||
│ │ @mosaic/gateway │ ← Central Orchestrator│
|
│ │ @mosaicstack/gateway │ ← Central Orchestrator│
|
||||||
│ │ (NestJS+Fastify) │ │
|
│ │ (NestJS+Fastify) │ │
|
||||||
│ └────┬────┬────┬─────┘ │
|
│ └────┬────┬────┬─────┘ │
|
||||||
│ │ │ │ │
|
│ │ │ │ │
|
||||||
│ ┌──────────────┤ │ ├──────────────┐ │
|
│ ┌──────────────┤ │ ├──────────────┐ │
|
||||||
│ │ │ │ │ │ │
|
│ │ │ │ │ │ │
|
||||||
│ ┌───────▼──────┐ ┌────▼────▼──┐ │ ┌───────────▼────────┐ │
|
│ ┌───────▼──────┐ ┌────▼────▼──┐ │ ┌───────────▼────────┐ │
|
||||||
│ │ @mosaic/brain│ │ @mosaic/ │ │ │ Agent Pool │ │
|
│ │ @mosaicstack/brain│ │ @mosaicstack/ │ │ │ Agent Pool │ │
|
||||||
│ │ (Data Layer) │ │ queue │ │ │ (Pi SDK sessions) │ │
|
│ │ (Data Layer) │ │ queue │ │ │ (Pi SDK sessions) │ │
|
||||||
│ └───────┬──────┘ └────────────┘ │ │ - Anthropic │ │
|
│ └───────┬──────┘ └────────────┘ │ │ - Anthropic │ │
|
||||||
│ │ │ │ - Codex │ │
|
│ │ │ │ - Codex │ │
|
||||||
@@ -111,12 +111,12 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
|
|||||||
│ └──────────────┴───────────┘ │ │ - llama.cpp │ │
|
│ └──────────────┴───────────┘ │ │ - llama.cpp │ │
|
||||||
│ │ └────────────────────┘ │
|
│ │ └────────────────────┘ │
|
||||||
│ ┌─────────────▼──────┐ │
|
│ ┌─────────────▼──────┐ │
|
||||||
│ │ @mosaic/coord │ │
|
│ │ @mosaicstack/coord │ │
|
||||||
│ │ Mission lifecycle │ │
|
│ │ Mission lifecycle │ │
|
||||||
│ └────────────────────┘ │
|
│ └────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||||
│ │ @mosaic/cli │ │ @mosaic/prdy │ │ @mosaic/ │ │
|
│ │ @mosaicstack/cli │ │ @mosaicstack/prdy │ │ @mosaicstack/ │ │
|
||||||
│ │ │ │ │ │ quality-rails │ │
|
│ │ │ │ │ │ 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 |
|
| Layer | Technology | Rationale |
|
||||||
| ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
| ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||||
| **Web Frontend** | Next.js 16 + React 19 + Tailwind CSS | SSR, RSC; design tokens from @mosaic/design-tokens (mosaic-stack-website) |
|
| **Web Frontend** | Next.js 16 + React 19 + Tailwind CSS | SSR, RSC; design tokens from @mosaicstack/design-tokens (mosaic-stack-website) |
|
||||||
| **API / Gateway** | NestJS + Fastify adapter | Module system, DI, guards/interceptors for complex gateway; Fastify performance underneath |
|
| **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 |
|
| **Agent Runtime** | Pi SDK (embedded) | Extensible harness with tools, skills, session management |
|
||||||
| **TUI** | Pi interactive mode | Native terminal agent interaction |
|
| **TUI** | Pi interactive mode | Native terminal agent interaction |
|
||||||
| **Auth** | BetterAuth + SSO adapters | Multi-user RBAC with Authentik/WorkOS/Keycloak |
|
| **Auth** | BetterAuth + SSO adapters | Multi-user RBAC with Authentik/WorkOS/Keycloak |
|
||||||
| **Database** | PostgreSQL 17 + pgvector | Canonical store; pgvector for embedding search |
|
| **Database** | PostgreSQL 17 + pgvector | Canonical store; pgvector for embedding search |
|
||||||
| **Vector DB** | pgvector + VectorStore interface | pgvector for v0.1.0; `VectorStore` abstraction in @mosaic/memory makes Qdrant a drop-in later |
|
| **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 @mosaic/queue |
|
| **Cache / Queue** | Valkey 8 | Redis-compatible; proven in @mosaicstack/queue |
|
||||||
| **ORM** | Drizzle ORM | TypeScript-native, lightweight, good migration story |
|
| **ORM** | Drizzle ORM | TypeScript-native, lightweight, good migration story |
|
||||||
| **Validation** | Zod | Already used across @mosaic/\* packages |
|
| **Validation** | Zod | Already used across @mosaicstack/\* packages |
|
||||||
| **Build** | pnpm workspaces + Turborepo | Proven in both jarvis-old and mosaic-mono-v0 |
|
| **Build** | pnpm workspaces + Turborepo | Proven in both jarvis-old and mosaic-mono-v0 |
|
||||||
| **Testing** | Vitest + Playwright | Unit/integration via Vitest, E2E via Playwright |
|
| **Testing** | Vitest + Playwright | Unit/integration via Vitest, E2E via Playwright |
|
||||||
| **Remote Control** | Discord.js + Telegraf | Inspired by OpenClaw plugin architecture |
|
| **Remote Control** | Discord.js + Telegraf | Inspired by OpenClaw plugin architecture |
|
||||||
| **MCP** | @modelcontextprotocol/sdk | Already used in @mosaic/brain and @mosaic/queue |
|
| **MCP** | @modelcontextprotocol/sdk | Already used in @mosaicstack/brain and @mosaicstack/queue |
|
||||||
| **Container** | Docker Compose | Self-hosted; bare-metal also supported |
|
| **Container** | Docker Compose | Self-hosted; bare-metal also supported |
|
||||||
| **CI** | Woodpecker CI | Existing infrastructure at git.mosaicstack.dev |
|
| **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 |
|
| **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.
|
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)**
|
**AD-3: Gateway as the central nervous system (NestJS + Fastify adapter)**
|
||||||
`@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.
|
`@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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
**AD-4: Brain migrates from JSON files to PostgreSQL**
|
**AD-4: Brain migrates from JSON files to PostgreSQL**
|
||||||
`@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.
|
`@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.
|
||||||
|
|
||||||
**AD-5: Tiered memory with summarization**
|
**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.
|
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)**
|
**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.
|
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 @mosaic/design-tokens**
|
**AD-13: Design system from @mosaicstack/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.
|
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-14: Multi-tier deployment readiness**
|
**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.
|
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/
|
mosaic-mono-v1/
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── web/ Next.js 16 web dashboard
|
│ ├── web/ Next.js 16 web dashboard
|
||||||
│ └── gateway/ @mosaic/gateway — NestJS API + WebSocket
|
│ └── gateway/ @mosaicstack/gateway — NestJS API + WebSocket
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── types/ @mosaic/types — shared type contracts
|
│ ├── types/ @mosaicstack/types — shared type contracts
|
||||||
│ ├── brain/ @mosaic/brain — data layer (PG-backed)
|
│ ├── brain/ @mosaicstack/brain — data layer (PG-backed)
|
||||||
│ ├── queue/ @mosaic/queue — Valkey task queue + MCP
|
│ ├── queue/ @mosaicstack/queue — Valkey task queue + MCP
|
||||||
│ ├── coord/ @mosaic/coord — mission coordination
|
│ ├── coord/ @mosaicstack/coord — mission coordination
|
||||||
│ ├── mosaic/ @mosaic/mosaic — install wizard
|
│ ├── mosaic/ @mosaicstack/mosaic — install wizard
|
||||||
│ ├── prdy/ @mosaic/prdy — PRD wizard
|
│ ├── prdy/ @mosaicstack/prdy — PRD wizard
|
||||||
│ ├── quality-rails/ @mosaic/quality-rails — code quality scaffolder
|
│ ├── quality-rails/ @mosaicstack/quality-rails — code quality scaffolder
|
||||||
│ ├── cli/ @mosaic/cli — unified CLI
|
│ ├── cli/ @mosaicstack/cli — unified CLI
|
||||||
│ ├── auth/ @mosaic/auth — BetterAuth config + SSO adapters
|
│ ├── auth/ @mosaicstack/auth — BetterAuth config + SSO adapters
|
||||||
│ ├── db/ @mosaic/db — Drizzle schema, migrations, connection
|
│ ├── db/ @mosaicstack/db — Drizzle schema, migrations, connection
|
||||||
│ ├── agent/ @mosaic/agent — Pi SDK integration, agent pool manager
|
│ ├── agent/ @mosaicstack/agent — Pi SDK integration, agent pool manager
|
||||||
│ ├── memory/ @mosaic/memory — tiered memory + summarization service
|
│ ├── memory/ @mosaicstack/memory — tiered memory + summarization service
|
||||||
│ ├── log/ @mosaic/log — agent log ingest + processing
|
│ ├── log/ @mosaicstack/log — agent log ingest + processing
|
||||||
│ └── design-tokens/ @mosaic/design-tokens — CSS vars, Tailwind preset, colors
|
│ └── design-tokens/ @mosaicstack/design-tokens — CSS vars, Tailwind preset, colors
|
||||||
├── plugins/
|
├── plugins/
|
||||||
│ ├── discord/ @mosaic/discord-plugin — Discord channel
|
│ ├── discord/ @mosaicstack/discord-plugin — Discord channel
|
||||||
│ └── telegram/ @mosaic/telegram-plugin — Telegram channel
|
│ └── telegram/ @mosaicstack/telegram-plugin — Telegram channel
|
||||||
├── docker/
|
├── docker/
|
||||||
│ ├── gateway.Dockerfile
|
│ ├── gateway.Dockerfile
|
||||||
│ ├── web.Dockerfile
|
│ ├── web.Dockerfile
|
||||||
@@ -244,7 +244,7 @@ mosaic-mono-v1/
|
|||||||
|
|
||||||
### Package Responsibilities
|
### Package Responsibilities
|
||||||
|
|
||||||
#### `apps/gateway` — @mosaic/gateway (NEW — critical path)
|
#### `apps/gateway` — @mosaicstack/gateway (NEW — critical path)
|
||||||
|
|
||||||
The central nervous system. All clients connect here. Built with NestJS (Fastify adapter).
|
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)
|
- User management (admin RBAC panel)
|
||||||
- Auth pages (login, SSO redirect, registration)
|
- Auth pages (login, SSO redirect, registration)
|
||||||
|
|
||||||
#### `packages/types` — @mosaic/types
|
#### `packages/types` — @mosaicstack/types
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0. Extended with:
|
Migrated from mosaic-mono-v0. Extended with:
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ Migrated from mosaic-mono-v0. Extended with:
|
|||||||
- Memory types (preference, insight, summary)
|
- Memory types (preference, insight, summary)
|
||||||
- Plugin channel types (Discord, Telegram message mapping)
|
- Plugin channel types (Discord, Telegram message mapping)
|
||||||
|
|
||||||
#### `packages/brain` — @mosaic/brain
|
#### `packages/brain` — @mosaicstack/brain
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0. **Storage backend changes from JSON to PostgreSQL.**
|
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: computed endpoints (today, stale, stats, search, audit) run against PG
|
||||||
- New: appreciation collection preserved for family use
|
- New: appreciation collection preserved for family use
|
||||||
|
|
||||||
#### `packages/queue` — @mosaic/queue
|
#### `packages/queue` — @mosaicstack/queue
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0 with minimal changes.
|
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
|
- MCP server with 8 tools
|
||||||
- Used by gateway for agent task dispatch and coordination
|
- Used by gateway for agent task dispatch and coordination
|
||||||
|
|
||||||
#### `packages/coord` — @mosaic/coord
|
#### `packages/coord` — @mosaicstack/coord
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0.
|
Migrated from mosaic-mono-v0.
|
||||||
|
|
||||||
@@ -342,7 +342,7 @@ Migrated from mosaic-mono-v0.
|
|||||||
- Continuation prompt generation
|
- Continuation prompt generation
|
||||||
- Integration with gateway for mission-driven orchestration
|
- Integration with gateway for mission-driven orchestration
|
||||||
|
|
||||||
#### `packages/db` — @mosaic/db (NEW)
|
#### `packages/db` — @mosaicstack/db (NEW)
|
||||||
|
|
||||||
Shared database package.
|
Shared database package.
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ Shared database package.
|
|||||||
- Connection pool configuration
|
- Connection pool configuration
|
||||||
- Shared by gateway, brain, auth, memory
|
- Shared by gateway, brain, auth, memory
|
||||||
|
|
||||||
#### `packages/auth` — @mosaic/auth (NEW)
|
#### `packages/auth` — @mosaicstack/auth (NEW)
|
||||||
|
|
||||||
Authentication and authorization.
|
Authentication and authorization.
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ Authentication and authorization.
|
|||||||
- API key generation for brain/MCP access
|
- API key generation for brain/MCP access
|
||||||
- Session management middleware
|
- Session management middleware
|
||||||
|
|
||||||
#### `packages/agent` — @mosaic/agent (NEW — critical path)
|
#### `packages/agent` — @mosaicstack/agent (NEW — critical path)
|
||||||
|
|
||||||
Pi SDK integration layer.
|
Pi SDK integration layer.
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ Pi SDK integration layer.
|
|||||||
- Skill management — loads and configures Pi skills for agent sessions
|
- Skill management — loads and configures Pi skills for agent sessions
|
||||||
- Session lifecycle — create, monitor, complete, fail, timeout
|
- Session lifecycle — create, monitor, complete, fail, timeout
|
||||||
|
|
||||||
#### `packages/memory` — @mosaic/memory (NEW)
|
#### `packages/memory` — @mosaicstack/memory (NEW)
|
||||||
|
|
||||||
Tiered memory system.
|
Tiered memory system.
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@ Tiered memory system.
|
|||||||
- Summarization pipeline — compress raw logs into structured insights
|
- Summarization pipeline — compress raw logs into structured insights
|
||||||
- Memory API — used by gateway and agent sessions
|
- Memory API — used by gateway and agent sessions
|
||||||
|
|
||||||
#### `packages/log` — @mosaic/log (NEW)
|
#### `packages/log` — @mosaicstack/log (NEW)
|
||||||
|
|
||||||
Agent log service.
|
Agent log service.
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ Agent log service.
|
|||||||
- Summarization trigger — invokes cheap LLM to compress aging logs
|
- Summarization trigger — invokes cheap LLM to compress aging logs
|
||||||
- Retention policy — configurable TTLs per tier
|
- Retention policy — configurable TTLs per tier
|
||||||
|
|
||||||
#### `packages/mosaic` — @mosaic/mosaic
|
#### `packages/mosaic` — @mosaicstack/mosaic
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0, updated for v1.
|
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
|
- Detects existing installations, offers upgrade path
|
||||||
- Configures `~/.config/mosaic/` with guides, tools, runtime configs
|
- Configures `~/.config/mosaic/` with guides, tools, runtime configs
|
||||||
|
|
||||||
#### `packages/prdy` — @mosaic/prdy
|
#### `packages/prdy` — @mosaicstack/prdy
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0.
|
Migrated from mosaic-mono-v0.
|
||||||
|
|
||||||
@@ -408,7 +408,7 @@ Migrated from mosaic-mono-v0.
|
|||||||
- Template-based PRD creation with Zod validation
|
- Template-based PRD creation with Zod validation
|
||||||
- CLI integration via `mosaic prdy`
|
- CLI integration via `mosaic prdy`
|
||||||
|
|
||||||
#### `packages/quality-rails` — @mosaic/quality-rails
|
#### `packages/quality-rails` — @mosaicstack/quality-rails
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0.
|
Migrated from mosaic-mono-v0.
|
||||||
|
|
||||||
@@ -416,15 +416,15 @@ Migrated from mosaic-mono-v0.
|
|||||||
- Generates ESLint, tsconfig, Woodpecker, husky, lint-staged configs
|
- Generates ESLint, tsconfig, Woodpecker, husky, lint-staged configs
|
||||||
- Supports project types: monorepo, typescript-node, nextjs
|
- Supports project types: monorepo, typescript-node, nextjs
|
||||||
|
|
||||||
#### `packages/cli` — @mosaic/cli
|
#### `packages/cli` — @mosaicstack/cli
|
||||||
|
|
||||||
Migrated from mosaic-mono-v0, extended.
|
Migrated from mosaic-mono-v0, extended.
|
||||||
|
|
||||||
- Unified `mosaic` binary
|
- Unified `mosaic` binary
|
||||||
- Subcommands: `mosaic coord`, `mosaic prdy`, `mosaic queue`, `mosaic quality`, `mosaic gateway`, `mosaic brain`
|
- Subcommands: `mosaic coord`, `mosaic prdy`, `mosaic queue`, `mosaic quality`, `mosaic gateway`, `mosaic brain`
|
||||||
- Plugin discovery for installed @mosaic/\* packages
|
- Plugin discovery for installed @mosaicstack/\* packages
|
||||||
|
|
||||||
#### `plugins/discord` — @mosaic/discord-plugin (NEW — high priority)
|
#### `plugins/discord` — @mosaicstack/discord-plugin (NEW — high priority)
|
||||||
|
|
||||||
Discord remote control channel. Architecture inspired by OpenClaw (https://github.com/openclaw/openclaw).
|
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)
|
- Bot pairing and permission management (Discord user → Mosaic user mapping)
|
||||||
- DM support for private conversations
|
- DM support for private conversations
|
||||||
|
|
||||||
#### `plugins/telegram` — @mosaic/telegram-plugin (NEW)
|
#### `plugins/telegram` — @mosaicstack/telegram-plugin (NEW)
|
||||||
|
|
||||||
Telegram remote control channel.
|
Telegram remote control channel.
|
||||||
|
|
||||||
@@ -547,7 +547,7 @@ Telegram remote control channel.
|
|||||||
- WebSocket hub — real-time updates for chat, agent status, notifications
|
- WebSocket hub — real-time updates for chat, agent status, notifications
|
||||||
- Rate limiting and request validation
|
- Rate limiting and request validation
|
||||||
|
|
||||||
### FR-3: Agent Pool (@mosaic/agent)
|
### FR-3: Agent Pool (@mosaicstack/agent)
|
||||||
|
|
||||||
- Manage concurrent Pi SDK sessions
|
- Manage concurrent Pi SDK sessions
|
||||||
- Provider configuration: API key management, endpoint URLs, model lists
|
- 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 CRUD (linked to project and PRD)
|
||||||
- Mission tasks with phases, dependencies, ordering
|
- Mission tasks with phases, dependencies, ordering
|
||||||
- Mission summary with computed progress
|
- Mission summary with computed progress
|
||||||
- Mission coordination via @mosaic/coord
|
- Mission coordination via @mosaicstack/coord
|
||||||
- Active mission dashboard in web UI
|
- Active mission dashboard in web UI
|
||||||
|
|
||||||
### FR-7: Memory System
|
### FR-7: Memory System
|
||||||
@@ -844,7 +844,7 @@ Telegram remote control channel.
|
|||||||
- [ ] Database migrations run automatically on first start
|
- [ ] Database migrations run automatically on first start
|
||||||
- [ ] `.env.example` documents all required configuration
|
- [ ] `.env.example` documents all required configuration
|
||||||
|
|
||||||
### AC-11: @mosaic/\* Packages
|
### AC-11: @mosaicstack/\* Packages
|
||||||
|
|
||||||
- [ ] All 7 migrated packages build, pass tests, and integrate with gateway
|
- [ ] All 7 migrated packages build, pass tests, and integrate with gateway
|
||||||
- [ ] `mosaic` CLI provides subcommands for each package
|
- [ ] `mosaic` CLI provides subcommands for each package
|
||||||
@@ -870,7 +870,7 @@ Telegram remote control channel.
|
|||||||
|
|
||||||
| Risk | Likelihood | Impact | Mitigation |
|
| Risk | Likelihood | Impact | Mitigation |
|
||||||
| -------------------------------------------------- | ---------- | ------ | ---------------------------------------------------------------------------------------- |
|
| -------------------------------------------------- | ---------- | ------ | ---------------------------------------------------------------------------------------- |
|
||||||
| Pi SDK API instability (pre-1.0) | Medium | High | Pin version, abstract behind @mosaic/agent interface |
|
| Pi SDK API instability (pre-1.0) | Medium | High | Pin version, abstract behind @mosaicstack/agent interface |
|
||||||
| Brain PG migration complexity | Medium | Medium | Preserve Brain REST/MCP API contract; only storage changes |
|
| 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 |
|
| 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 |
|
| 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 |
|
| # | 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 `@mosaic/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 `@mosaicstack/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) |
|
| 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. |
|
| 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. |
|
| 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)
|
### Phase 0: Foundation (v0.0.1)
|
||||||
|
|
||||||
- Scaffold monorepo (pnpm + turbo + tsconfig + eslint + vitest)
|
- Scaffold monorepo (pnpm + turbo + tsconfig + eslint + vitest)
|
||||||
- `@mosaic/types` — migrate and extend from v0
|
- `@mosaicstack/types` — migrate and extend from v0
|
||||||
- `@mosaic/db` — Drizzle schema, PG connection, migrations
|
- `@mosaicstack/db` — Drizzle schema, PG connection, migrations
|
||||||
- `@mosaic/auth` — BetterAuth setup with email/password
|
- `@mosaicstack/auth` — BetterAuth setup with email/password
|
||||||
- OTEL foundation — `@opentelemetry/sdk-node` setup, SigNoz in docker-compose, trace propagation wired
|
- OTEL foundation — `@opentelemetry/sdk-node` setup, SigNoz in docker-compose, trace propagation wired
|
||||||
- Docker Compose (PG 17 + Valkey + SigNoz)
|
- Docker Compose (PG 17 + Valkey + SigNoz)
|
||||||
- CI pipeline (Woodpecker)
|
- 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)
|
### Phase 1: Core API (v0.0.2)
|
||||||
|
|
||||||
- `apps/gateway` — NestJS server (Fastify adapter), auth middleware, health endpoints
|
- `apps/gateway` — NestJS server (Fastify adapter), auth middleware, health endpoints
|
||||||
- `@mosaic/brain` — migrate from v0, swap JSON store for PG via @mosaic/db
|
- `@mosaicstack/brain` — migrate from v0, swap JSON store for PG via @mosaicstack/db
|
||||||
- `@mosaic/queue` — migrate from v0 (minimal changes)
|
- `@mosaicstack/queue` — migrate from v0 (minimal changes)
|
||||||
- Gateway routes: conversations, tasks, projects, missions
|
- Gateway routes: conversations, tasks, projects, missions
|
||||||
- WebSocket server for chat streaming
|
- WebSocket server for chat streaming
|
||||||
- Basic agent dispatch (single provider, no routing)
|
- Basic agent dispatch (single provider, no routing)
|
||||||
|
|
||||||
### Phase 2: Agent Layer (v0.0.3)
|
### Phase 2: Agent Layer (v0.0.3)
|
||||||
|
|
||||||
- `@mosaic/agent` — Pi SDK integration, agent pool manager
|
- `@mosaicstack/agent` — Pi SDK integration, agent pool manager
|
||||||
- Multi-provider support (Anthropic + Ollama minimum)
|
- Multi-provider support (Anthropic + Ollama minimum)
|
||||||
- Agent routing engine (cost/capability matrix)
|
- Agent routing engine (cost/capability matrix)
|
||||||
- Tool registration (brain, queue, memory tools injected into agent sessions)
|
- Tool registration (brain, queue, memory tools injected into agent sessions)
|
||||||
- `@mosaic/coord` — migrate from v0, integrate with gateway
|
- `@mosaicstack/coord` — migrate from v0, integrate with gateway
|
||||||
|
|
||||||
### Phase 3: Web Dashboard (v0.0.4)
|
### 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)
|
### Phase 4: Memory & Intelligence (v0.0.5)
|
||||||
|
|
||||||
- `@mosaic/memory` — preference store, insight store, semantic search
|
- `@mosaicstack/memory` — preference store, insight store, semantic search
|
||||||
- `@mosaic/log` — log ingest, parsing, tiered storage
|
- `@mosaicstack/log` — log ingest, parsing, tiered storage
|
||||||
- Summarization pipeline
|
- Summarization pipeline
|
||||||
- Memory integration into agent sessions
|
- Memory integration into agent sessions
|
||||||
- Skill management interface (web UI + CLI)
|
- Skill management interface (web UI + CLI)
|
||||||
|
|
||||||
### Phase 5: Remote Control (v0.0.6)
|
### Phase 5: Remote Control (v0.0.6)
|
||||||
|
|
||||||
- `@mosaic/discord-plugin` — Discord channel plugin
|
- `@mosaicstack/discord-plugin` — Discord channel plugin
|
||||||
- `@mosaic/telegram-plugin` — Telegram channel plugin
|
- `@mosaicstack/telegram-plugin` — Telegram channel plugin
|
||||||
- Plugin host in gateway
|
- Plugin host in gateway
|
||||||
- SSO configuration (Authentik)
|
- SSO configuration (Authentik)
|
||||||
|
|
||||||
### Phase 6: CLI & Tools (v0.0.7)
|
### Phase 6: CLI & Tools (v0.0.7)
|
||||||
|
|
||||||
- `@mosaic/cli` — unified CLI with all subcommands
|
- `@mosaicstack/cli` — unified CLI with all subcommands
|
||||||
- `@mosaic/prdy` — migrate from v0
|
- `@mosaicstack/prdy` — migrate from v0
|
||||||
- `@mosaic/quality-rails` — migrate from v0
|
- `@mosaicstack/quality-rails` — migrate from v0
|
||||||
- `@mosaic/mosaic` — install wizard updated for v1
|
- `@mosaicstack/mosaic` — install wizard updated for v1
|
||||||
- Pi TUI integration (`mosaic tui`)
|
- Pi TUI integration (`mosaic tui`)
|
||||||
|
|
||||||
### Phase 7: Polish & Beta (v0.0.8 → v0.1.0)
|
### 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
|
## 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). `@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.
|
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.
|
||||||
|
|
||||||
2. RESOLVED: **Authentik is the first SSO provider** — confirmed, already running in Jason's infrastructure. WorkOS and Keycloak adapters follow in Phase 7.
|
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"). @mosaic/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"). @mosaicstack/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.
|
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.
|
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 @mosaic/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 @mosaicstack/agent — only configuration management.
|
||||||
|
|||||||
@@ -108,4 +108,4 @@ The web login page renders provider buttons from `NEXT_PUBLIC_*_ENABLED` flags.
|
|||||||
|
|
||||||
## Failure mode
|
## 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, `@mosaic/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, `@mosaicstack/auth` throws a bootstrap error with the missing keys instead of silently registering a broken provider.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user