Skip to content
Closed

Main #215

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
52a57ed
Merge pull request #74 from trycompai/main
carhartlewis Aug 7, 2026
1561073
chore: release release
github-actions[bot] Aug 7, 2026
5074fc4
Merge pull request #75 from trycompai/release-please--branches--release
carhartlewis Aug 7, 2026
3c20d80
Merge pull request #77 from trycompai/main
carhartlewis Aug 7, 2026
808b835
Merge pull request #79 from trycompai/main
carhartlewis Aug 7, 2026
407280a
Merge pull request #81 from trycompai/main
carhartlewis Aug 7, 2026
c26a08d
Merge pull request #84 from trycompai/main
carhartlewis Aug 7, 2026
d585dc3
Merge pull request #90 from trycompai/main
carhartlewis Aug 8, 2026
d0299d9
Merge pull request #98 from trycompai/main
carhartlewis Aug 11, 2026
7d4a573
Merge pull request #107 from trycompai/main
carhartlewis Aug 11, 2026
56f4eeb
Merge pull request #116 from trycompai/main
github-actions[bot] Aug 11, 2026
57001e6
Merge pull request #119 from trycompai/main
github-actions[bot] Aug 11, 2026
ad1d702
Merge pull request #122 from trycompai/main
github-actions[bot] Aug 11, 2026
fc0c594
Merge pull request #127 from trycompai/main
github-actions[bot] Aug 11, 2026
4ffe150
Merge pull request #130 from trycompai/main
github-actions[bot] Aug 11, 2026
14cd220
Merge pull request #135 from trycompai/main
github-actions[bot] Aug 11, 2026
f2484fb
Merge pull request #141 from trycompai/main
github-actions[bot] Aug 12, 2026
bb63520
Merge pull request #161 from trycompai/main
github-actions[bot] Aug 18, 2026
517d859
Merge pull request #165 from trycompai/main
github-actions[bot] Aug 20, 2026
b842bd6
Merge pull request #168 from trycompai/main
github-actions[bot] Aug 20, 2026
77089e4
Merge pull request #172 from trycompai/main
github-actions[bot] Aug 20, 2026
6d4793d
Merge pull request #177 from trycompai/main
github-actions[bot] Aug 21, 2026
167abb4
infra: add production Dockerfile for apps/api
franckh-stack Aug 28, 2026
a28aabb
merge: bring apps/api Dockerfile (Task 3) onto release, the fork's ac…
franckh-stack Aug 28, 2026
e9af385
infra: add production Dockerfile for apps/app
franckh-stack Aug 28, 2026
00cebb9
fix(infra): apps/app runtime image is node:22-slim directly, no bun
franckh-stack Aug 28, 2026
595a6f6
feat(agent): route the LLM through DeepSeek instead of Vercel AI Gate…
franckh-stack Aug 28, 2026
eef531c
fix(infra): slim api/agent runtime images to production deps only
franckh-stack Aug 29, 2026
120df57
fix(infra): skip postinstall scripts on the production-only reinstall
franckh-stack Aug 29, 2026
7a0a50a
fix(infra): move @crm/typescript-config and typescript to agent's dep…
franckh-stack Aug 29, 2026
5438f58
feat(gmail): add searchByParticipant for contact history backfill
franckh-stack Sep 2, 2026
68c8b34
feat(calendar): add q param + searchByParticipant for contact history…
franckh-stack Sep 2, 2026
573c89c
refactor(gmail): extract parseGmailMessage into a pure, reusable module
franckh-stack Sep 2, 2026
319f3ac
feat(mailbox): ThreadWriterService.store() accepts preresolved compan…
franckh-stack Sep 2, 2026
03a91cc
feat(calendar): CalendarSyncService gains public apply()/preresolved/…
franckh-stack Sep 2, 2026
e97d9b2
feat(contacts): add ContactHistoryBackfillService orchestrator
franckh-stack Sep 2, 2026
fae57ac
chore(di): wire ContactHistoryBackfillService into the module graph
franckh-stack Sep 2, 2026
6946d28
feat(contacts): wire the history backfill into ContactsService.create()
franckh-stack Sep 2, 2026
5b914c0
chore(lint): fix import ordering flagged by biome assist/source/organ…
franckh-stack Sep 2, 2026
4dd3000
Merge pull request #3 from franckh-stack/feat/contact-history-backfill
franckh-stack Sep 2, 2026
0ffb62c
fix(gmail): filter out mailing-list broadcasts from personal correspo…
franckh-stack Sep 2, 2026
e8ca9c2
Merge pull request #4 from franckh-stack/fix/filter-bulk-mail-noise
franckh-stack Sep 2, 2026
2c3192e
fix(activities): scope the Notes tab to notes/calls, not every synced…
franckh-stack Sep 2, 2026
cb6e6f2
Merge pull request #5 from franckh-stack/fix/notes-tab-scope
franckh-stack Sep 2, 2026
044e350
feat(activities): let a rep exclude an email thread from the CRM synt…
franckh-stack Sep 3, 2026
6a4892d
Merge pull request #6 from franckh-stack/feat/exclude-email-thread
franckh-stack Sep 3, 2026
c1f4305
feat(agent): notify VigieProcure on CRM deal lifecycle events
franckh-stack Sep 3, 2026
e65fb26
style: applique le formatage biome sur vigieprocure-bridge.spec.ts
franckh-stack Sep 4, 2026
46ea247
Merge pull request #7 from franckh-stack/feat/webhook-vigieprocure
franckh-stack Sep 4, 2026
db2184b
chore(lint): apply Biome safe fixes to apps/api (formatting only)
franckh-stack Sep 4, 2026
e507553
fix(anti-slop): rename error to cause in ContactHistoryBackfillServic…
franckh-stack Sep 4, 2026
8d4bb3f
fix(anti-slop): let stubCapturingUrl's return type infer instead of w…
franckh-stack Sep 4, 2026
cba97d1
fix(anti-slop): use toBeFunction() instead of typeof x === "function"
franckh-stack Sep 4, 2026
b645260
fix(anti-slop): name captured-value types by their owner, drop dead i…
franckh-stack Sep 4, 2026
a8184f6
Merge pull request #8 from franckh-stack/chore/fix-anti-slop
franckh-stack Sep 4, 2026
d22d613
fix(anti-slop): type VigieProcureEvent.payload with Prisma.InputJsonV…
franckh-stack Sep 4, 2026
1a99fc4
Merge pull request #9 from franckh-stack/chore/fix-vigieprocure-bridg…
franckh-stack Sep 4, 2026
9061103
feat(db): colonne siren sur Company, unicite scopee archivedAt
franckh-stack Sep 4, 2026
aec33d5
feat(db): migration SQL add_company_siren
franckh-stack Sep 4, 2026
3d8b5b8
Merge pull request #10 from franckh-stack/feat/company-siren-column
franckh-stack Sep 4, 2026
cfdb641
feat(companies): resolve and set SIREN via VigieProcure
franckh-stack Sep 4, 2026
5d16454
fix(companies): validate SIREN digits, drop unused conflict fields
franckh-stack Sep 4, 2026
9402fb9
style(companies): apply biome formatting to siren input schema
franckh-stack Sep 4, 2026
32b7f2b
Merge pull request #11 from franckh-stack/feat/company-siren-ui
franckh-stack Sep 5, 2026
49707d4
ci: build et push les 3 images CRM vers ghcr.io sur push release
franckh-stack Sep 5, 2026
aec4f9d
Merge pull request #12 from franckh-stack/feat/ci-build-images
franckh-stack Sep 5, 2026
650d364
fix(tracking): utilise APP_URL au lieu de l'origine derivee de la req…
franckh-stack Sep 12, 2026
314fbda
Merge pull request #15 from franckh-stack/fix/tracking-origin
franckh-stack Sep 12, 2026
e6b20bb
chore(main): release 1.16.0
github-actions[bot] Sep 12, 2026
d8eb2b0
Merge pull request #14 from franckh-stack/release-please--branches--m…
franckh-stack Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ GOOGLE_CLIENT_SECRET=""
# schedule.
# AGENT_BRIDGE_SECRET=""

