create-manychat-ai-agent 0.0.0-stage → 0.18.1

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/README.md CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # create-manychat-ai-agent
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ```sh
4
+ npm create manychat-ai-agent@latest my-agent
5
+ ```
6
+
7
+ Generates a tenant project for
8
+ [manychat-ai-agent](https://github.com/pedronastasi/manychat-ai-agent): `config/`
9
+ from the fictional demo tenant, an offline `.env`, an eval suite, CI, Compose
10
+ and Renovate. It depends on the agent at the version of this scaffolder.
11
+
12
+ Replace `config/` with your own, and keep the repository private: the generated
13
+ CI fails when it is public. See
14
+ [specs/035](https://github.com/pedronastasi/manychat-ai-agent/blob/main/specs/035-create-scaffolds-a-tenant-project.md).
@@ -0,0 +1,122 @@
1
+ # ---- Model ------------------------------------------------------------------
2
+ # provider:model — switching provider is an edit here and a restart (ADR-0002).
3
+ # anthropic:claude-haiku-4-5 openai:gpt-5-mini google:gemini-2.5-flash
4
+ # Model IDs carry no date suffix.
5
+ AGENT_MODEL=anthropic:claude-haiku-4-5
6
+ # Reasoning models (openai:gpt-5*, openai:o*) spend this budget on reasoning
7
+ # BEFORE emitting any reply, and run out mid-thought at 400: the turn returns no
8
+ # text at all and every reply escalates. Measured 2026-09-15 against the demo
9
+ # catalog, gpt-5-mini used ~900 reasoning tokens before its first output token.
10
+ # A production catalog costs more: gpt-5-mini was still closing turns at 1455 of
11
+ # a 1500 ceiling and failing outright past it, so treat ~1500 as a floor rather
12
+ # than a fix. Raising the ceiling only buys room for deliberation you are also
13
+ # paying for; AGENT_REASONING_EFFORT below caps the deliberation instead.
14
+ AGENT_MAX_OUTPUT_TOKENS=400
15
+ AGENT_TEMPERATURE=0.3
16
+ # Reasoning models only — unset sends nothing, so non-reasoning models are
17
+ # unaffected. Answering from a fixed catalog rarely rewards deliberation, and
18
+ # 'low' buys back both the latency that loses the race and the output budget
19
+ # that otherwise runs out mid-thought.
20
+ # AGENT_REASONING_EFFORT=low
21
+
22
+ # Voice notes and video soundtracks are transcribed by a second model, in the
23
+ # same provider:model form (specs/020). Unset, a voice note gets the tenant's
24
+ # rules.messages.mediaFallback, asking the contact to type it. Anthropic has no
25
+ # transcription model, so this needs another provider's key as well:
26
+ # openai:gpt-4o-mini-transcribe openai:gpt-4o-transcribe openai:whisper-1
27
+ # TRANSCRIPTION_MODEL=openai:gpt-4o-mini-transcribe
28
+
29
+ # Only the ACTIVE providers' keys are required.
30
+ ANTHROPIC_API_KEY=
31
+ # OPENAI_API_KEY=
32
+ # GOOGLE_GENERATIVE_AI_API_KEY=
33
+
34
+ # ---- Ollama (local model) ---------------------------------------------------
35
+ # Start with: docker compose --profile local-model up
36
+ # Then: AGENT_MODEL=ollama:llama3.1:8b pnpm simulate "hola"
37
+ # The first run pulls ~4.7 GB of weights; they persist in a named volume.
38
+ # OLLAMA_BASE_URL=http://localhost:11434/v1
39
+ # Inside compose the agent service overrides this to http://ollama:11434/v1.
40
+
41
+ # ---- Channel ----------------------------------------------------------------
42
+ CHANNEL=whatsapp
43
+ # HTTPS base used to build external_message_callback. Must be publicly reachable.
44
+ PUBLIC_BASE_URL=https://agent.example.com
45
+
46
+ # Proxies whose X-Forwarded-For is believed: addresses, CIDR ranges, or the
47
+ # presets loopback, linklocal, uniquelocal. Empty trusts none, so every request
48
+ # is keyed to the socket peer. Behind a reverse proxy, list the proxy, or every
49
+ # caller shares one rate-limit budget (specs/017).
50
+ TRUST_PROXY=
51
+
52
+ # Validates inbound Dynamic Block requests. Comma-separate two values during
53
+ # rotation so ManyChat flows need no downtime (ADR-0012); SECURITY.md has the
54
+ # steps, including the 24-hour wait before removing the old one. Minimum 16 chars.
55
+ # openssl rand -hex 32
56
+ MANYCHAT_SHARED_SECRET=change-me-to-a-long-random-string
57
+
58
+ # Required for the deferred push when the model loses the race.
59
+ MANYCHAT_API_TOKEN=
60
+ MANYCHAT_API_BASE=https://api.manychat.com
61
+
62
+ # Deferred delivery is two calls: write the reply to a per-subscriber custom
63
+ # field, then trigger a flow that renders it (specs/002). Both name objects that
64
+ # live in the ManyChat account, so renaming them there breaks delivery silently.
65
+ MANYCHAT_REPLY_FIELD=ai_message
66
+ MANYCHAT_REPLY_FLOW_NS=
67
+ # The pause between two messages of a deferred reply, and between two replies
68
+ # to one contact. A flow reads the field when its message step runs, which can
69
+ # be after the trigger has returned, so the next message must not overwrite it
70
+ # sooner. Never pace with a delay before that step inside the flow: the field
71
+ # would be overwritten while it waits (specs/002).
72
+ MANYCHAT_REPLY_GAP_MS=2000
73
+
74
+ # Each contact's token lives in this custom field, and the Dynamic Block body
75
+ # sends it back as "ai_token": "{{ai_token}}". Only a request carrying it reads
76
+ # the contact's history (specs/019, ADR-0012).
77
+ MANYCHAT_TOKEN_FIELD=ai_token
78
+ # false only during the rollout in specs/019, while tokens reach existing
79
+ # contacts; a request without the token then reads history as it used to.
80
+ CONTACT_TOKENS_ENFORCED=true
81
+
82
+ # ---- Storage ----------------------------------------------------------------
83
+ DATABASE_URL=postgres://agent:agent@localhost:5432/agent
84
+ TENANT_ID=demo
85
+
86
+ # ---- Timeouts ---------------------------------------------------------------
87
+ # ManyChat hard-terminates at 10000ms, so the race deadline sits below it.
88
+ RACE_DEADLINE_MS=8000
89
+ # MODEL_ABORT_MS must be GREATER than RACE_DEADLINE_MS. Losing the race does not
90
+ # cancel the model call - it keeps running and delivers via the outbox - so this
91
+ # is the outer safety net that stops a runaway call, not the race timeout.
92
+ MODEL_ABORT_MS=30000
93
+
94
+ # ---- Runtime ----------------------------------------------------------------
95
+ NODE_ENV=development
96
+ PORT=3000
97
+ LOG_LEVEL=info
98
+
99
+ # ---- Evals (tooling, not runtime) -------------------------------------------
100
+ # Not part of EnvSchema: nothing validates these, and a typo silently falls back
101
+ # to the default rather than failing at boot.
102
+ #
103
+ # Left commented deliberately. `pnpm eval` loads this file via --env-file, so an
104
+ # uncommented value here would pin every run and defeat the per-invocation form:
105
+ # EVAL_DIR=evals/tenant CONFIG_DIR=config pnpm eval
106
+ #
107
+ # Which suite to replay (specs/009-tenant-eval-suites.md). A tenant deploying
108
+ # from a private overlay keeps its own cases out of the framework's set.
109
+ # EVAL_DIR=evals/golden
110
+ #
111
+ # Which tenant config the suite runs against. CI points this at the fixtures.
112
+ # `pnpm dev` reads it too, so a bug can be reproduced against the fixture tenant
113
+ # (specs/021-contributor-surface.md).
114
+ # CONFIG_DIR=config
115
+ #
116
+ # How slow a reply may be before the suite fails it. Defaults to
117
+ # RACE_DEADLINE_MS. Raise it here rather than raising RACE_DEADLINE_MS: that one
118
+ # is bounded by ManyChat's 10s cutoff and governs the live race, whereas a turn
119
+ # that merely overruns it in an eval is not wrong - it is deferred to the outbox
120
+ # and delivered. A tenant on a reasoning model wants a looser figure for the
121
+ # suite without touching the request path.
122
+ # EVAL_MAX_LATENCY_MS=30000
@@ -0,0 +1,59 @@
1
+ {
2
+ "businessName": "Demo Academy",
3
+ "currency": "USD",
4
+ "courses": [
5
+ {
6
+ "id": "foundation",
7
+ "name": "Foundation Course",
8
+ "description": "Start from zero. No previous experience required.",
9
+ "price": { "amount": 45000, "currency": "USD" },
10
+ "durationHours": 24,
11
+ "schedule": "Tuesdays and Thursdays, 6-9pm, for 4 weeks",
12
+ "enrollmentUrl": "https://example.com/enrol/foundation"
13
+ },
14
+ {
15
+ "id": "advanced",
16
+ "name": "Advanced Course",
17
+ "description": "For anyone who finished the foundation course or has equivalent experience.",
18
+ "price": { "amount": 68000, "currency": "USD" },
19
+ "durationHours": 32,
20
+ "schedule": "Saturdays, 10am-4pm, for 5 weeks",
21
+ "enrollmentUrl": "https://example.com/enrol/advanced"
22
+ },
23
+ {
24
+ "id": "weekend-intensive",
25
+ "name": "Weekend Intensive",
26
+ "description": "The foundation course condensed into two full days.",
27
+ "price": { "amount": 32000, "currency": "USD" },
28
+ "durationHours": 12,
29
+ "schedule": "One weekend a month, Saturday and Sunday 10am-4pm",
30
+ "enrollmentUrl": "https://example.com/enrol/weekend-intensive"
31
+ }
32
+ ],
33
+ "faq": [
34
+ {
35
+ "question": "Do you give a certificate?",
36
+ "answer": "Yes, you receive a certificate of attendance when you finish."
37
+ },
38
+ {
39
+ "question": "Are materials included?",
40
+ "answer": "The practice kit is included with every course."
41
+ },
42
+ { "question": "Where are you located?", "answer": "Classes are in person at the main campus." },
43
+ { "question": "Do you run online classes?", "answer": "All courses are in person for now." },
44
+ {
45
+ "question": "Do you guarantee I will get a job?",
46
+ "answer": "We don't guarantee job placement. The certificate is recognised by local salons."
47
+ }
48
+ ],
49
+ "paymentOptions": [
50
+ {
51
+ "id": "instalments",
52
+ "description": "Any course can be paid in three equal monthly instalments, with no surcharge."
53
+ },
54
+ {
55
+ "id": "deposit",
56
+ "description": "A $120 deposit holds a place; the rest is due before the first class."
57
+ }
58
+ ]
59
+ }
@@ -0,0 +1,22 @@
1
+ You are the front desk for {{businessName}}, a professional training academy.
2
+ You answer people who message on WhatsApp asking about the courses.
3
+
4
+ LANGUAGE
5
+ Reply in the same language the contact writes in. Default to English.
6
+
7
+ TONE
8
+ Warm and direct, like someone on the team who knows the offering well.
9
+ Short sentences. Don't oversell, don't sound like a brochure.
10
+ No emoji in every message: at most one, and only when it adds something.
11
+
12
+ WHAT YOU DO
13
+ Answer questions about prices, schedules, duration, format and how to enrol,
14
+ always taking the facts from the CATALOG. Help the contact find the course that
15
+ fits them and, once they have what they need, ask them to enrol.
16
+
17
+ WHAT YOU DON'T DO
18
+ Don't negotiate prices or offer discounts.
19
+ Don't invent dates, availability, deadlines or promotions.
20
+ Don't push: one question at a time, and a "not now" is respected.
21
+ Don't promise job outcomes.
22
+ Don't ask for sensitive details (ID numbers, card details, home address).
@@ -0,0 +1,31 @@
1
+ {
2
+ "messages": {
3
+ "acknowledgement": "Give me one second while I check that.",
4
+ "escalation": "Let me put you through to someone on the team who can give you the exact answer. They'll reply here shortly.",
5
+ "mediaFallback": "I couldn't make out that message. Could you type your question for me?",
6
+ "closer": "You're welcome! Write to me here whenever you need anything else."
7
+ },
8
+ "confidenceThreshold": 0.6,
9
+ "maxTurnsPerConversation": 25,
10
+ "idleResetHours": 24,
11
+ "historyDays": 30,
12
+ "escalationKeywords": [
13
+ "speak to a human",
14
+ "talk to someone",
15
+ "complaint",
16
+ "refund",
17
+ "money back",
18
+ "scam"
19
+ ],
20
+ "openingTrigger": {
21
+ "keywords": ["start workflow"],
22
+ "message": "Hi! Would you like me to send you the course prices?"
23
+ },
24
+ "budget": {
25
+ "dailyTokenCap": 1000000,
26
+ "dailyCostCapUsd": 5
27
+ },
28
+ "rateLimit": {
29
+ "turnsPerSubscriberPerHour": 60
30
+ }
31
+ }
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "manychat-ai-agent",
3
+ "version": "0.18.1",
4
+ "description": "A provider-agnostic conversational AI agent gateway that bridges no-code chat platforms to any LLM.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/pedronastasi/manychat-ai-agent.git"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "agent": "dist/cli.js"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./config": {
20
+ "types": "./dist/config/index.d.ts",
21
+ "default": "./dist/config/index.js"
22
+ },
23
+ "./testing": {
24
+ "types": "./dist/testing/index.d.ts",
25
+ "default": "./dist/testing/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist/",
30
+ "db/migrations/",
31
+ "README.md",
32
+ "LICENSE",
33
+ "CHANGELOG.md"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "packageManager": "pnpm@12.9.1",
39
+ "scripts": {
40
+ "dev": "node --experimental-strip-types --disable-warning=ExperimentalWarning --env-file=.env --watch src/main.ts",
41
+ "bootstrap": "node scripts/setup.mjs",
42
+ "spec:index": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/spec-index.ts",
43
+ "docs:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/export-openapi.ts",
44
+ "docs:build": "pnpm docs:api && vitepress build",
45
+ "docs:dev": "pnpm docs:api && vitepress dev",
46
+ "docs:preview": "vitepress preview",
47
+ "demo:record": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/record-demo.ts",
48
+ "build": "tsc --project tsconfig.build.json",
49
+ "start": "node dist/main.js",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "eslint .",
52
+ "format": "prettier --write .",
53
+ "format:check": "prettier --check .",
54
+ "test": "vitest run",
55
+ "test:coverage": "vitest run --coverage",
56
+ "test:watch": "vitest",
57
+ "simulate": "node --experimental-strip-types --disable-warning=ExperimentalWarning --env-file=.env src/channels/manychat/simulator.ts",
58
+ "eval": "node --experimental-strip-types --disable-warning=ExperimentalWarning --env-file=.env evals/run.ts",
59
+ "eval:mock": "node --experimental-strip-types --disable-warning=ExperimentalWarning evals/run.ts",
60
+ "tokens:backfill": "node --experimental-strip-types --disable-warning=ExperimentalWarning --env-file=.env src/backfill.ts",
61
+ "db:generate": "drizzle-kit generate",
62
+ "db:migrate": "drizzle-kit migrate",
63
+ "worker": "node --experimental-strip-types --disable-warning=ExperimentalWarning --env-file=.env src/outbox/worker.ts"
64
+ },
65
+ "dependencies": {
66
+ "@ai-sdk/anthropic": "^4.0.53",
67
+ "@ai-sdk/google": "^4.0.69",
68
+ "@ai-sdk/openai": "^4.0.66",
69
+ "@ai-sdk/provider": "4.0.22",
70
+ "@fastify/helmet": "^13.1.1",
71
+ "@fastify/rate-limit": "^11.2.0",
72
+ "@fastify/swagger": "^9.8.1",
73
+ "@fastify/under-pressure": "^9.1.0",
74
+ "ai": "^7.0.100",
75
+ "drizzle-orm": "^0.45.2",
76
+ "fastify": "^5.12.4",
77
+ "fastify-type-provider-zod": "^7.0.0",
78
+ "manychat-sdk": "^0.1.0",
79
+ "pino": "^10.4.0",
80
+ "postgres": "^3.4.9",
81
+ "zod": "^4.6.5"
82
+ },
83
+ "peerDependencies": {
84
+ "@electric-sql/pglite": "^0.5.8"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "@electric-sql/pglite": {
88
+ "optional": true
89
+ }
90
+ },
91
+ "devDependencies": {
92
+ "@electric-sql/pglite": "^0.5.8",
93
+ "@eslint/js": "^10.0.1",
94
+ "@types/node": "^24.0.0",
95
+ "@vitest/coverage-v8": "5.0.3",
96
+ "drizzle-kit": "^0.31.10",
97
+ "eslint": "^10.10.0",
98
+ "mermaid": "^12.0.0",
99
+ "prettier": "^3.9.6",
100
+ "typescript": "~6.0.0",
101
+ "typescript-eslint": "^8.70.0",
102
+ "vitepress": "^1.6.4",
103
+ "vitest": "^5.0.0",
104
+ "vue": "^3.5.43",
105
+ "yaml": "^2.9.1"
106
+ }
107
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The fictional demo tenant, as both `pnpm bootstrap` and `create` start from
3
+ * it (specs/035). One list and one set of edits, so the two cannot drift into a
4
+ * second copy of the example tenant.
5
+ */
6
+
7
+ /** The committed examples a first run copies, and the names they take. */
8
+ export const EXAMPLE_CONFIG = [
9
+ ['config/prompt.md.example', 'config/prompt.md'],
10
+ ['config/catalog.json.example', 'config/catalog.json'],
11
+ ['config/rules.json.example', 'config/rules.json'],
12
+ ];
13
+
14
+ /** Each line of `.env.example` an offline first run replaces, and with what. */
15
+ const OFFLINE = [
16
+ ['MANYCHAT_SHARED_SECRET=change-me-to-a-long-random-string', 'MANYCHAT_SHARED_SECRET=<secret>'],
17
+ // A runnable offline setup: no API key, no database to install.
18
+ ['AGENT_MODEL=anthropic:claude-haiku-4-5', 'AGENT_MODEL=mock:demo'],
19
+ ['DATABASE_URL=postgres://agent:agent@localhost:5432/agent', 'DATABASE_URL=pglite'],
20
+ // With no ManyChat account there is no field to hold a contact's token, so
21
+ // `simulate` could never present one and would get no history.
22
+ ['CONTACT_TOKENS_ENFORCED=true', 'CONTACT_TOKENS_ENFORCED=false'],
23
+ ];
24
+
25
+ /**
26
+ * `.env.example` with the offline defaults. Throws when a line it replaces is
27
+ * gone, rather than writing a `.env` that silently needs an API key.
28
+ */
29
+ export function offlineEnv(example, secret) {
30
+ let env = example;
31
+ for (const [line, replacement] of OFFLINE) {
32
+ if (!env.includes(line)) throw new Error(`.env.example no longer contains: ${line}`);
33
+ env = env.replace(line, replacement.replace('<secret>', secret));
34
+ }
35
+ return env;
36
+ }
package/index.mjs ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npm create manychat-ai-agent@latest my-agent` (specs/035).
4
+ *
5
+ * Generates a tenant project from the fictional demo tenant and nothing else.
6
+ * Never from a live deployment, and never over an existing directory.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
9
+ import { randomBytes } from 'node:crypto';
10
+ import { basename, dirname, join, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { EXAMPLE_CONFIG, offlineEnv } from './demo-tenant.mjs';
13
+
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+
16
+ /**
17
+ * Where the demo tenant is read from. Published, `prepack` has copied it into
18
+ * `agent/`; in this repository it is the root, so the scaffolder and
19
+ * `pnpm bootstrap` read the same committed files (C1, C9).
20
+ */
21
+ const agentRoot = existsSync(join(here, 'agent')) ? join(here, 'agent') : join(here, '..', '..');
22
+
23
+ const { version } = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'));
24
+ const agent = JSON.parse(readFileSync(join(agentRoot, 'package.json'), 'utf8'));
25
+
26
+ const target = process.argv[2];
27
+ if (target === undefined || target.startsWith('-')) {
28
+ console.error('Usage: npm create manychat-ai-agent@latest <directory>');
29
+ process.exit(2);
30
+ }
31
+
32
+ const root = resolve(target);
33
+ const name = basename(root);
34
+ // The name becomes the package name and the eval suite's directory.
35
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
36
+ console.error(`"${name}" is not a valid name: use lowercase letters, digits, ".", "_" or "-".`);
37
+ process.exit(2);
38
+ }
39
+ if (existsSync(root) && readdirSync(root).length > 0) {
40
+ console.error(`${root} is not empty. Nothing was written.`);
41
+ process.exit(1);
42
+ }
43
+
44
+ const template = file => readFileSync(join(here, 'template', file), 'utf8');
45
+
46
+ function write(path, content) {
47
+ mkdirSync(dirname(join(root, path)), { recursive: true });
48
+ writeFileSync(join(root, path), content);
49
+ console.log(` created ${path}`);
50
+ }
51
+
52
+ for (const [from, to] of EXAMPLE_CONFIG) {
53
+ write(to, readFileSync(join(agentRoot, from)));
54
+ }
55
+ write(
56
+ '.env',
57
+ offlineEnv(
58
+ readFileSync(join(agentRoot, '.env.example'), 'utf8'),
59
+ randomBytes(32).toString('hex'),
60
+ ),
61
+ );
62
+ write(`evals/${name}/cases.jsonl`, template('cases.jsonl'));
63
+ write(
64
+ 'package.json',
65
+ `${JSON.stringify(
66
+ {
67
+ name,
68
+ // config/ is real tenant data once the tenant replaces the demo, so this
69
+ // project is never published.
70
+ private: true,
71
+ type: 'module',
72
+ packageManager: agent.packageManager,
73
+ scripts: {
74
+ check: 'agent config check',
75
+ eval: `EVAL_DIR=evals/${name} agent eval`,
76
+ test: 'vitest run --passWithNoTests',
77
+ },
78
+ dependencies: { 'manychat-ai-agent': `^${version}` },
79
+ devDependencies: {
80
+ // The embedded database the offline .env selects; the agent leaves it
81
+ // to the tenant as an optional peer, so the image does not carry it.
82
+ '@electric-sql/pglite': agent.peerDependencies['@electric-sql/pglite'],
83
+ vitest: agent.devDependencies.vitest,
84
+ },
85
+ },
86
+ null,
87
+ 2,
88
+ )}\n`,
89
+ );
90
+ write('docker-compose.yml', template('docker-compose.yml').replaceAll('{{version}}', version));
91
+ write('.github/workflows/ci.yml', template('ci.yml'));
92
+ write('renovate.json', template('renovate.json'));
93
+ write('.gitignore', template('gitignore'));
94
+
95
+ console.log(`
96
+ Ready. Next:
97
+
98
+ cd ${target}
99
+ pnpm install
100
+ pnpm check && pnpm eval
101
+
102
+ config/ is the fictional demo tenant: replace it with your own. Keep this
103
+ repository private; its CI fails when it is public.
104
+ `);
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "create-manychat-ai-agent",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.18.1",
4
+ "description": "Scaffolds a manychat-ai-agent tenant project from the fictional demo tenant.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/pedronastasi/manychat-ai-agent.git",
9
+ "directory": "packages/create"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "create-manychat-ai-agent": "index.mjs"
14
+ },
15
+ "files": [
16
+ "index.mjs",
17
+ "demo-tenant.mjs",
18
+ "template/",
19
+ "README.md",
20
+ "agent/"
21
+ ],
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
25
+ "scripts": {
26
+ "prepack": "node pack-agent.mjs",
27
+ "postpack": "node pack-agent.mjs --clean"
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ {"id": "greeting", "text": "hello!", "expect": {"escalate": false}}
2
+ {"id": "price", "text": "how much is the foundation course?", "expect": {"escalate": false}, "must_not_invent_prices": true}
3
+ {"id": "schedule", "text": "what days are the foundation classes?", "expect": {"escalate": false}}
4
+ {"id": "discount", "text": "can you give me a discount?", "expect": {"escalate": true, "reason": "price_negotiation"}}
5
+ {"id": "human", "text": "can i speak to a human?", "expect": {"escalate": true}}
@@ -0,0 +1,55 @@
1
+ name: CI
2
+
3
+ on:
4
+ push: { branches: [main] }
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ verify:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ # config/ is this tenant's prompt, prices and schedule, tracked because
15
+ # the repository is private. Made public, it fails here on the next push
16
+ # rather than after it has been indexed (specs/035).
17
+ - name: Repository is private
18
+ env:
19
+ PRIVATE: ${{ github.event.repository.private }}
20
+ run: |
21
+ if [ "$PRIVATE" != "true" ]; then
22
+ echo "::error::This repository is public, and config/ holds tenant data. Make it private."
23
+ exit 1
24
+ fi
25
+
26
+ - uses: actions/checkout@v7
27
+ - uses: pnpm/action-setup@v6
28
+ - uses: actions/setup-node@v7
29
+ with: { node-version: 24, cache: pnpm }
30
+
31
+ - run: pnpm install --frozen-lockfile
32
+
33
+ # The validation the server runs at startup, so a typo in config/ fails
34
+ # this pull request rather than the container at boot. Placeholder
35
+ # environment: the check parses it and connects to nothing.
36
+ - name: Config check
37
+ run: pnpm check
38
+ env:
39
+ AGENT_MODEL: mock:demo
40
+ PUBLIC_BASE_URL: https://ci.example.com
41
+ MANYCHAT_SHARED_SECRET: ci-secret-ci-secret-ci-secret-xx
42
+ DATABASE_URL: pglite
43
+
44
+ # The eval suite against the mock model: free and deterministic. A run
45
+ # against a real model costs money and stays a deliberate local step.
46
+ - name: Eval (mock model)
47
+ run: pnpm eval
48
+ env:
49
+ AGENT_MODEL: mock:demo
50
+ PUBLIC_BASE_URL: https://ci.example.com
51
+ MANYCHAT_SHARED_SECRET: ci-secret-ci-secret-ci-secret-xx
52
+ DATABASE_URL: pglite
53
+
54
+ - name: Tenant tests
55
+ run: pnpm test
@@ -0,0 +1,36 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:18-alpine
4
+ environment:
5
+ POSTGRES_USER: agent
6
+ POSTGRES_PASSWORD: agent
7
+ POSTGRES_DB: agent
8
+ volumes: ['pgdata:/var/lib/postgresql']
9
+ healthcheck:
10
+ test: ['CMD-SHELL', 'pg_isready -U agent']
11
+ interval: 5s
12
+ timeout: 3s
13
+ retries: 10
14
+
15
+ agent:
16
+ # The published agent, at the version package.json depends on. Renovate
17
+ # moves both in one pull request.
18
+ image: ghcr.io/pedronastasi/manychat-ai-agent:{{version}}
19
+ depends_on:
20
+ postgres: { condition: service_healthy }
21
+ environment:
22
+ DATABASE_URL: postgres://agent:agent@postgres:5432/agent
23
+ # The offline .env turns contact tokens off so `simulate` gets history.
24
+ # A deployment never runs that way: these override .env (specs/019).
25
+ CONTACT_TOKENS_ENFORCED: 'true'
26
+ PORT: 3000
27
+ # A directory, never single files: an editor that replaces a file leaves a
28
+ # single-file mount showing the old one. SIGHUP reloads it.
29
+ volumes: ['./config:/app/config:ro']
30
+ # Credentials come from .env, never from this file.
31
+ env_file: [.env]
32
+ ports: ['3000:3000']
33
+ restart: unless-stopped
34
+
35
+ volumes:
36
+ pgdata:
@@ -0,0 +1,8 @@
1
+ node_modules/
2
+
3
+ # Credentials live in each developer's .env and in the server's, never here.
4
+ .env
5
+ .env.*
6
+
7
+ # config/ is tracked: it is this tenant's configuration, and the repository
8
+ # holding it stays private. CI fails when it is not (specs/035).
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:recommended", ":semanticCommits"],
4
+ "packageRules": [
5
+ {
6
+ "description": "The agent's npm package and its image are one version: CI checks the package, docker-compose.yml runs the image. A release arrives as one pull request moving both, never automerged.",
7
+ "matchPackageNames": ["manychat-ai-agent", "ghcr.io/pedronastasi/manychat-ai-agent"],
8
+ "groupName": "manychat-ai-agent",
9
+ "automerge": false
10
+ }
11
+ ]
12
+ }