@promptowl/contextnest-community 1.12.0 → 1.14.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.
package/CONFIGURATION.md CHANGED
@@ -73,7 +73,8 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
73
73
  | `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
74
74
  | `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
75
75
  | `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
76
- | `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. |
76
+ | `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. The asset-upload route (`POST /nests/:id/assets`) is exempt up to the video cap below. |
77
+ | `VIDEO_MAX_MB` | `30` | Max size (in MB) of a video uploaded into a doc. Default `30` keeps it under Cloud Run's ~32 MiB HTTP/1 request-body limit, so an oversized video is rejected with a clear message instead of a bare `413` from the platform. Raise only where the deployment can actually accept larger request bodies (not behind Cloud Run, or on HTTP/2 / direct-to-bucket upload). Images are fixed at 10 MB. |
77
78
  | `LOGO_URL` | _(unset)_ | Custom logo shown in the UI header + login screen. Must start with `https://`, `http://`, or `data:image/` — other schemes (`file://`, relative, `javascript:`) are rejected with a warning and the bundled icon is used. |
78
79
  | `PROMPTOWL_TEAMS_ENABLED` | _(unset — off)_ | Lets users who signed in with PromptOwl import their PromptOwl teams as local teams. Off by default; set `true` to enable, or toggle from Settings → Advanced. When off, `GET/POST /teams/promptowl` return `404` and the PromptOwl Teams panel is hidden. Independent of `PROMPTOWL_SIGN_IN_GATE`. |
79
80
  | `TYPE_ARTIFACT_ENABLED` | `true` | Set `false` to disable creation of **artifact** nodes server-wide (existing artifact nodes stay readable — never data loss). Runnable types (agent/skill/tool) are gated by `FEATURE_WORKFLOW_PLANE`, not here. Also editable from Settings (`/admin/settings`). |
@@ -86,6 +87,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
86
87
  | `RUN_MAX_CONCURRENT_ROOTS` | `50` | Max concurrently-running root (depth-0) runs per nest (clamped 1..1000). The subagent caps bound a single tree; this bounds how many trees can run at once, so triggers can't flood a nest. |
87
88
  | `ANTHROPIC_API_KEY` | _(unset)_ | Server-wide default runner key for workflow-plane agent runs. Never returned by the API — the health endpoint reports presence only, and Settings shows a masked tail. |
88
89
  | `SLACK_WEBHOOK_URL` | _(unset — connector off)_ | Slack incoming-webhook URL for governance-event notifications (review submitted/approved/rejected, collaborator added). `https://` only — the URL embeds a secret. Also editable from Settings in the UI. |