# Notifies VigieProcure (the tender-monitoring product this CRM is paired
# with) when a deal's lifecycle changes -- same events already queued for
# the agent worker (deal.created, deal.stage.changed, deal.opened,
# deal.closed). Best-effort, HMAC-SHA256 signed on the raw request body
# (header X-VigieProcure-Signature: sha256=<hex>). Leave either unset and
# no bridge exists -- not an open one, same rule as AGENT_BRIDGE_SECRET
# above. Decided 2026-09-03 (DEC-C-CRM-10).
# VIGIEPROCURE_WEBHOOK_URL="https://api.vigieproc.fr/api/v1/crm/webhooks"
# VIGIEPROCURE_WEBHOOK_SECRET=""

# Lets a rep resolve a company's SIREN (the French business registry ID)
# against VigieProcure (GET /api/v1/companies/resolve), from the company
# sheet. Leave either unset and the sheet reports the lookup as not
# configured -- not an unauthenticated call, same rule as the pairs above.
# VIGIEPROCURE_API_JWT is a long-lived service token minted by VigieProcure
# (issue_jwt("service:crm-api", ...)) -- provisioned by Franck, not by this
# codebase.
# VIGIEPROCURE_API_URL="https://api.vigieproc.fr"
# VIGIEPROCURE_API_JWT=""

