@cloudgrid-io/mcp 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/corpus/capability-map.md +44 -15
  3. package/src/corpus/install.md +27 -0
  4. package/src/corpus/templates/semantic-search/AGENTS.md +216 -0
  5. package/src/corpus/templates/semantic-search/README.md +75 -0
  6. package/src/corpus/templates/semantic-search/cloudgrid.yaml +219 -0
  7. package/src/corpus/templates/semantic-search/index.md +108 -0
  8. package/src/corpus/templates/semantic-search/services/backend/requirements.txt +7 -0
  9. package/src/corpus/templates/semantic-search/services/backend/src/app/__init__.py +0 -0
  10. package/src/corpus/templates/semantic-search/services/backend/src/app/answer.py +90 -0
  11. package/src/corpus/templates/semantic-search/services/backend/src/app/auth.py +65 -0
  12. package/src/corpus/templates/semantic-search/services/backend/src/app/config.py +110 -0
  13. package/src/corpus/templates/semantic-search/services/backend/src/app/db.py +89 -0
  14. package/src/corpus/templates/semantic-search/services/backend/src/app/embeddings.py +44 -0
  15. package/src/corpus/templates/semantic-search/services/backend/src/app/indexing.py +223 -0
  16. package/src/corpus/templates/semantic-search/services/backend/src/app/main.py +259 -0
  17. package/src/corpus/templates/semantic-search/services/backend/src/app/search.py +222 -0
  18. package/src/corpus/templates/semantic-search/services/backend/src/app/source.py +259 -0
  19. package/src/corpus/templates/semantic-search/services/backend/src/main.py +12 -0
  20. package/src/corpus/templates/semantic-search/services/refresh/README.md +42 -0
  21. package/src/corpus/templates/semantic-search/services/refresh/requirements.txt +9 -0
  22. package/src/corpus/templates/semantic-search/services/refresh/src/app/__init__.py +0 -0
  23. package/src/corpus/templates/semantic-search/services/refresh/src/app/config.py +110 -0
  24. package/src/corpus/templates/semantic-search/services/refresh/src/app/db.py +89 -0
  25. package/src/corpus/templates/semantic-search/services/refresh/src/app/embeddings.py +44 -0
  26. package/src/corpus/templates/semantic-search/services/refresh/src/app/indexing.py +223 -0
  27. package/src/corpus/templates/semantic-search/services/refresh/src/app/source.py +259 -0
  28. package/src/corpus/templates/semantic-search/services/refresh/src/main.py +45 -0
  29. package/src/corpus/templates/semantic-search/services/web/index.html +12 -0
  30. package/src/corpus/templates/semantic-search/services/web/package-lock.json +1680 -0
  31. package/src/corpus/templates/semantic-search/services/web/package.json +19 -0
  32. package/src/corpus/templates/semantic-search/services/web/src/App.jsx +55 -0
  33. package/src/corpus/templates/semantic-search/services/web/src/api.js +125 -0
  34. package/src/corpus/templates/semantic-search/services/web/src/components/Dashboard.jsx +61 -0
  35. package/src/corpus/templates/semantic-search/services/web/src/components/ManagerLogin.jsx +39 -0
  36. package/src/corpus/templates/semantic-search/services/web/src/components/SearchView.jsx +166 -0
  37. package/src/corpus/templates/semantic-search/services/web/src/components/Settings.jsx +148 -0
  38. package/src/corpus/templates/semantic-search/services/web/src/main.jsx +10 -0
  39. package/src/corpus/templates/semantic-search/services/web/src/styles.css +78 -0
  40. package/src/corpus/templates/semantic-search/services/web/vite.config.js +10 -0
  41. package/src/corpus/workflows/semantic-search.md +142 -0
  42. package/src/tools.js +76 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@ cloudgrid.yaml schema (every field, the `needs:` injection table, the
13
13
  requires-vs-needs caveat, validation rules), fetch the companion reference:
14
14
  `gridctl_fetch("doc", "cloudgrid-yaml")`.
15
15
 
16
- ## The 58 templates
16
+ ## The 59 templates
17
17
 
18
18
  | Intent (match on `when:`) | Template | `needs:` | Deploy | Edition |
19
19
  |---|---|---|---|---|
@@ -57,6 +57,7 @@ requires-vs-needs caveat, validation rules), fetch the companion reference:
57
57
  | revenue dashboard, sales dashboard, MRR/revenue view | `revenue-dashboard` | `database: true` | runtime (async, poll) | local |
58
58
  | property listings, real estate site, rentals/homes listings | `property-listings` | `database: true` | runtime (async, poll) | local |
59
59
  | project management, projects and tasks tracker, team project board | `project-management` | `database: true` | runtime (async, poll) | local |