90
+ | `MSTEAMS_WEBHOOK_URL` | _(unset — connector off)_ | Microsoft Teams incoming-webhook URL for the same governance events, posted as Adaptive Cards. `https://` only — the URL embeds a secret. Also editable from Settings in the UI. See [Microsoft Teams notifications](#microsoft-teams-notifications). |
89
91
  | `SMTP_URL` | _(unset — connector off)_ | SMTP connection URL for email notifications (`smtp://` or `smtps://`, credentials inline). Requires `NOTIFY_EMAIL_FROM` and `NOTIFY_EMAIL_TO`. Also editable from Settings. |
90
92
  | `NOTIFY_EMAIL_FROM` | _(unset)_ | From address for notification emails. |
91
93
  | `NOTIFY_EMAIL_TO` | _(unset)_ | Comma-separated recipients for notification emails. |
@@ -93,6 +95,61 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
93
95
 
94
96
  ---
95
97
 
98
+ ## Microsoft Teams notifications
99
+
100
+ Set `MSTEAMS_WEBHOOK_URL` (or paste the URL into **Settings → Notifications →
101
+ Microsoft Teams notifications**) and the server posts the same governance
102
+ events the Slack connector covers — review requested / approved / rejected,
103
+ collaborator added, plus per-nest burst digests — to one Teams channel as
104
+ [Adaptive Cards](https://adaptivecards.io/). Leave it empty to turn the
105
+ connector off. Delivery is fire-and-forget with a short timeout: a Teams
106
+ outage or a bad URL never slows down or fails the action that triggered the
107
+ message.
108
+
109
+ > Naming note: the setting is `msteams_webhook_url` / `MSTEAMS_WEBHOOK_URL`
110
+ > because "teams" in this product means user groups (the `/teams` API), not
111
+ > Microsoft Teams.
112
+
113
+ ### Create the incoming webhook in a Teams channel
114
+
115
+ Microsoft retired the classic Office 365 "Incoming Webhook" connectors;
116
+ webhooks are now created with the **Workflows** (Power Automate) app:
117
+
118
+ 1. In Microsoft Teams, open the channel that should receive notifications,
119
+ click **⋯ (More options) → Workflows**.
120
+ 2. Pick the template **"Post to a channel when a webhook request is
121
+ received"** (searching for "webhook" finds it), sign in if prompted, and
122
+ confirm the team + channel.
123
+ 3. Create the flow and **copy the HTTP POST URL** it shows (a
124
+ `https://….logic.azure.com/…` or `https://….powerplatform.com/…` address).
125
+ 4. Paste that URL into **Settings → Notifications → Microsoft Teams
126
+ notifications** (or set `MSTEAMS_WEBHOOK_URL`).
127
+ 5. Save, then click **Send test** on the card to post a test message and
128
+ confirm the channel receives it. (The button tests the *saved* URL — save
129
+ first, then test. The Slack card has the same button.)
130
+
131
+ If your tenant still has a legacy Office 365 connector webhook, it is expected
132
+ to accept the same payload envelope (legacy connectors documented support for
133
+ Adaptive Card attachments, though we have not verified one end-to-end — the
134
+ card's plain-text `fallbackText` is the safety net) — but create new hooks via
135
+ Workflows.
136
+
137
+ The URL embeds a secret — anyone holding it can post to the channel, so treat
138
+ it like a password (it is why the setting requires `https://`). The server
139
+ sends the standard envelope Workflows expects:
140
+ `{"type": "message", "attachments": [{"contentType":
141
+ "application/vnd.microsoft.card.adaptive", "content": {…card…}}]}` with a
142
+ plain-text `fallbackText` for clients that cannot render cards.
143
+
144
+ Per-nest connector rows post the same Adaptive Card format to a nest-specific
145
+ webhook, with per-event filtering: `POST /nests/:id/connectors` with a JSON
146
+ body like `{"channel": "teams", "url": "https://…", "events":
147
+ ["review_requested", "review_rejected"]}` (`events` may also be `["*"]`; the
148
+ `url` may be an `env:KEY` reference into the nest's env store). Manage rows
149
+ with `GET`/`PATCH`/`DELETE` on the same path.
150
+
151
+ ---
152
+
96
153
  ## Single sign-on (OIDC)
97
154
 
98
155
  Generic OpenID Connect sign-in against any spec-compliant identity provider —
@@ -233,7 +290,7 @@ Postgres backend starts empty.
233
290
  ### Runtime settings persistence
234
291
 
235
292
  Settings you change at runtime — everything on the **Settings page** (`/admin/settings`:
236
- sign-in gate, logo, base URL, upload limit, feature flags, OIDC SSO, Slack/SMTP connectors)
293
+ sign-in gate, logo, base URL, upload limit, feature flags, OIDC SSO, Slack/Teams/SMTP connectors)
237
294
  plus the **installed license key** — are stored in the database (`server_settings`
238
295
  table), **not** in a `.env` file. This is deliberate: on Cloud Run the container
239
296
  filesystem is ephemeral, so a file-based value was wiped by every rebuild, and each
package/README.md CHANGED
@@ -12,6 +12,7 @@ ContextNest Community Edition is a self-hosted server that lets you:
12
12
 
13
13
  - Store, version, and govern markdown-based context documents ("nests")
14
14
  - Import an existing folder or vault of markdown files in one step
15
+ - Export a nest as a portable bundle and re-import it on another self-hosted host
15
16
  - Apply stewardship workflows — draft, pending review, approved
16
17
  - Share nests with collaborators or publish them read-only to the public
17
18
  - Serve approved context to AI agents via MCP, HTTP, or CLI
@@ -93,6 +94,7 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
93
94
  | Configurable nest storage (external / GCS volume mount) | ✅ | ✅ |
94
95
  | Markdown + YAML frontmatter vaults | ✅ | ✅ |
95
96
  | Import existing folder / vault | ✅ | ✅ |
97
+ | Export nest as portable bundle (.zip) | ✅ | ✅ |
96
98
  | Markdown rendering + wiki cross-linking | ✅ | ✅ |
97
99
  | External-edit detection + version diff | ✅ | ✅ |
98
100
  | Stewardship workflow (draft/review/approve) | ✅ | ✅ |
@@ -101,7 +103,7 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
101
103
  | Custom logo / branding | ✅ | ✅ |
102
104
  | Admin password reset + user removal (in-platform) | ✅ | ✅ |
103
105
  | Wiki backlinks, outline, hover-preview, link health | ✅ | ✅ |
104
- | Rich editor — tables, callouts, toggles, code highlight, find/replace | ✅ | ✅ |
106
+ | Rich editor — tables, callouts, toggles, code highlight, find/replace, image & video upload | ✅ | ✅ |
105
107
  | Steward version revert | ✅ | ✅ |
106
108
  | MCP server for AI agents | ✅ | ✅ |
107
109
  | Centralized multi-tenant admin console | — | ✅ |
@@ -12,6 +12,7 @@ import { join, dirname } from "path";
12
12
  import { existsSync } from "fs";
13
13
  import { fileURLToPath } from "url";
14
14
  import dotenv from "dotenv";
15
+ import { z } from "zod";
15
16
  var __filename = fileURLToPath(import.meta.url);
16
17
  var __dirname = dirname(__filename);
17
18
  var envCandidates = [
@@ -26,11 +27,13 @@ if (envFileLoaded && !isTestRun) {
26
27
  var canonicalEnvFile = process.env.ENV_FILE_PATH || join(dataRoot(), ".env");
27
28
  var canonicalEnvLoaded = null;
28
29
  var slackUrlWarned = false;
30
+ var msteamsUrlWarned = false;
29
31
  var emailFromWarned = false;
30
32
  var emailToWarned = false;
31
33
  var oidcIssuerWarned = false;
34
+ var EMAIL_SCHEMA = z.email();
32
35
  function isEmailish(v) {
33
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) && !/[\r\n]/.test(v);
36
+ return EMAIL_SCHEMA.safeParse(v.trim()).success && !/[\r\n]/.test(v);
34
37
  }
35
38
  function isEmailListish(v) {
36
39
  return v.split(",").every((e) => isEmailish(e.trim()));
@@ -353,6 +356,27 @@ var config = {
353
356
  }
354
357
  return raw;
355
358
  },
359
+ /**
360
+ * Optional Microsoft Teams incoming-webhook URL (Workflows app) for team
361
+ * notifications — same events as SLACK_WEBHOOK_URL, posted as Adaptive
362
+ * Cards. Empty/unset = connector off. https only — a webhook carries an
363
+ * implicit secret in its path, so it never travels plaintext. Named
364
+ * MSTEAMS_* deliberately: "teams" in this codebase means user groups.
365
+ */
366
+ get MSTEAMS_WEBHOOK_URL() {
367
+ const raw = process.env.MSTEAMS_WEBHOOK_URL?.trim();
368
+ if (!raw) return null;
369
+ if (!/^https:\/\//i.test(raw)) {
370
+ if (!msteamsUrlWarned) {
371
+ msteamsUrlWarned = true;
372
+ console.warn(
373
+ "[config] MSTEAMS_WEBHOOK_URL rejected: must be an https:// URL. Microsoft Teams notifications disabled."
374
+ );
375
+ }
376
+ return null;
377
+ }
378
+ return raw;
379
+ },
356
380
  get FEATURE_WORKFLOW_PLANE() {
357
381
  return process.env.FEATURE_WORKFLOW_PLANE === "true";
358
382
  },
@@ -494,6 +518,17 @@ var config = {
494
518
  */
495
519
  get MAX_BODY_BYTES() {
496
520
  return parseInt(process.env.MAX_BODY_BYTES || String(10 * 1024 * 1024), 10);
521
+ },
522
+ /**
523
+ * Max video upload size in bytes. Default 30 MB — kept under Cloud Run's
524
+ * 32 MiB HTTP/1 request-body limit so a too-big video is rejected cleanly by
525
+ * our own check (clear message) instead of a bare 413 from the platform.
526
+ * Raise via VIDEO_MAX_MB only when the deployment can actually accept it
527
+ * (e.g. not behind Cloud Run, or on HTTP/2 / direct-to-bucket upload).
528
+ */
529
+ get VIDEO_MAX_BYTES() {
530
+ const mb = parseInt(process.env.VIDEO_MAX_MB || "30", 10);
531
+ return (Number.isFinite(mb) && mb > 0 ? mb : 30) * 1024 * 1024;
497
532
  }
498
533
  };
499
534
 
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GUNJTORH.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-3M7677XW.js";
6
+ } from "./chunk-I5KYGMIT.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-XQ46F76G.js";
5
5
  import {
6
6
  getDb
7
- } from "./chunk-3M7677XW.js";
7
+ } from "./chunk-I5KYGMIT.js";
8
8
 
9
9
  // src/governance/version-service.ts
10
10
  import { createHash } from "crypto";
@@ -41,6 +41,38 @@ async function createVersion(params) {
41
41
  status: params.status
42
42
  };
43
43
  }
44
+ async function upsertVersion(params) {
45
+ const db = getDb();
46
+ const contentHash = hashContent(params.content);
47
+ const sql = insertOrReplace(
48
+ db,
49
+ `INSERT INTO node_versions (nest_id, node_id, version, content_hash, author, status, change_note, tags_json)
50
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
51
+ ["nest_id", "node_id", "version"],
52
+ // Reset created_at on conflict too: SQLite's INSERT OR REPLACE deletes+
53
+ // reinserts (created_at → now), so mirror that on Postgres — otherwise a
54
+ // draft edited in place would keep its original (import-time) "edited at".
55
+ `content_hash = excluded.content_hash, author = excluded.author, status = excluded.status, change_note = excluded.change_note, tags_json = excluded.tags_json, created_at = ${nowExpr(db)}`
56
+ );
57
+ await db.run(sql, [
58
+ params.nestId,
59
+ params.nodeId,
60
+ params.version,
61
+ contentHash,
62
+ params.author,
63
+ params.status,
64
+ params.changeNote || null,
65
+ params.tags ? JSON.stringify(params.tags) : null
66
+ ]);
67
+ return {
68
+ version: params.version,
69
+ content: params.content,
70
+ editedBy: params.author,
71
+ editedAt: (/* @__PURE__ */ new Date()).toISOString(),
72
+ changeNote: params.changeNote,
73
+ status: params.status
74
+ };
75
+ }
44
76
  async function getVersions(nestId, nodeId) {
45
77
  const db = getDb();
46
78
  const rows = await db.all(
@@ -172,6 +204,7 @@ export {
172
204
  SYSTEM_AUTHOR_PREFIX,
173
205
  systemAuthor,
174
206
  createVersion,
207
+ upsertVersion,
175
208
  getVersions,
176
209
  getVersion,
177
210
  getCurrentVersion,