# PORT="3001"


Expand Down
2 changes: 1 addition & 1 deletion .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.15.3"
".": "1.16.0"
}
70 changes: 70 additions & 0 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Build & push CRM images

on:
push:
branches: [release]
paths:
- "apps/**"
- "packages/**"
- "package.json"
- "bun.lock"
- "turbo.json"
- ".github/workflows/build-images.yml"
workflow_dispatch: {}

concurrency:
group: build-images-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
# Domaine unique fige au build pour apps/app -- Next.js grave API_URL/
# APP_URL dans le bundle client compile, pas au runtime (piege deja
# rencontre en prod le 29/08/2026, cf. docker-compose.yml sur vigiep1).
CRM_ORIGIN: https://crm.vigieproc.fr

jobs:
build-push:
name: build-push (${{ matrix.service }})
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
service: [api, app, agent]

steps:
- uses: actions/checkout@v5

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}-${{ matrix.service }}
tags: |
type=raw,value=latest
type=sha,format=short

- uses: docker/build-push-action@v6
with:
context: .
file: apps/${{ matrix.service }}/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
API_URL=${{ env.CRM_ORIGIN }}
APP_URL=${{ env.CRM_ORIGIN }}
cache-from: type=gha,scope=${{ matrix.service }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
# Changelog

## [1.16.0](https://github.com/franckh-stack/crm/compare/v1.15.3...v1.16.0) (2026-09-12)


### Features

* **activities:** allow excluding an email thread from the CRM synthesis ([6a4892d](https://github.com/franckh-stack/crm/commit/6a4892d3f6f8d5c1f838487a1e3cab15c44db60a))
* **activities:** let a rep exclude an email thread from the CRM synthesis ([044e350](https://github.com/franckh-stack/crm/commit/044e350103e955417df5e26cd2f0c348e27e4600))
* **agent:** notify VigieProcure on CRM deal lifecycle events ([c1f4305](https://github.com/franckh-stack/crm/commit/c1f430510589ddb454bc906ad7821cfa75ba5e60))
* **agent:** route the LLM through DeepSeek instead of Vercel AI Gateway, add Dockerfile ([595a6f6](https://github.com/franckh-stack/crm/commit/595a6f6bb1aba2c681094b5c938bc8d0ea818cd2))
* **calendar:** add q param + searchByParticipant for contact history backfill ([68c8b34](https://github.com/franckh-stack/crm/commit/68c8b348170b735b68921d653a29f8647af93322))
* **calendar:** CalendarSyncService gains public apply()/preresolved/backfillForParticipant() ([03a91cc](https://github.com/franckh-stack/crm/commit/03a91cc8a8f84705b7da1c6f6716a3905e10a24c))
* **companies:** resolve and set SIREN via VigieProcure ([32b7f2b](https://github.com/franckh-stack/crm/commit/32b7f2bb4e9d0d67e68b79206aa41bf0a9aff716))
* **companies:** resolve and set SIREN via VigieProcure ([cfdb641](https://github.com/franckh-stack/crm/commit/cfdb64141ff58d29eeadf5d48d57a75e353c29ed))
* **contacts:** add ContactHistoryBackfillService orchestrator ([e97d9b2](https://github.com/franckh-stack/crm/commit/e97d9b2610ae1930ba5c5df0f4a3e7a015099ee8))
* **contacts:** automatic Gmail/Calendar history backfill on contact creation ([4dd3000](https://github.com/franckh-stack/crm/commit/4dd300041f587e263966c1a9dc1710049d0ef91a))
* **contacts:** wire the history backfill into ContactsService.create() ([6946d28](https://github.com/franckh-stack/crm/commit/6946d280b01aa7f87e177b19d22964d5bce3b42a))
* **db:** add siren column to Company, unique scoped on active records ([3d8b5b8](https://github.com/franckh-stack/crm/commit/3d8b5b853b08f22eec8e356f09d450174a3e2418))
* **db:** colonne siren sur Company, unicite scopee archivedAt ([9061103](https://github.com/franckh-stack/crm/commit/9061103e247fa8fd713fb686ac6e26f4589a604b))
* **db:** migration SQL add_company_siren ([aec33d5](https://github.com/franckh-stack/crm/commit/aec33d59927dddf32855b8524ba0d548a937d49e))
* **gmail:** add searchByParticipant for contact history backfill ([5438f58](https://github.com/franckh-stack/crm/commit/5438f58adac3bf8875e94701673575e29e684a4b))
* **mailbox:** ThreadWriterService.store() accepts preresolved company/contact + relinks ([319f3ac](https://github.com/franckh-stack/crm/commit/319f3ac5d85e3fb3b4920617d4f151e24caa2a02))
* release release ([aec4f9d](https://github.com/franckh-stack/crm/commit/aec4f9dcaa934b7147a7fd2ae527163c8aecf2a4))


### Fixes

* **activities:** Notes tab was showing every synced email and meeting ([cb6e6f2](https://github.com/franckh-stack/crm/commit/cb6e6f2473057ea65583bd68209ae7835b693b4e))
* **activities:** scope the Notes tab to notes/calls, not every synced email/meeting ([2c3192e](https://github.com/franckh-stack/crm/commit/2c3192e036ce7d47322cbdd25745ebdd990b7094))
* **anti-slop:** let stubCapturingUrl's return type infer instead of widening ([8d4bb3f](https://github.com/franckh-stack/crm/commit/8d4bb3f9ccfdf4c40108adc20cc9688c46cf45c3))
* **anti-slop:** name captured-value types by their owner, drop dead import ([b645260](https://github.com/franckh-stack/crm/commit/b64526014419238cdd6d23ff882b50d70b379413))
* **anti-slop:** rename error to cause in ContactHistoryBackfillService.failed ([e507553](https://github.com/franckh-stack/crm/commit/e507553f5eb809c9772794185e638d2b11e6d9b7))
* **anti-slop:** type VigieProcureEvent.payload with Prisma.InputJsonValue ([1a99fc4](https://github.com/franckh-stack/crm/commit/1a99fc4c53739f58204e7c5b937f6b75d1862beb))
* **anti-slop:** type VigieProcureEvent.payload with Prisma.InputJsonValue ([d22d613](https://github.com/franckh-stack/crm/commit/d22d6132665c2130d3cefb6953a2f44b5eee1b48))
* **anti-slop:** use toBeFunction() instead of typeof x === "function" ([cba97d1](https://github.com/franckh-stack/crm/commit/cba97d13ea82c0154b0f3a5ebfcc42c00f3dd67e))
* **companies:** validate SIREN digits, drop unused conflict fields ([5d16454](https://github.com/franckh-stack/crm/commit/5d164549f0ebaf9c489fbf36b6a99a377d870916))
* **gmail:** filter mailing-list broadcasts out of the contact relationship view ([e8ca9c2](https://github.com/franckh-stack/crm/commit/e8ca9c2bf4e1a2d0994f82af9ff5f716b6f9ae83))
* **gmail:** filter out mailing-list broadcasts from personal correspondence ([0ffb62c](https://github.com/franckh-stack/crm/commit/0ffb62cb8ff73a538cb012da4df9273798547e06))
* **infra:** apps/app runtime image is node:22-slim directly, no bun ([00cebb9](https://github.com/franckh-stack/crm/commit/00cebb9a0e721d88d1f0a2a8b3b02ea7dcbf29e0))
* **infra:** move @crm/typescript-config and typescript to agent's dependencies ([7a0a50a](https://github.com/franckh-stack/crm/commit/7a0a50a8981c6aaff2c378049b47e698a6268655))
* **infra:** skip postinstall scripts on the production-only reinstall ([120df57](https://github.com/franckh-stack/crm/commit/120df57babc2bde867345e418767b4e00699fd36))
* **infra:** slim api/agent runtime images to production deps only ([eef531c](https://github.com/franckh-stack/crm/commit/eef531c14d8d12678467c9d33b4755287ef77953))
* **tracking:** utilise APP_URL au lieu de l'origine derivee de la requete ([314fbda](https://github.com/franckh-stack/crm/commit/314fbda4d0f3090a54f777b22d8ff5e4d608ad8f))
* **tracking:** utilise APP_URL au lieu de l'origine derivee de la requete ([650d364](https://github.com/franckh-stack/crm/commit/650d3647b7790f8008a41cc17b9462caef4c9679))


### Refactors

* **gmail:** extract parseGmailMessage into a pure, reusable module ([573c89c](https://github.com/franckh-stack/crm/commit/573c89c74cf8549697c4d323090b26945a8c3ad9))

## [1.15.3](https://github.com/trycompai/crm/compare/v1.15.2...v1.15.3) (2026-08-21)


Expand Down
53 changes: 53 additions & 0 deletions apps/agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# apps/agent/Dockerfile
FROM oven/bun:1.3.12 AS deps
WORKDIR /repo
COPY package.json bun.lock turbo.json ./
COPY apps/api/package.json apps/api/package.json
COPY apps/api/scripts/ apps/api/scripts/
COPY apps/app/package.json apps/app/package.json
COPY apps/agent/package.json apps/agent/package.json
COPY packages/ packages/
# @crm/db's postinstall runs `prisma generate`, and prisma.config.ts resolves
# DATABASE_URL eagerly (env(...) throws if unset) even though `generate` never
# opens a connection. A build-time placeholder satisfies that check; the real
# value is supplied at `docker run` time and overrides this.
ENV DATABASE_URL="postgresql://user:password@localhost:5432/db?schema=public"
RUN bun install --frozen-lockfile

FROM deps AS build
COPY . .
RUN bunx turbo run build --filter=agent

# Runtime image was copying the FULL monorepo node_modules from `build`
# (dev + prod deps for app/api/agent/packages/* all at once, ~4.5GB) --
# reinstall production-only deps here instead. Discovered in prod
# (29/08/2026): this image alone, plus apps/api's identical pattern,
# repeatedly exhausted vigiep1's disk (72GB) across rebuild cycles.
# microsandbox/just-bash moved from devDependencies to dependencies in
# apps/agent/package.json first -- eve's sandbox backend needs at least
# one of them importable at runtime even when docker.sock (the preferred
# backend here) is mounted (cf. Task 5's carried-forward minor finding).
FROM build AS prod-deps
# --ignore-scripts : sans ca, la reinstallation redeclenche le postinstall
# de @crm/db ("prisma generate"), qui echoue (exit 127, prisma est une
# devDependency, exclue par --production). Sans objet ici : le Prisma
# Client a deja ete genere pendant `build` (packages/db/src/generated/),
# et ce find ne touche qu'aux node_modules, pas a ce dossier.
RUN find . -maxdepth 4 -type d -name node_modules -prune -exec rm -rf {} + \
&& bun install --production --frozen-lockfile --ignore-scripts

FROM oven/bun:1.3.12-slim AS runtime
WORKDIR /repo
ENV NODE_ENV=production
COPY --from=prod-deps /repo /repo
EXPOSE 2000
# Not `bun apps/agent/scripts/start.ts` from /repo: start.ts spawns the `eve`
# CLI via child_process.spawn, which resolves against $PATH, and the plain
# system PATH in this image does not include node_modules/.bin (only "bun
# run" from inside the package augments PATH with its local
# node_modules/.bin -- confirmed empirically: the direct form failed with
# `Executable not found in $PATH: "eve"`, even though
# apps/agent/node_modules/.bin/eve exists on disk). `cd`-ing into the
# package and using `bun run start` mirrors apps/app/Dockerfile's runtime
# CMD pattern and gets the PATH augmentation start.ts's spawn() relies on.
CMD ["sh", "-c", "cd apps/agent && bun run start"]
18 changes: 18 additions & 0 deletions apps/agent/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# apps/agent/Dockerfile.dockerignore
#
# Named to match Docker's Dockerfile-specific ignore-file convention
# (<path-to-dockerfile-relative-to-context>.dockerignore, resolved at the
# build context root) — same mechanism verified empirically in Task 3
# (apps/api/Dockerfile.dockerignore): a plain apps/agent/.dockerignore is NOT
# picked up because this build's context is the repo root
# (`docker build -f apps/agent/Dockerfile ... .`), not apps/agent/.
node_modules
**/node_modules
.turbo
**/.turbo
dist
**/dist
.eve
**/.eve
.git
*.log
10 changes: 3 additions & 7 deletions apps/agent/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import "@crm/env/load";

import { DEFAULT_AGENT_MODEL } from "@crm/db/settings";
import { onTelemetryProblem, syncVersion } from "@crm/telemetry";
import { defineAgent, defineDynamic } from "eve";
import { defineAgent } from "eve";
import { logCapabilities } from "./lib/capabilities";
import { selectedModel } from "./lib/model";
import { deepseekModel } from "./lib/model";

void logCapabilities();

Expand All @@ -13,10 +12,7 @@ onTelemetryProblem((message) => console.debug(`[telemetry] ${message}`));
void syncVersion();

export default defineAgent({
model: defineDynamic({
fallback: DEFAULT_AGENT_MODEL.id,
events: { "session.started": () => selectedModel() },
}),
model: deepseekModel(),
limits: {
maxInputTokensPerSession: 500_000,
maxOutputTokensPerSession: 50_000,
Expand Down
43 changes: 41 additions & 2 deletions apps/agent/agent/lib/model.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,52 @@
import { db } from "@crm/db";
import { readAgentModel } from "@crm/db/settings";
import { createOpenAI } from "@ai-sdk/openai";
import type { LanguageModelV4 } from "@ai-sdk/provider";

const deepseek = createOpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: process.env.DEEPSEEK_API_KEY,
// Overrides the `openai` provider id @ai-sdk/openai stamps onto the
// model by default -- documented for exactly this "3rd party provider
// behind the OpenAI-compatible API" case.
name: "deepseek",
});

/**
* DeepSeek instead of Vercel AI Gateway (VigieProcure fork adaptation, cf.
* scripts/SPEC-fork-trycompai-crm.md Task 5) -- avoids a Vercel account
* dependency for the main agent's LLM calls. `deepseek-v4-flash` is
* VigieProcure's pinned DeepSeek model id, matching what its other AI
* agents use (cf. .claude/secrets/deepseek.env in the vigieprocure repo).
*
* Uses `.chat(...)` explicitly, NOT calling the provider directly
* (`deepseek(modelId)`) -- @ai-sdk/openai 4.x defaults the callable-provider
* shorthand to the Responses API (`provider: "openai.responses"`, POSTs to
* `/responses`), which DeepSeek's OpenAI-compatible endpoint does not
* implement. `.chat(...)` targets `/chat/completions`, which DeepSeek does
* support. Found empirically: the shorthand form compiled fine and only
* failed the unit test's `provider` assertion, not at the type level.
*/
export function deepseekModel(): LanguageModelV4 {
return deepseek.chat("deepseek-v4-flash");
}

export interface ModelSelection {
model: string;
modelContextWindowTokens: number;
}

/**
* `db` and `readAgentModel` are imported lazily here (not at module scope)
* so that this module can be imported -- e.g. for `deepseekModel()`, in
* model.test.ts -- without eagerly initializing the Prisma client, which
* throws at import time if DATABASE_URL/TEST_DATABASE_URL isn't set (see
* packages/db/src/client.ts). The try/catch below already treats any
* failure here as "no configured model", so this changes nothing about
* this function's observable behavior.
*/
export async function selectedModel(): Promise<ModelSelection | null> {
try {
const { db } = await import("@crm/db");
const { readAgentModel } = await import("@crm/db/settings");
const setting = await readAgentModel(db);

if (setting.isDefault) return null;
Expand Down
12 changes: 7 additions & 5 deletions apps/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@
"clean": "rm -rf .turbo .eve node_modules"
},
"dependencies": {
"@ai-sdk/openai": "^4.0.51",
"@crm/db": "workspace:*",
"@crm/env": "workspace:*",
"@crm/telemetry": "workspace:*",
"@crm/typescript-config": "workspace:*",
"@crm/validation": "workspace:*",
"context.dev": "2.10.0",
"eve": "^0.29.4",
"just-bash": "^3.2.0",
"microsandbox": "^0.6.8",
"typescript": "^5.9.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@crm/typescript-config": "workspace:*",
"@types/node": "^24.0.0",
"just-bash": "^3.2.0",
"microsandbox": "^0.6.8",
"typescript": "^5.9.2"
"@ai-sdk/provider": "^4.0.8",
"@types/node": "^24.0.0"
}
}
12 changes: 12 additions & 0 deletions apps/agent/test/model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, it } from "bun:test";
import { deepseekModel } from "../agent/lib/model";

describe("deepseekModel", () => {
it("returns a language model targeting DeepSeek's OpenAI-compatible endpoint", () => {
const model = deepseekModel();
expect(model.provider).toContain("deepseek");
expect(model.modelId).toBe("deepseek-v4-flash");
expect(model.doGenerate).toBeFunction();
expect(model.doStream).toBeFunction();
});
});
40 changes: 40 additions & 0 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# apps/api/Dockerfile
FROM oven/bun:1.3.12 AS deps
WORKDIR /repo
COPY package.json bun.lock turbo.json ./
COPY apps/api/package.json apps/api/package.json
COPY apps/api/scripts/ apps/api/scripts/
COPY apps/app/package.json apps/app/package.json
COPY apps/agent/package.json apps/agent/package.json
COPY packages/ packages/
# @crm/db's postinstall runs `prisma generate`, and prisma.config.ts resolves
# DATABASE_URL eagerly (env(...) throws if unset) even though `generate` never
# opens a connection. A build-time placeholder satisfies that check; the real
# value is supplied at `docker run` time and overrides this.
ENV DATABASE_URL="postgresql://user:password@localhost:5432/db?schema=public"
RUN bun install --frozen-lockfile

FROM deps AS build
COPY . .
RUN bunx turbo run build --filter=api

# Runtime image was copying the FULL monorepo node_modules from `build`
# (dev + prod deps for app/agent/api/packages/* all at once, ~3.6GB) --
# reinstall production-only deps here instead. Discovered in prod
# (29/08/2026): this image alone, plus apps/agent's identical pattern,
# repeatedly exhausted vigiep1's disk (72GB) across rebuild cycles.
FROM build AS prod-deps
# --ignore-scripts : sans ca, la reinstallation redeclenche le postinstall
# de @crm/db ("prisma generate"), qui echoue (exit 127, prisma est une
# devDependency, exclue par --production). Sans objet ici : le Prisma
# Client a deja ete genere pendant `build` (packages/db/src/generated/),
# et ce find ne touche qu'aux node_modules, pas a ce dossier.
RUN find . -maxdepth 4 -type d -name node_modules -prune -exec rm -rf {} + \
&& bun install --production --frozen-lockfile --ignore-scripts

FROM oven/bun:1.3.12-slim AS runtime
WORKDIR /repo
ENV NODE_ENV=production
COPY --from=prod-deps /repo /repo
EXPOSE 3001
CMD ["bun", "apps/api/dist/main.js"]
Loading