60
+ | search over my documents / PDFs / notes / knowledge base, semantic search, document search, find across my files, searchable archive | `semantic-search` | `database: true` (NO `vector` #1545; active daily refresh cron) | runtime (async, poll) | local |
60
61
 
61
62
  **Rule of thumb:** if the app must SAVE/remember data, share state across
62
63
  users/sessions, log in, or store submissions → it is persistent → runtime, local
@@ -73,14 +74,23 @@ differ only by domain schema + UI. Match the request to the closest one by its
73
74
  domain, use `app-with-data` (the minimal reference). Deploy path is identical for
74
75
  all of them.
75
76
 
76
- ## Held / pending platform (not yet buildable)
77
+ **Document search (`semantic-search`).** For "search over my own documents / PDFs
78
+ / notes / knowledge base" with keyword **and** meaning, `semantic-search` ships
79
+ real runnable code in a different shape from the Next.js family: a React (Vite)
80
+ **static** frontend at `/` plus a **Python FastAPI** backend at `/backend`, on the
81
+ same grid-shared Mongo (`needs: { database: true }`). Hybrid search = Mongo
82
+ `$text` + in-app NumPy cosine over embedding arrays + metadata filters. It stores
83
+ embeddings in the Mongo `chunks` collection (NO `needs: vector` — pgvector is
84
+ blocked on **#1545**) and refreshes via a manager endpoint (NO active cron — a
85
+ Python `type: cron` is blocked on **#1585**). Read its `AGENTS.md` for the source
86
+ adapter (dropbox/local/url) + embeddings wiring and the health-without-secrets /
87
+ startup-index patterns.
77
88
 
78
- Two more archetypes are designed but blocked on platform work — the LLM should
79
- know they are coming but must NOT author them yet:
89
+ ## Held / pending platform
80
90
 
81
91
  | Intent | Would need | Status |
82
92
  |---|---|---|
83
- | scheduled task, cron job, "run every day/hour", periodic worker | a `type: cron` service (`schedule`, `timezone`) | **HELD** — platform issue #1543 (cron entities crash the online-check). Do not build. |
93
+ | scheduled task, cron job, "run every day/hour", periodic worker | a `type: cron` service (`schedule`, `timezone`) | **SUPPORTED** on CLI 0.14.0 — Python and Node `type: cron` services validate, deploy, and fire (platform #1585 fixed). See the semantic-search `refresh` cron for a working Python-job example. |
84
94
  | RAG chatbot, "answer over my own docs", semantic search, retrieval-augmented Q&A | `ai-app` + `needs: { vector: pgvector }` | **HELD** — platform issue #1545 (pgvector crashes the deploy). Ship the plain `ai-app` (no vector) for now. |
85
95
 
86
96
  ## Blueprints (structure + cloudgrid.yaml; adapt the app — some pending platform support)
@@ -108,17 +118,17 @@ runtime, async build, **local edition only**. Auth/payment secrets map through a
108
118
  | billing dashboard, invoicing and payments dashboard, SaaS billing | `billing-dashboard` | `database: true` (+ Stripe via `vault:`) | runtime (async, poll) | local |
109
119
  | internal tools portal, admin tools hub, back-office portal | `internal-tools-portal` | `database: true` (+ auth via `vault:`) | runtime (async, poll) | local |
110
120
  | approval workflow, request approvals, multi-step approval system | `approval-workflow` | `database: true` (+ auth via `vault:`) | runtime (async, poll) | local |
111
- | booking system, reservations, appointment booking, scheduling | `booking-system` | `database: true` (+ Stripe/auth via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
112
- | calendar scheduler, team calendar, scheduling app, meeting scheduler | `calendar-scheduler` | `database: true` (+ optional auth/Stripe via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
113
- | appointment booking, clinic/salon booking, appointment scheduler | `appointment-booking` | `database: true` (+ auth/Stripe via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
114
- | restaurant website with reservations, table booking, restaurant site | `restaurant-reservations` | `database: true` (+ Stripe/email via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
115
- | travel booking portal, trip booking, flights/hotels booking, travel reservations | `travel-booking` | `database: true` (+ Stripe/auth via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
121
+ | booking system, reservations, appointment booking, scheduling | `booking-system` | `database: true` (+ Stripe/auth via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
122
+ | calendar scheduler, team calendar, scheduling app, meeting scheduler | `calendar-scheduler` | `database: true` (+ optional auth/Stripe via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
123
+ | appointment booking, clinic/salon booking, appointment scheduler | `appointment-booking` | `database: true` (+ auth/Stripe via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
124
+ | restaurant website with reservations, table booking, restaurant site | `restaurant-reservations` | `database: true` (+ Stripe/email via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
125
+ | travel booking portal, trip booking, flights/hotels booking, travel reservations | `travel-booking` | `database: true` (+ Stripe/auth via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
116
126
  | RAG, ask my docs, knowledge base chatbot, retrieval-augmented search over documents | `ai-knowledge-base` | `ai: true, database: true`; ideal `vector: pgvector` **pending #1545** (store embeddings in Mongo + cosine-rank until it lands) | runtime (async, poll) | local |
117
127
 
118
128
  The booking family (`booking-system`, `calendar-scheduler`, `appointment-booking`,
119
- `restaurant-reservations`, `travel-booking`) is fully buildable today **minus**
120
- the reminder cron — a `type: cron` service is HELD on platform issue **#1543**, so
121
- ship without it. `ai-knowledge-base` builds today on the AI Gateway + Mongo; its
129
+ `restaurant-reservations`, `travel-booking`) is fully buildable today, **including**
130
+ the reminder cron — Python and Node `type: cron` services work on CLI 0.14.0
131
+ (platform #1585 fixed). `ai-knowledge-base` builds today on the AI Gateway + Mongo; its
122
132
  ideal `needs: { vector: pgvector }` embedding store is HELD on **#1545**, so store
123
133
  chunks in Mongo and cosine-rank in-app until it lands.
124
134
 
@@ -160,8 +170,27 @@ engine hint. Cron is NOT a need — it is a **service type** (`type: cron` with
160
170
 
161
171
  1. Read the request; match it against the workflow `when:` triggers above.
162
172
  2. Adopt that template's `needs:`. Persistence → `database` (`app-with-data` or
163
- `api-service`); talks to an LLM → `ai: true` + `database` (`ai-app`). (Held for
164
- now: scheduled → a `cron` service, #1543; RAG → `vector: pgvector`, #1545.)
173
+ `api-service`); talks to an LLM → `ai: true` + `database` (`ai-app`); scheduled
174
+ work → a `type: cron` service (Python/Node, supported on 0.14.0). (Held for now:
175
+ RAG's ideal `vector: pgvector`, #1545 — store embeddings in Mongo until it lands.)
165
176
  3. Static (`needs: none`) → publish as an inspiration with `gridctl_drop`
166
177
  (instant, any edition). Anything with a `needs:` → runtime, local edition,
167
178
  `gridctl_plug` a linked folder, then poll to a live URL.
179
+
180
+ ## FAQ
181
+
182
+ **"What database does CloudGrid support?"**
183
+ > All of them — use CloudGrid's managed database out of the box, or bring your own
184
+ > by giving me the connection keys (Postgres, MySQL, MongoDB, Supabase, Neon, and
185
+ > more). Which would you like?
186
+
187
+ Under the hood: **managed** is `needs: { database: true }` (grid provisions Mongo,
188
+ injects `DATABASE_MONGODB_URL`); **bring-your-own** is
189
+ `needs: { database: { tier: external, secret: MY_DB } }` plus
190
+ `grid secrets set MY_DB=<connection-string>` (the connection string lives in env
191
+ secrets, never committed).
192
+
193
+ **"Can I run something on a schedule / every day?"**
194
+ > Yes — add a `type: cron` service (`schedule` + `timezone`). Python and Node cron
195
+ > jobs are supported on CLI 0.14.0. See the semantic-search `refresh` cron for a
196
+ > working Python-job example.
@@ -80,6 +80,33 @@ Or the MCP server, in `~/.cursor/mcp.json`:
80
80
 
81
81
  Remote variant, nothing installed: `{ "mcpServers": { "cloudgrid": { "url": "https://mcp.cloudgrid.io/mcp" } } }`
82
82
 
83
+ ### Gemini CLI
84
+
85
+ Add the MCP server with the built-in command:
86
+
87
+ ```
88
+ gemini mcp add cloudgrid npx -y @cloudgrid-io/mcp
89
+ ```
90
+
91
+ Or edit `~/.gemini/settings.json` (user) or `.gemini/settings.json` (project) directly:
92
+
93
+ ```json
94
+ { "mcpServers": { "cloudgrid": { "command": "npx", "args": ["-y", "@cloudgrid-io/mcp"] } } }
95
+ ```
96
+
97
+ ### Antigravity
98
+
99
+ Google's Antigravity IDE/CLI shares one MCP config at `~/.gemini/config/mcp_config.json`
100
+ (open it from the IDE: Settings, Customizations, Open MCP Config / Manage MCP Servers,
101
+ View raw config). Add the local stdio server:
102
+
103
+ ```json
104
+ { "mcpServers": { "cloudgrid": { "command": "npx", "args": ["-y", "@cloudgrid-io/mcp"] } } }
105
+ ```
106
+
107
+ Antigravity uses `serverUrl` (not `url`) for remote HTTP servers, so the remote variant is
108
+ `{ "mcpServers": { "cloudgrid": { "serverUrl": "https://mcp.cloudgrid.io/mcp" } } }`.
109
+
83
110
  ### Manual
84
111
 
85
112
  ```
@@ -0,0 +1,216 @@
1
+ # AGENTS.md — semantic-search template (CloudGrid wiring guide)
2
+
3
+ This template is a **runnable app**, not a blueprint — it ships real FastAPI +
4
+ React code. Read this guide, adapt the naming/metadata to the user's domain, wire
5
+ the secrets, and deploy. The four "baked-in learnings" (health-without-secrets,
6
+ startup index, `$text` guard, no `needs: vector`) are already in the code — do not
7
+ regress them.
8
+
9
+ ---
10
+
11
+ ## 1. File tree
12
+
13
+ Every service's code lives under `services/<name>/` — `path:` in `cloudgrid.yaml`
14
+ is the URL mount, NOT the filesystem path. Files at the repo root fail with
15
+ `Error: Service directory not found: …/services/<name>`.
16
+
17
+ ```
18
+ cloudgrid.yaml # name + services(web,backend,refresh) + needs:{database:true}
19
+ services/web/ # React + Vite static frontend, served at /
20
+ package.json vite.config.js index.html
21
+ src/main.jsx App.jsx api.js styles.css
22
+ src/components/SearchView.jsx ManagerLogin.jsx Settings.jsx Dashboard.jsx
23
+ services/backend/ # FastAPI, mounted at /backend
24
+ requirements.txt
25
+ src/main.py # uvicorn app.main:app (reads PORT)
26
+ src/app/
27
+ main.py config.py db.py search.py indexing.py
28
+ source.py embeddings.py answer.py auth.py
29
+ services/refresh/ # type: cron, run: job — scheduled reindex (§8)
30
+ requirements.txt README.md
31
+ src/main.py # calls app.indexing.run_sync(triggered_by="cron")
32
+ src/app/ # VENDORED copies of the backend modules run_sync needs:
33
+ config.py db.py source.py embeddings.py indexing.py __init__.py
34
+ ```
35
+
36
+ ---
37
+
38
+ ## 2. Mongo collections + fields
39
+
40
+ The grid provisions one MongoDB database (`needs: { database: true }`). Two
41
+ primary collections (`_id` is the Mongo ObjectId on every doc):
42
+
43
+ **`documents`** — one row per source document
44
+ - `title` (string), `collection` (string, free-form grouping tag, nullable),
45
+ `date` (ISO date string, nullable) — the metadata filters.
46
+ - `source_filename` (string, unique), `source_rev` (string — content hash / rev
47
+ for incremental sync), `file_path` (string, source-internal, never exposed).
48
+ - `has_media` (bool), `word_count` (int), `indexed_at` (Date).
49
+
50
+ **`chunks`** — one embeddable passage per document (the retrieval unit)
51
+ - `document_id` (ObjectId → documents), `chunk_index` (int),
52
+ - `content` (string — indexed by the `$text` index, returned as the passage),
53
+ - `embedding` (float[] — the semantic vector; `null` if embeddings unconfigured).
54
+
55
+ Support collections: `settings` (feature flags, synonyms, `catalog.last_refresh`),
56
+ `usage_events`, `index_reports`, `sessions`.
57
+
58
+ **Adapt the metadata to the domain:** rename/extend `collection` and `date` in
59
+ `indexing.py::_parse_metadata`, and mirror the field names in `search.py`,
60
+ `main.py`, and the web `SearchView`.
61
+
62
+ ---
63
+
64
+ ## 3. How CloudGrid injects the datastore
65
+
66
+ You never provision infra or set a connection string by hand. `needs:{database:
67
+ true}` provisions Mongo and injects it as an env var at `grid dev` (local) and at
68
+ runtime (after `grid plug`):
69
+
70
+ | What | Declared in cloudgrid.yaml | Injected env var(s) | Read in code |
71
+ |------|----------------------------|---------------------|--------------|
72
+ | MongoDB | `needs: { database: true }` | `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`) | `app/db.py::get_client` (lazy) |
73
+
74
+ Read it LAZILY — `os.environ.get("DATABASE_MONGODB_URL") or
75
+ os.environ.get("MONGODB_URL")` inside `get_client()`, never at module top level.
76
+ `requires:` is the deprecated v1 alias; never author it and never set `needs:` and
77
+ `requires:` together (the validator rejects the combination).
78
+
79
+ **No `needs: vector`.** The ideal embedding store is pgvector
80
+ (`needs: { vector: pgvector }` → `VECTOR_PGVECTOR_URL`), but the runtime
81
+ role-grant is **blocked on platform issue #1545**. Until it lands, embeddings live
82
+ in the Mongo `chunks.embedding` float array and are cosine-ranked in-app
83
+ (`search.py::_cosine`). When #1545 ships, move the vectors to pgvector and replace
84
+ the in-app cosine block with an `ORDER BY embedding <=> $q LIMIT k` query — the
85
+ rest of the pipeline is unchanged.
86
+
87
+ ---
88
+
89
+ ## 4. Pluggable document source (SOURCE_TYPE)
90
+
91
+ The source is where raw documents live, not the query DB. `SOURCE_TYPE` selects
92
+ the adapter in `app/source.py`; all creds are gated in `app/config.py` and read
93
+ lazily:
94
+
95
+ - **`dropbox`** (default) — read-only, offline refresh-token flow. Secrets:
96
+ `DROPBOX_APP_KEY`, `DROPBOX_APP_SECRET`, `DROPBOX_REFRESH_TOKEN`,
97
+ `DROPBOX_FOLDER_PATH` (optional).
98
+ - **`local`** — a directory on the container filesystem. Set `LOCAL_SOURCE_PATH`
99
+ (or add `needs: { disk: true }` and it defaults to the injected `DISK_PATH`).
100
+ - **`url`** — a manifest of document URLs. Set `URL_SOURCE_MANIFEST` to a
101
+ newline- or comma-separated list.
102
+
103
+ **Add your own adapter:** subclass `Source` (implement `list_files()` returning
104
+ `SourceFile[]` and `download(path) -> bytes`), gate its config in
105
+ `config.py::source_configured`, and return it from `get_source()`. Text
106
+ extraction for txt/md/pdf/docx is shared in `source.extract_text`.
107
+
108
+ ---
109
+
110
+ ## 5. Embeddings (one config point)
111
+
112
+ `app/embeddings.py` is the single provider seam — OpenAI-compatible by default.
113
+ Secrets/config (all gated, read lazily):
114
+
115
+ - `EMBEDDINGS_API_KEY` — the key (semantic search + generated answers degrade
116
+ gracefully to lexical/extractive when unset).
117
+ - `EMBEDDINGS_MODEL` (default `text-embedding-3-small`) — changing it changes
118
+ vector dimensions and requires a full reindex.
119
+ - `EMBEDDINGS_BASE_URL` (default `https://api.openai.com/v1`) — point at any
120
+ OpenAI-compatible endpoint.
121
+ - `ANSWER_MODEL` (default `gpt-4o-mini`) — the grounded-answer chat model.
122
+
123
+ ---
124
+
125
+ ## 6. Secrets via `grid secrets set`
126
+
127
+ Store secrets with the CLI; never commit them, never read them at import time:
128
+
129
+ ```bash
130
+ grid secrets set EMBEDDINGS_API_KEY=sk-...
131
+ grid secrets set SOURCE_TYPE=dropbox
132
+ grid secrets set DROPBOX_APP_KEY=... DROPBOX_APP_SECRET=... DROPBOX_REFRESH_TOKEN=...
133
+ grid secrets set MANAGER_PASSWORD_HASH=$(printf '%s' 'your-password' | shasum -a 256 | cut -d' ' -f1)
134
+ ```
135
+
136
+ `MANAGER_PASSWORD_HASH` is the sha256 hex of the manager password, compared
137
+ server-side in `auth.py`. The in-app "Change password" screen returns a new hash
138
+ to store the same way. Non-secret public config can also go via `grid env`.
139
+
140
+ ---
141
+
142
+ ## 7. The baked-in learnings (do NOT regress)
143
+
144
+ 1. **Startup index.** `main.py` has `@app.on_event("startup")` →
145
+ `db.ensure_indexes()`, which creates the chunks `$text` index (plus the
146
+ documents metadata indexes) at boot. Without it, `$text` raises
147
+ `IndexNotFound` (a 500) on an empty catalog. `ensure_indexes()` is safe to call
148
+ repeatedly and never blocks startup if Mongo is briefly unavailable.
149
+ 2. **`$text` guard.** The lexical query in `search.py` is wrapped in
150
+ `except OperationFailure` so a missing index degrades to "lexical contributes
151
+ nothing", never a 500 — belt-and-suspenders on top of the startup index.
152
+ 3. **Health without secrets.** No secret is read at import time; every secret is
153
+ gated inside a function (`config.py`). `GET /backend/health` needs only Mongo.
154
+ Search/indexing degrade gracefully (empty results + a clear note) when the
155
+ source or embeddings are unconfigured. `GET /backend/status` reports capability
156
+ **booleans** (`source_configured`, `embeddings_configured`, …), never secret
157
+ values.
158
+ 4. **No `needs: vector`.** `needs:` is `database` only — pgvector's runtime
159
+ role-grant is still blocked (#1545). (The scheduled cron refresh IS active — see
160
+ §8.)
161
+
162
+ ---
163
+
164
+ ## 8. Refresh: on-demand endpoint + scheduled cron
165
+
166
+ Two refresh paths, both running the same incremental pipeline
167
+ (`indexing.run_sync`):
168
+
169
+ 1. **On-demand** — the manager-only endpoint `POST /backend/admin/refresh` (wired
170
+ to the "Refresh now" button in Settings). `triggered_by="manager"`.
171
+ 2. **Scheduled** — a `type: cron`, `run: job` Python service (`services/refresh/`)
172
+ that runs daily at `0 3 * * *` UTC and calls
173
+ `app.indexing.run_sync(triggered_by="cron")`. This is ACTIVE — Python `type:
174
+ cron` now validates, deploys, and fires on CLI 0.14.0 (platform #1585 fixed).
175
+
176
+ **How the cron reuses the pipeline (and the drift note).** Each CloudGrid service
177
+ builds in its own isolated container from its own folder, so a cross-folder import
178
+ of `services/backend/src/app` will not resolve. The reuse mechanism is a
179
+ **vendored copy**: the backend modules `run_sync` transitively needs —
180
+ `config.py db.py source.py embeddings.py indexing.py __init__.py` — are copied
181
+ verbatim into `services/refresh/src/app/`. The cron connects directly to the
182
+ grid-injected `DATABASE_MONGODB_URL` (no HTTP hop to the backend, no self-URL
183
+ dependency). If you change the pipeline, chunking, source adapters, embeddings, or
184
+ the schema in the backend, **re-copy the changed module(s)** into
185
+ `services/refresh/src/app/` so the scheduled run matches the endpoint. See
186
+ `services/refresh/README.md`.
187
+
188
+ **Secrets the cron needs:** the same ones as the backend, read lazily — `SOURCE_TYPE`
189
+ + its source creds (`DROPBOX_*` / `LOCAL_SOURCE_PATH` / `URL_SOURCE_MANIFEST`) and
190
+ `EMBEDDINGS_API_KEY` (+ `EMBEDDINGS_MODEL`, `EMBEDDINGS_BASE_URL`). With none set,
191
+ the scheduled run is a clean no-op (`status: skipped`, 0 documents, exit 0), never a
192
+ crash. Its outcome lands in the same `index_reports` collection, visible via the
193
+ manager index-report and in `grid logs` as a `cron refresh: status=… files=…` line.
194
+
195
+ ---
196
+
197
+ ## 9. Deploy + edition note
198
+
199
+ This is a built + deployed multi-service app → the **local edition** (Claude
200
+ Desktop / Claude Code) or the CLI; the hosted edition cannot build a runtime
201
+ container.
202
+
203
+ 1. `grid init semantic-search` — creates the entity + `.cloudgrid/link.json`.
204
+ 2. Put the code under `services/web/` and `services/backend/`; set `cloudgrid.yaml`
205
+ to the active fields (web static+build, backend python, `needs:{database:true}`).
206
+ 3. `grid secrets set …` for embeddings + source + `MANAGER_PASSWORD_HASH` (§6).
207
+ 4. (Optional) `grid dev` to run locally against injected dev Mongo.
208
+ 5. **Build the frontend first:** `(cd services/web && npm install && npm run build)`.
209
+ CloudGrid validates that a `type: static` service carrying a `build:` block
210
+ already has `services/web/dist/index.html` on disk at plug time — plug fails
211
+ validation otherwise.
212
+ 6. `grid plug` — a runtime deploy is **ASYNC**: the first response is
213
+ `status: building`, not a live URL. Poll `grid status` (surface a liveness
214
+ signal, never a bare wait) until Ready.
215
+ 7. Sign in as manager, hit "Refresh now" to index the source, then search.
216
+ Re-plug the same entity to update the same URL.
@@ -0,0 +1,75 @@
1
+ # semantic-search template — CloudGrid-native document search
2
+
3
+ A deployable, domain-agnostic **document search** app: index a folder of
4
+ documents (PDF / DOCX / TXT / MD), then search them by keyword, by meaning, and
5
+ by metadata — with a grounded answer mode and a manager admin. Data lives in the
6
+ grid-shared MongoDB, so the catalog survives refresh and is shared across users.
7
+
8
+ **Hybrid search** = Mongo `$text` (lexical) + in-app NumPy cosine over chunk
9
+ embeddings (semantic) + metadata filters (date / collection), blended and
10
+ aggregated to the document level with a best-passage highlight.
11
+
12
+ ## Services
13
+
14
+ | Service | Type | Path | Entry |
15
+ |---|---|---|---|
16
+ | `web` | `static` (React, Vite) | `/` | build → `dist` |
17
+ | `backend` | `python` (FastAPI) | `/backend` | `services/backend/src/main.py` |
18
+
19
+ `needs: { database: true }` → CloudGrid injects `DATABASE_MONGODB_URL` (fallback
20
+ `MONGODB_URL`). **No `needs: vector`** — pgvector runtime role-grant is blocked
21
+ (platform #1545), so embeddings live in the Mongo `chunks` collection and are
22
+ cosine-ranked in-app. Refresh runs two ways: the manager "Refresh now" endpoint
23
+ (on demand) and an active Python `type: cron` job (`services/refresh/`, daily 03:00
24
+ UTC) — both call the same `indexing.run_sync` (see AGENTS.md §8).
25
+
26
+ ## Pluggable document source
27
+
28
+ `SOURCE_TYPE` selects the adapter, all credentials from env secrets:
29
+
30
+ - `dropbox` (default) — read-only Dropbox via the offline refresh-token flow.
31
+ - `local` — a directory on the container filesystem (`LOCAL_SOURCE_PATH`, or a
32
+ `needs: disk` mount at `DISK_PATH`).
33
+ - `url` — a manifest of document URLs (`URL_SOURCE_MANIFEST`).
34
+
35
+ Add your own by subclassing `Source` in
36
+ `services/backend/src/app/source.py` and returning it from `get_source()`.
37
+
38
+ ## Embeddings
39
+
40
+ Behind one config point (`services/backend/src/app/embeddings.py`), OpenAI by
41
+ default. `EMBEDDINGS_API_KEY` from env; `EMBEDDINGS_MODEL` / `EMBEDDINGS_BASE_URL`
42
+ to point at any OpenAI-compatible provider.
43
+
44
+ ## Secrets (set with `grid secrets set KEY=VALUE`)
45
+
46
+ `EMBEDDINGS_API_KEY`, `SOURCE_TYPE`, and the source's own secrets
47
+ (`DROPBOX_APP_KEY` / `DROPBOX_APP_SECRET` / `DROPBOX_REFRESH_TOKEN` /
48
+ `DROPBOX_FOLDER_PATH`, or `LOCAL_SOURCE_PATH`, or `URL_SOURCE_MANIFEST`), and
49
+ `MANAGER_PASSWORD_HASH` (sha256 hex of the manager password).
50
+
51
+ The app starts and passes `GET /backend/health` with **none** of these set —
52
+ search / indexing degrade to a clear "not indexed / missing secret" state.
53
+
54
+ ## Health without secrets (baked-in)
55
+
56
+ - `@app.on_event("startup")` calls `db.ensure_indexes()` so the chunks `$text`
57
+ index exists at boot — `$text` returns `[]` on an empty catalog, never a 500.
58
+ - The `$text` query is additionally guarded in `except OperationFailure`.
59
+ - No secret is read at import time; every secret is gated inside a function.
60
+ - `/backend/status` reports capability booleans, never secret values.
61
+
62
+ ## Deploy (manager does this)
63
+
64
+ ```bash
65
+ grid init semantic-search # creates the entity + .cloudgrid/link.json
66
+ grid secrets set EMBEDDINGS_API_KEY=sk-... # + source secrets + MANAGER_PASSWORD_HASH
67
+
68
+ # Build the static frontend BEFORE plug — CloudGrid validates that a
69
+ # type:static service with a build: block already has services/web/dist/index.html.
70
+ (cd services/web && npm install && npm run build)
71
+
72
+ grid plug # async build + deploy; poll to a live URL
73
+ ```
74
+
75
+ See `AGENTS.md` for the full wiring guide.
@@ -0,0 +1,219 @@
1
+ # ============================================================================
2
+ # cloudgrid.yaml — Complete Example
3
+ # ============================================================================
4
+ #
5
+ # This file shows EVERY field the platform supports, with comments explaining
6
+ # each one. It is a reference — no real project would use all of these at once.
7
+ #
8
+ # STANDARD: every CloudGrid template ships its cloudgrid.yaml in THIS full form.
9
+ # Uncomment ONLY the fields the archetype needs (always: name + services; plus
10
+ # needs: for runtime apps). Leave everything else commented. Comments are
11
+ # ignored by the parser, so the file deploys to exactly the active fields.
12
+ #
13
+ # Only two fields are required: name and services. Everything else is optional.
14
+ # Full docs: docs/cloudgrid-yaml-reference.md
15
+ # ============================================================================
16
+
17
+
18
+ # ─── IDENTITY ───────────────────────────────────────────────────────────────
19
+
20
+ # Entity slug (REQUIRED). 2-42 chars, lowercase alphanumeric + hyphens.
21
+ # Must start and end with a letter or digit.
22
+ # Becomes part of your public URL: <name>-<4hex>.<org>.cloudgrid.io
23
+ name: semantic-search
24
+
25
+ # One-line summary. Max 280 chars. If omitted, Grid Brain auto-generates one.
26
+ # description: Hybrid semantic + keyword search over your documents
27
+
28
+ # Single emoji or short text shown as the entity icon. Max 64 chars.
29
+ # icon: "🔎"
30
+
31
+ # Entity type: 'app' (default) or 'agent'. An agent is an app with an agent: block.
32
+ # kind: app
33
+
34
+
35
+ # ─── NETWORKING ─────────────────────────────────────────────────────────────
36
+
37
+ # Whether this entity serves public HTTP traffic.
38
+ # true = public URL + Ingress (default). false = internal only.
39
+ # expose: true
40
+
41
+ # Additional domain names this entity serves on (the platform URL is always active).
42
+ # custom_domains:
43
+ # - orders.example.com
44
+ # - shop.example.com
45
+
46
+
47
+ # ─── HEALTH PROBE (entity-level override) ──────────────────────────────────
48
+
49
+ # Override the default health check for the entire entity.
50
+ # probe:
51
+ # path: /healthz # HTTP path to probe (default: /).
52
+ # expected: "<500" # '<500' (default), '200', '2xx'.
53
+
54
+
55
+ # ─── DEPLOY NOTIFICATIONS ──────────────────────────────────────────────────
56
+
57
+ # Slack notification to the org's default channel on deploy failure.
58
+ # true = notify (default), false = silent.
59
+ # notify: true
60
+
61
+
62
+ # ─── SCALING ────────────────────────────────────────────────────────────────
63
+
64
+ # Replica scaling intent.
65
+ # 'auto' — platform decides from the grid tier (default).
66
+ # 'alert' — single-replica, emit a SPOF warning at plug time.
67
+ # 1-10 — explicit replica count, clamped to the grid max.
68
+ # scale: auto
69
+
70
+
71
+ # ─── INFRASTRUCTURE NEEDS ──────────────────────────────────────────────────
72
+ #
73
+ # Declare infrastructure; the platform provisions it and injects connection
74
+ # details as env vars. Nine needs: database, cache, kv, queue, pubsub, vector,
75
+ # object_storage, disk, ai. Simplest form is `true`.
76
+ #
77
+ # needs:
78
+ # # database — MongoDB. Injects DATABASE_MONGODB_URL (+ legacy MONGODB_URL).
79
+ # # true | pool | dedicated | { tier: external, secret: MY_DB }
80
+ # database: true
81
+ # # cache — Redis, LRU eviction (derived data). Injects CACHE_REDIS_URL (+ REDIS_URL).
82
+ # # true | redis | { engine: external, secret: MY_REDIS_URL }
83
+ # cache: true
84
+ # # kv — Redis, no eviction (durable: flags, idempotency). Injects KV_REDIS_URL.
85
+ # kv: true
86
+ # # queue — Redis job-queue substrate (BullMQ). Injects QUEUE_REDIS_URL.
87
+ # queue: true
88
+ # # pubsub — Redis broadcast pub/sub. Injects PUBSUB_REDIS_URL.
89
+ # pubsub: true
90
+ # # vector — pgvector on Postgres. Injects VECTOR_PGVECTOR_URL (+ PGVECTOR_URL).
91
+ # # true | pgvector | { engine: pgvector, dim: 1536 }
92
+ # # NOT USED HERE — pgvector runtime role-grant is blocked (platform #1545).
93
+ # # This template stores embeddings in the Mongo `chunks` collection and
94
+ # # cosine-ranks in-app instead. Swap to `vector: pgvector` once #1545 lands.
95
+ # vector: pgvector
96
+ # # object_storage — GCS bucket. Injects OBJECT_STORAGE_GCS_BUCKET, _GCS_REGION.
97
+ # # true | { size: 10Gi }
98
+ # object_storage: true
99
+ # # disk — persistent FS mount (PVC). Injects DISK_PATH. Pins to 1 replica.
100
+ # # true | { size: 1Gi, mount: /var/data } (forbidden: /proc /sys /dev)
101
+ # # Use with SOURCE_TYPE=local to read documents from the mount.
102
+ # disk: true
103
+ # # ai — AI Gateway via @cloudgrid-io/ai. Injects AI_GATEWAY_URL. true | false.
104
+ # ai: true
105
+
106
+
107
+ # ─── INTER-ENTITY COMMUNICATION ────────────────────────────────────────────
108
+
109
+ # Entities this app calls (outbound). Service-account token injected per pair.
110
+ # calls:
111
+ # - org:atomic/billing-api
112
+
113
+ # Inbound caller policy — who can call YOUR entity.
114
+ # callers:
115
+ # policy: auto # 'auto' | 'manual' (default).
116
+ # scope: org # 'org' (default) | 'space' | 'explicit'.
117
+ # fallback: deny # 'deny' (default) | 'warn'.
118
+ # # allowed_apps: # required only when scope: explicit
119
+ # # - org:atomic/frontend
120
+
121
+
122
+ # ─── VAULT ──────────────────────────────────────────────────────────────────
123
+
124
+ # Map org vault items to env vars. Keys UPPER_SNAKE_CASE; values = vault item keys.
125
+ # The source + embeddings secrets can also be stored directly with
126
+ # `grid secrets set KEY=VALUE` — see AGENTS.md.
127
+ # vault:
128
+ # EMBEDDINGS_API_KEY: embeddings-api-key
129
+ # DROPBOX_REFRESH_TOKEN: dropbox-refresh-token
130
+
131
+
132
+ # ─── CONNECTORS AND HOOKS ──────────────────────────────────────────────────
133
+
134
+ # connectors:
135
+ # - id: team-slack
136
+ # target: org # 'system' | 'org'.
137
+ # channel: "#deploys"
138
+ # hooks:
139
+ # - on: deploy.failed # release.published | deploy.failed | feedback.received | entity.created
140
+ # via: team-slack
141
+ # template: "Deploy failed for {entity_name}: {error}"
142
+ # channel: "#alerts"
143
+ # triggers:
144
+ # - from: team-slack
145
+ # event: message
146
+ # channel: "#support"
147
+ # route: /webhooks/slack
148
+
149
+
150
+ # ─── LOCAL DEVELOPMENT ──────────────────────────────────────────────────────
151
+
152
+ # Config for `grid dev`. Redact fields in dev data snapshots.
153
+ # dev:
154
+ # snapshot:
155
+ # mask:
156
+ # - users.email
157
+ # - users.phone
158
+
159
+
160
+ # ─── AGENT METADATA ────────────────────────────────────────────────────────
161
+
162
+ # An agent is an app with kind: agent (above) and an agent: block.
163
+ # agent:
164
+ # purpose: Sync customer data from external CRM every night
165
+ # schedule: "0 2 * * *"
166
+ # trigger: webhook
167
+
168
+
169
+ # ─── PRIVATE RESOURCE AUTO-DEPENDENCIES ────────────────────────────────────
170
+
171
+ # When true (default), private resources are auto-injected as depends_on.
172
+ # auto_depends_on_private_requires: true
173
+
174
+
175
+ # ─── SERVICES (REQUIRED) ───────────────────────────────────────────────────
176
+ #
177
+ # At least one service is required. Key = service name (slug rules).
178
+ # Types: node, nextjs, python, static, cron.
179
+ #
180
+ services:
181
+ # React (Vite) built to static assets, served at the root.
182
+ web:
183
+ type: static
184
+ path: /
185
+ build:
186
+ command: npm run build
187
+ output: dist
188
+ node_version: "22"
189
+ # Python FastAPI backend — hybrid search, answer mode, manager admin.
190
+ # Entry: services/backend/src/main.py (uvicorn app.main:app).
191
+ backend:
192
+ type: python
193
+ path: /backend
194
+ # Scheduled reindex — Python `type: cron` job. Runs the SAME incremental
195
+ # pipeline as the manager "Refresh now" endpoint (app.indexing.run_sync),
196
+ # connecting directly to the injected DATABASE_MONGODB_URL. `run: job` = batch
197
+ # (no HTTP server, no path/port). Cron has no injected self-URL, so the backend
198
+ # `app/` package is vendored into services/refresh/src/app/ and called locally —
199
+ # see services/refresh/README.md (incl. the drift note). Degrades gracefully:
200
+ # with no source/embeddings secrets it records a report and exits 0 (no-op).
201
+ refresh:
202
+ type: cron
203
+ schedule: "0 3 * * *" # daily at 03:00
204
+ timezone: UTC
205
+ run: job # batch job — runs services/refresh/src/main.py once
206
+ # CloudGrid managed Mongo → injects DATABASE_MONGODB_URL (+ legacy MONGODB_URL).
207
+ # NO `needs: vector` — pgvector runtime role-grant is still blocked (#1545);
208
+ # embeddings live in the Mongo `chunks` collection and are cosine-ranked in-app.
209
+ needs:
210
+ database: true
211
+
212
+
213
+ # ============================================================================
214
+ # NOTES
215
+ # - NOT here: org:/entity_id: (→ .cloudgrid/link.json), secrets (→ grid secrets set),
216
+ # Dockerfile (platform generates from services.*.type).
217
+ # - Deploy: grid plug | Local with grid resources: grid dev
218
+ # - Full reference: docs/cloudgrid-yaml-reference.md | CLI: docs/cli-reference.md
219
+ # ============================================================================