create-ailk 0.30.0 → 0.31.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/README.md +71 -3
- package/component-catalog.json +524 -8
- package/component-catalog.md +8 -5
- package/dist/cli.js +99 -0
- package/dist/cutover.d.ts +109 -0
- package/dist/cutover.js +494 -0
- package/dist/design-presets.js +20 -4
- package/dist/export-content.d.ts +55 -0
- package/dist/export-content.js +303 -0
- package/dist/inventory.d.ts +216 -0
- package/dist/inventory.js +833 -0
- package/dist/language-import.d.ts +114 -0
- package/dist/language-import.js +435 -0
- package/dist/lib/env-catalog.js +1 -1
- package/dist/lib/import-mdx.d.ts +38 -0
- package/dist/lib/import-mdx.js +411 -64
- package/dist/lib/render-comparison.d.ts +200 -0
- package/dist/lib/render-comparison.js +374 -0
- package/dist/migrate-content.d.ts +6 -2
- package/dist/migrate-content.js +60 -14
- package/dist/seed-workspace.d.ts +3 -1
- package/dist/seed-workspace.js +45 -16
- package/dist/topology.d.ts +12 -0
- package/dist/topology.js +24 -10
- package/package.json +3 -3
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/content-source-guard.sh +75 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.github/workflows/ci.yml +2 -0
- package/templates/CLAUDE.md +5 -0
- package/templates/apps/mcp/CLAUDE.md +8 -4
- package/templates/apps/web/.env.example +4 -2
- package/templates/apps/web/CLAUDE.md +16 -5
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +153 -4
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +45 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +6 -4
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/__tests__/program-event.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +11 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +44 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.thanks-by-tier.test.tsx +306 -0
- package/templates/apps/web/app/[locale]/layout.tsx +122 -27
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +21 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/llms.txt/route.ts +16 -9
- package/templates/apps/web/app/sitemap.xml/route.ts +3 -0
- package/templates/apps/web/jest.config.cjs +46 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +247 -4
- package/templates/apps/web/lib/__tests__/in-page-magnet-self-request.test.ts +92 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +90 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +33 -8
- package/templates/apps/web/lib/__tests__/site-identity-feature-list.test.ts +196 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +11 -1
- package/templates/apps/web/lib/bot-check.ts +113 -13
- package/templates/apps/web/lib/in-page-magnet.ts +4 -1
- package/templates/apps/web/lib/page-chrome.ts +65 -5
- package/templates/apps/web/lib/relay.ts +22 -3
- package/templates/apps/web/lib/site-identity.ts +75 -24
- package/templates/apps/web/messages/en.json +5 -1
- package/templates/apps/web/next.config.mjs +15 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/apps/web/scripts/assert-content-bundled.ts +36 -8
- package/templates/apps/web/scripts/content-snapshot.ts +33 -0
- package/templates/package.json +2 -1
- package/templates/tools/check-content-source.ts +108 -0
- package/versions.json +48 -48
package/dist/migrate-content.js
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
* [--workspace <id>]` — import an existing site's MDX pages into its
|
|
4
4
|
* workspace (#10207).
|
|
5
5
|
*
|
|
6
|
-
* The same import as `seed-workspace` — `importMdx` over `<dir>/content
|
|
6
|
+
* The same import as `seed-workspace` — `importMdx` over `<dir>/content`, and
|
|
7
|
+
* the docs-blog tree `<dir>/apps/web/content` when it exists (#10701), with
|
|
7
8
|
* the site's own installed adapter — for the site `seed-workspace` refuses: one
|
|
8
9
|
* with no `.ailk-site-manifest.json`, or a spoke, whose workspace lives on its
|
|
9
10
|
* hub (D1). No manifest and no topology is read; the operator names the API
|
|
10
11
|
* origin and the workspace, which is all the topology would decide.
|
|
11
12
|
*
|
|
12
13
|
* A dry run is the default (D3): the importer runs over `readOnlyFetch`, which
|
|
13
|
-
* passes each `GET` through and answers a `POST` or `
|
|
14
|
-
* sending it, so the plan — create · update · unchanged · skipped · failed — is
|
|
14
|
+
* passes each `GET` through and answers a `POST`, `PATCH` or `PUT` locally
|
|
15
|
+
* without sending it, so the plan — create · update · unchanged · skipped · failed — is
|
|
15
16
|
* decided by the importer itself against the live workspace, and no write can
|
|
16
17
|
* leave the process. `--apply` writes; a second `--apply` reports every page
|
|
17
18
|
* `unchanged`.
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
*/
|
|
28
29
|
import { join, resolve } from "node:path";
|
|
29
30
|
import { parseArgs } from "node:util";
|
|
30
|
-
import {
|
|
31
|
+
import { languageLines, languageOffLine, withLanguage, } from "./language-import.js";
|
|
32
|
+
import { formatImportMdxReport, importMdx, isDirectory, loadSiteMdxAdapter, outcomePath, } from "./lib/import-mdx.js";
|
|
31
33
|
import { ParseError } from "./parse-args.js";
|
|
32
34
|
import { AGENT_KEY_ENV_VAR, WORKSPACE_ID_ENV_VAR } from "./seed-workspace.js";
|
|
33
35
|
import { HUB_ORIGIN_ENV_VAR, TopologyError, normalizeHubUrl, } from "./topology.js";
|
|
@@ -53,17 +55,20 @@ function given(value) {
|
|
|
53
55
|
/**
|
|
54
56
|
* A fetch that sends `GET`s through `inner` and never sends a write: a `POST`
|
|
55
57
|
* is answered `201` and a `PATCH` `200` locally, with the one member of a
|
|
56
|
-
* write's answer `importMdx` reads (`data.locale`)
|
|
58
|
+
* write's answer `importMdx` reads (`data.locale`), and the structure write's
|
|
59
|
+
* `PUT` (#10534) `200` as a layout unchanged (`data.written: false`). Any
|
|
60
|
+
* other method throws.
|
|
57
61
|
*/
|
|
58
62
|
export function readOnlyFetch(inner, locale) {
|
|
59
63
|
return (async (input, init) => {
|
|
60
64
|
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
61
65
|
if (method === "GET")
|
|
62
66
|
return inner(input, init);
|
|
63
|
-
if (method !== "POST" && method !== "PATCH") {
|
|
67
|
+
if (method !== "POST" && method !== "PATCH" && method !== "PUT") {
|
|
64
68
|
throw new Error(`a dry run sends no ${method} request`);
|
|
65
69
|
}
|
|
66
|
-
|
|
70
|
+
const data = method === "PUT" ? { locale, written: false } : { locale };
|
|
71
|
+
return new Response(JSON.stringify({ ok: true, data }), {
|
|
67
72
|
status: method === "POST" ? 201 : 200,
|
|
68
73
|
headers: { "content-type": "application/json" },
|
|
69
74
|
});
|
|
@@ -81,8 +86,8 @@ const PLAN_VERB = {
|
|
|
81
86
|
function planLine(outcome) {
|
|
82
87
|
const verb = PLAN_VERB[outcome.result];
|
|
83
88
|
return "reason" in outcome
|
|
84
|
-
? `${outcome
|
|
85
|
-
: `${outcome
|
|
89
|
+
? `${outcomePath(outcome)}: ${verb} — ${outcome.reason}`
|
|
90
|
+
: `${outcomePath(outcome)}: ${verb}`;
|
|
86
91
|
}
|
|
87
92
|
// ── The command (D1–D4) ─────────────────────────────────────────────────────
|
|
88
93
|
export async function migrateContent(opts) {
|
|
@@ -119,14 +124,29 @@ export async function migrateContent(opts) {
|
|
|
119
124
|
: `Planning ${opts.targetDir} against ${at} Nothing is written without --apply.`);
|
|
120
125
|
const adapters = opts.adapters ?? (await loadSiteMdxAdapter(targetDir));
|
|
121
126
|
const fetch = opts.fetch ?? globalThis.fetch;
|
|
127
|
+
const transport = apply ? fetch : readOnlyFetch(fetch, LOCALE);
|
|
128
|
+
// #10735 D1 — the Language pass wraps the importer's read.
|
|
129
|
+
const pass = withLanguage(adapters, {
|
|
130
|
+
apiOrigin: origin,
|
|
131
|
+
workspaceId,
|
|
132
|
+
apiKey,
|
|
133
|
+
locale: LOCALE,
|
|
134
|
+
fetch: transport,
|
|
135
|
+
apply,
|
|
136
|
+
...(opts.sleep !== undefined && { sleep: opts.sleep }),
|
|
137
|
+
onLine,
|
|
138
|
+
});
|
|
139
|
+
// #10701 D7 — the docs-blog tree, when the site has one.
|
|
140
|
+
const editorialRoot = join(targetDir, "apps", "web", "content");
|
|
122
141
|
const result = await importMdx({
|
|
123
142
|
contentRoot: join(targetDir, "content"),
|
|
124
|
-
|
|
143
|
+
...(isDirectory(editorialRoot) && { editorialRoot }),
|
|
144
|
+
adapters: pass.adapters,
|
|
125
145
|
apiOrigin: origin,
|
|
126
146
|
workspaceId,
|
|
127
147
|
apiKey,
|
|
128
148
|
defaultLocale: LOCALE,
|
|
129
|
-
fetch:
|
|
149
|
+
fetch: transport,
|
|
130
150
|
...(opts.sleep !== undefined && { sleep: opts.sleep }),
|
|
131
151
|
// A dry run's importer lines say `created`/`updated`, which nothing was.
|
|
132
152
|
...(apply && { onLine }),
|
|
@@ -148,7 +168,17 @@ export async function migrateContent(opts) {
|
|
|
148
168
|
onLine(`${failed} page(s) would fail. Fix them before --apply.`);
|
|
149
169
|
}
|
|
150
170
|
}
|
|
151
|
-
|
|
171
|
+
// #10735 D7 — the Language lines come last.
|
|
172
|
+
const { result: language, off } = pass.report();
|
|
173
|
+
if (off !== undefined)
|
|
174
|
+
onLine(languageOffLine(off));
|
|
175
|
+
if (language !== undefined)
|
|
176
|
+
languageLines(language, apply).forEach(onLine);
|
|
177
|
+
return {
|
|
178
|
+
result,
|
|
179
|
+
exitCode: failed > 0 ? 1 : 0,
|
|
180
|
+
...(language !== undefined && { language }),
|
|
181
|
+
};
|
|
152
182
|
}
|
|
153
183
|
export const MIGRATE_CONTENT_HELP = `create-ailk migrate-content — import an existing site's pages into its workspace.
|
|
154
184
|
|
|
@@ -174,11 +204,27 @@ content/<Type>/<slug>.mdx) is written to the workspace as a draft through
|
|
|
174
204
|
the API's content routes, with the agent key, which is read from
|
|
175
205
|
AILK_AGENT_KEY only and needs the content-edit grant. Nothing is published,
|
|
176
206
|
so the live site does not change. The site needs no .ailk-site-manifest.json
|
|
177
|
-
and may be a spoke; its dependencies must be installed.
|
|
207
|
+
and may be a spoke; its dependencies must be installed. A page whose
|
|
208
|
+
frontmatter carries sections is converted: its words are written to the
|
|
209
|
+
page's Markdown and its sections to its layout.
|
|
210
|
+
|
|
211
|
+
Each FAQ item and glossary-index term becomes one Language item of its kind,
|
|
212
|
+
set Live when the run creates it, and its section pins the items in the
|
|
213
|
+
order the file lists them. A question on several pages is one item, with
|
|
214
|
+
the FAQ page's words (the glossary page's, for a term). Lines prefixed
|
|
215
|
+
language: report each item, and each page the database read will not show
|
|
216
|
+
as its file does. An API with no Language collection leaves them inline.
|
|
217
|
+
|
|
218
|
+
When the site has apps/web/content, each blog post and docs page in it is
|
|
219
|
+
written too: apps/web/content/en/blog/<slug>.mdx (or <slug>/index.mdx) as a
|
|
220
|
+
BlogPosting, the same under docs as a DocsDetail, and each directory's own
|
|
221
|
+
index.mdx as its BlogList or DocsList index. Their lines are prefixed
|
|
222
|
+
editorial: — each outcome's root is content or editorial.
|
|
178
223
|
|
|
179
224
|
A dry run prints create, update, unchanged, skipped or failed per page. It
|
|
180
225
|
cannot see a write the API would refuse at --apply (a conflict or a server
|
|
181
|
-
error),
|
|
226
|
+
error), nor a layout change on a page whose words did not change, and a
|
|
227
|
+
page whose content fails its page type is planned and saved
|
|
182
228
|
with errors the editor shows, not failed. --apply prints created, updated,
|
|
183
229
|
unchanged, skipped or failed per page, so a re-run is safe. Exit 0 when no
|
|
184
230
|
page failed, 1 when any did, 2 on a refusal (nothing is sent).
|
package/dist/seed-workspace.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { type Topology } from "./topology.js";
|
|
|
4
4
|
export declare const AGENT_KEY_ENV_VAR = "AILK_AGENT_KEY";
|
|
5
5
|
/** The environment variable the workspace id is read from when no flag names one. */
|
|
6
6
|
export declare const WORKSPACE_ID_ENV_VAR = "AILK_WORKSPACE_ID";
|
|
7
|
+
/** The environment variable each app chooses its content source by (#10659). */
|
|
8
|
+
export declare const CONTENT_ADAPTER_ENV_VAR = "CONTENT_ADAPTER";
|
|
7
9
|
/** A refusal the CLI exits 2 on, thrown before any request is sent. */
|
|
8
10
|
export declare class SeedWorkspaceRefusedError extends Error {
|
|
9
11
|
readonly exitCode = 2;
|
|
@@ -67,5 +69,5 @@ export interface SeedWorkspaceArgs {
|
|
|
67
69
|
workspaceId: string | undefined;
|
|
68
70
|
help: boolean;
|
|
69
71
|
}
|
|
70
|
-
export declare const SEED_WORKSPACE_HELP = "create-ailk seed-workspace \u2014 import a starter's pages into its workspace.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk seed-workspace [<dir>] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --api-url <origin> The site's API origin. Else AILK_API_URL. https:\n only (http: on localhost or 127.0.0.1).\n --workspace <wksp_id> The workspace the pages are written to. Else\n AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nEach content/en/<Type>/<slug>.mdx page is written to the workspace
|
|
72
|
+
export declare const SEED_WORKSPACE_HELP = "create-ailk seed-workspace \u2014 import a starter's pages into its workspace.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk seed-workspace [<dir>] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --api-url <origin> The site's API origin. Else AILK_API_URL. https:\n only (http: on localhost or 127.0.0.1).\n --workspace <wksp_id> The workspace the pages are written to. Else\n AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nEach content/en/<Type>/<slug>.mdx page is written to the workspace, and set\nLive when it has no Live version, through the API's content routes, with the\nagent key, which is read from AILK_AGENT_KEY only and needs the content-edit\nand content-publish grants. Each page prints created, updated, unchanged,\nskipped or failed, so a re-run is safe. When every page is Live, the site\nswitches apps/web and apps/api to CONTENT_ADAPTER=database, keeping a value\neither already sets. The site's dependencies must be installed. Exit 0 when\nevery page was seeded, 1 when any failed, 2 on a refusal (nothing is sent).\n";
|
|
71
73
|
export declare function parseSeedWorkspaceArgs(argv: string[]): SeedWorkspaceArgs;
|
package/dist/seed-workspace.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* A fresh scaffold's `content/` tree is exactly its roster (spec §1), so the
|
|
7
7
|
* seed is `importMdx` over `<dir>/content` with the site's own installed
|
|
8
8
|
* adapter (D1): one `text/page` Artifact per page, and a re-run writes
|
|
9
|
-
* nothing and reports every page `unchanged`.
|
|
9
|
+
* nothing and reports every page `unchanged`. Each page with no Live version
|
|
10
|
+
* is set Live, and when every page is Live both apps that choose a content
|
|
11
|
+
* source are switched to `CONTENT_ADAPTER=database`, each keeping a value it
|
|
12
|
+
* already sets (#10659 D3); anything less leaves the site on `content/`.
|
|
10
13
|
*
|
|
11
14
|
* The API origin comes from `--api-url`, else `AILK_API_URL`; the workspace
|
|
12
15
|
* id from `--workspace`, else `AILK_WORKSPACE_ID`; the agent key from
|
|
@@ -18,12 +21,14 @@
|
|
|
18
21
|
*
|
|
19
22
|
* The scaffold-time step (D3, D6) runs only on a hub with an Admin
|
|
20
23
|
* (`workspace` or `multi-tenant`), after `--install`, with all three
|
|
21
|
-
* variables set; otherwise it prints why and the command that seeds
|
|
22
|
-
* or nothing on a site with no Admin. It never fails the
|
|
24
|
+
* variables set; otherwise it prints why and the command that seeds and
|
|
25
|
+
* switches later, or nothing on a site with no Admin. It never fails the
|
|
26
|
+
* scaffold.
|
|
23
27
|
*
|
|
24
28
|
* Pure module: no `import.meta.url`, so jest can import it directly. `fetch`,
|
|
25
29
|
* `sleep`, `env` and the adapter are injectable. Spec:
|
|
26
|
-
* docs/specs/10204-seed-the-starter-s-roster-spec.md
|
|
30
|
+
* docs/specs/10204-seed-the-starter-s-roster-spec.md; the publish and the
|
|
31
|
+
* switch docs/specs/10659-a-licensed-hub-scaffold-starts-spec.md.
|
|
27
32
|
*/
|
|
28
33
|
import { join, resolve } from "node:path";
|
|
29
34
|
import { parseArgs } from "node:util";
|
|
@@ -31,11 +36,13 @@ import { AddModuleRefusedError } from "./add-module-core.js";
|
|
|
31
36
|
import { ImportMdxRefusedError, formatImportMdxReport, importMdx, loadSiteMdxAdapter, } from "./lib/import-mdx.js";
|
|
32
37
|
import { ParseError } from "./parse-args.js";
|
|
33
38
|
import { readSiteRoster } from "./site-roster.js";
|
|
34
|
-
import { HUB_ORIGIN_ENV_VAR, TopologyError, normalizeHubUrl, readSiteTopology, } from "./topology.js";
|
|
39
|
+
import { HUB_ORIGIN_ENV_VAR, TopologyError, normalizeHubUrl, readSiteTopology, writeApiEnv, writeWebEnv, } from "./topology.js";
|
|
35
40
|
/** The environment variable the agent key is read from — and nowhere else. */
|
|
36
41
|
export const AGENT_KEY_ENV_VAR = "AILK_AGENT_KEY";
|
|
37
42
|
/** The environment variable the workspace id is read from when no flag names one. */
|
|
38
43
|
export const WORKSPACE_ID_ENV_VAR = "AILK_WORKSPACE_ID";
|
|
44
|
+
/** The environment variable each app chooses its content source by (#10659). */
|
|
45
|
+
export const CONTENT_ADAPTER_ENV_VAR = "CONTENT_ADAPTER";
|
|
39
46
|
/** A refusal the CLI exits 2 on, thrown before any request is sent. */
|
|
40
47
|
export class SeedWorkspaceRefusedError extends Error {
|
|
41
48
|
exitCode = 2;
|
|
@@ -83,7 +90,7 @@ export async function seedWorkspace(opts) {
|
|
|
83
90
|
}
|
|
84
91
|
const apiKey = given(env[AGENT_KEY_ENV_VAR]);
|
|
85
92
|
if (apiKey === undefined) {
|
|
86
|
-
refuse(`set ${AGENT_KEY_ENV_VAR} to an agent key with the content-edit
|
|
93
|
+
refuse(`set ${AGENT_KEY_ENV_VAR} to an agent key with the content-edit and content-publish grants. It is read from the environment only — never a flag or a file.`);
|
|
87
94
|
}
|
|
88
95
|
const workspaceId = given(opts.workspaceId) ?? given(env[WORKSPACE_ID_ENV_VAR]);
|
|
89
96
|
if (workspaceId === undefined) {
|
|
@@ -115,12 +122,30 @@ export async function seedWorkspace(opts) {
|
|
|
115
122
|
...(opts.fetch !== undefined && { fetch: opts.fetch }),
|
|
116
123
|
...(opts.sleep !== undefined && { sleep: opts.sleep }),
|
|
117
124
|
onLine,
|
|
125
|
+
publish: true,
|
|
118
126
|
});
|
|
119
127
|
// The report's count line alone: no page rows.
|
|
120
128
|
onLine(formatImportMdxReport({ pages: [], counts: result.counts }).trimEnd());
|
|
121
|
-
const { failed } = result.counts;
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
const { created, updated, unchanged, failed } = result.counts;
|
|
130
|
+
const live = result.live ?? 0;
|
|
131
|
+
onLine(`Live: ${live} page(s), ${result.published ?? 0} published now.`);
|
|
132
|
+
// #10659 D3 — both apps switch only when every page is in and Live.
|
|
133
|
+
if (failed === 0 && live >= 1 && live === created + updated + unchanged) {
|
|
134
|
+
for (const [app, file, write] of [
|
|
135
|
+
["apps/web", "apps/web/.env.local", writeWebEnv],
|
|
136
|
+
["apps/api", "apps/api/.env", writeApiEnv],
|
|
137
|
+
]) {
|
|
138
|
+
onLine(write(targetDir, CONTENT_ADAPTER_ENV_VAR, () => "database")
|
|
139
|
+
? `Content source: the database for ${app} (${CONTENT_ADAPTER_ENV_VAR}=database in ${file}).`
|
|
140
|
+
: `Content source: ${app} already sets ${CONTENT_ADAPTER_ENV_VAR}, which was kept.`);
|
|
141
|
+
}
|
|
142
|
+
onLine(" The API serves these pages from the database it was seeded through (DATABASE_URL), for the workspace its site resolves.");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (failed > 0) {
|
|
146
|
+
onLine(`${failed} page(s) were not seeded. Fix them and re-run: create-ailk seed-workspace ${opts.targetDir}`);
|
|
147
|
+
}
|
|
148
|
+
onLine("Content source: content/ (unchanged) until every page is seeded and Live.");
|
|
124
149
|
}
|
|
125
150
|
return { result, exitCode: failed > 0 ? 1 : 0 };
|
|
126
151
|
}
|
|
@@ -166,9 +191,10 @@ export async function runScaffoldSeed(opts) {
|
|
|
166
191
|
const plan = scaffoldSeedPlan(opts);
|
|
167
192
|
if (plan.kind === "not-applicable")
|
|
168
193
|
return;
|
|
194
|
+
const later = ` To seed it and switch to the database later: ${AGENT_KEY_ENV_VAR}=<key> create-ailk seed-workspace ${opts.targetDir} --api-url <origin> --workspace <wksp_id>`;
|
|
169
195
|
if (plan.kind === "skip") {
|
|
170
196
|
opts.onLine(plan.reason);
|
|
171
|
-
opts.onLine(
|
|
197
|
+
opts.onLine(later);
|
|
172
198
|
return;
|
|
173
199
|
}
|
|
174
200
|
opts.onLine("Seeding the workspace:");
|
|
@@ -188,6 +214,7 @@ export async function runScaffoldSeed(opts) {
|
|
|
188
214
|
throw err;
|
|
189
215
|
}
|
|
190
216
|
opts.onLine(err.message);
|
|
217
|
+
opts.onLine(later);
|
|
191
218
|
}
|
|
192
219
|
}
|
|
193
220
|
export const SEED_WORKSPACE_HELP = `create-ailk seed-workspace — import a starter's pages into its workspace.
|
|
@@ -205,12 +232,14 @@ Options:
|
|
|
205
232
|
AILK_WORKSPACE_ID.
|
|
206
233
|
-h, --help Show this help.
|
|
207
234
|
|
|
208
|
-
Each content/en/<Type>/<slug>.mdx page is written to the workspace
|
|
209
|
-
|
|
210
|
-
from AILK_AGENT_KEY only and needs the content-edit
|
|
211
|
-
created, updated, unchanged,
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
Each content/en/<Type>/<slug>.mdx page is written to the workspace, and set
|
|
236
|
+
Live when it has no Live version, through the API's content routes, with the
|
|
237
|
+
agent key, which is read from AILK_AGENT_KEY only and needs the content-edit
|
|
238
|
+
and content-publish grants. Each page prints created, updated, unchanged,
|
|
239
|
+
skipped or failed, so a re-run is safe. When every page is Live, the site
|
|
240
|
+
switches apps/web and apps/api to CONTENT_ADAPTER=database, keeping a value
|
|
241
|
+
either already sets. The site's dependencies must be installed. Exit 0 when
|
|
242
|
+
every page was seeded, 1 when any failed, 2 on a refusal (nothing is sent).
|
|
214
243
|
`;
|
|
215
244
|
export function parseSeedWorkspaceArgs(argv) {
|
|
216
245
|
let parsed;
|
package/dist/topology.d.ts
CHANGED
|
@@ -215,6 +215,18 @@ export declare const HUB_ORIGIN_ENV_VAR = "AILK_API_URL";
|
|
|
215
215
|
* the hub issues those.
|
|
216
216
|
*/
|
|
217
217
|
export declare function writeHubOriginEnv(targetDir: string, origin: string): void;
|
|
218
|
+
/**
|
|
219
|
+
* `<name>=<value()>` into `apps/web/.env.local`, unless any env file Next
|
|
220
|
+
* loads for `apps/web` already assigns `name` (`value` is then never called);
|
|
221
|
+
* and `.env.local` gitignored either way. `true` when the line was written.
|
|
222
|
+
*/
|
|
223
|
+
export declare function writeWebEnv(targetDir: string, name: string, value: () => string): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* `<name>=<value()>` into `apps/api/.env` — the one file the API's
|
|
226
|
+
* `dotenv.config()` reads — under `writeWebEnv`'s never-clobber rule, and
|
|
227
|
+
* `.env` gitignored either way (#10659 D6). `true` when the line was written.
|
|
228
|
+
*/
|
|
229
|
+
export declare function writeApiEnv(targetDir: string, name: string, value: () => string): boolean;
|
|
218
230
|
/** The env var naming the hub's web origin a hub-mode spoke signs in at (#7894 D11). */
|
|
219
231
|
export declare const HUB_SIGN_IN_URL_ENV_VAR = "AILK_HUB_SIGN_IN_URL";
|
|
220
232
|
/** The env var holding a hub-mode spoke's own cookie key material (#7894 D7, D11). */
|
package/dist/topology.js
CHANGED
|
@@ -903,19 +903,32 @@ export function writeHubOriginEnv(targetDir, origin) {
|
|
|
903
903
|
writeWebEnv(targetDir, HUB_ORIGIN_ENV_VAR, () => origin);
|
|
904
904
|
}
|
|
905
905
|
/**
|
|
906
|
-
* `<name>=<value()>` into
|
|
907
|
-
*
|
|
908
|
-
*
|
|
906
|
+
* `<name>=<value()>` into `apps/web/.env.local`, unless any env file Next
|
|
907
|
+
* loads for `apps/web` already assigns `name` (`value` is then never called);
|
|
908
|
+
* and `.env.local` gitignored either way. `true` when the line was written.
|
|
909
909
|
*/
|
|
910
|
-
function writeWebEnv(targetDir, name, value) {
|
|
911
|
-
|
|
910
|
+
export function writeWebEnv(targetDir, name, value) {
|
|
911
|
+
return writeEnvLine(join(targetDir, "apps", "web"), NEXT_ENV_FILES, ".env.local", targetDir, ENV_LOCAL_IGNORE_LINES, name, value);
|
|
912
|
+
}
|
|
913
|
+
/** The lines that gitignore `apps/api/.env`; `.env` is appended when none is there. */
|
|
914
|
+
const ENV_IGNORE_LINES = new Set([".env", "**/.env", ".env*"]);
|
|
915
|
+
/**
|
|
916
|
+
* `<name>=<value()>` into `apps/api/.env` — the one file the API's
|
|
917
|
+
* `dotenv.config()` reads — under `writeWebEnv`'s never-clobber rule, and
|
|
918
|
+
* `.env` gitignored either way (#10659 D6). `true` when the line was written.
|
|
919
|
+
*/
|
|
920
|
+
export function writeApiEnv(targetDir, name, value) {
|
|
921
|
+
return writeEnvLine(join(targetDir, "apps", "api"), [".env"], ".env", targetDir, ENV_IGNORE_LINES, name, value);
|
|
922
|
+
}
|
|
923
|
+
/** One app's never-clobber env append, and the root `.gitignore` guard. */
|
|
924
|
+
function writeEnvLine(appDir, files, target, targetDir, ignoreLines, name, value) {
|
|
912
925
|
const assigned = new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*\\S`, "m");
|
|
913
|
-
const already =
|
|
914
|
-
const path = join(
|
|
926
|
+
const already = files.some((file) => {
|
|
927
|
+
const path = join(appDir, file);
|
|
915
928
|
return existsSync(path) && assigned.test(readFileSync(path, "utf8"));
|
|
916
929
|
});
|
|
917
930
|
if (!already) {
|
|
918
|
-
const file = join(
|
|
931
|
+
const file = join(appDir, target);
|
|
919
932
|
const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
920
933
|
writeOwnerOnly(file, appendLine(existing, `${name}=${value()}`));
|
|
921
934
|
}
|
|
@@ -923,9 +936,10 @@ function writeWebEnv(targetDir, name, value) {
|
|
|
923
936
|
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
|
|
924
937
|
const covered = ignore
|
|
925
938
|
.split("\n")
|
|
926
|
-
.some((line) =>
|
|
939
|
+
.some((line) => ignoreLines.has(line.trim()));
|
|
927
940
|
if (!covered)
|
|
928
|
-
writeFileSync(ignorePath, appendLine(ignore,
|
|
941
|
+
writeFileSync(ignorePath, appendLine(ignore, target));
|
|
942
|
+
return !already;
|
|
929
943
|
}
|
|
930
944
|
/** The env var naming the hub's web origin a hub-mode spoke signs in at (#7894 D11). */
|
|
931
945
|
export const HUB_SIGN_IN_URL_ENV_VAR = "AILK_HUB_SIGN_IN_URL";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tar": "^7.5.21"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@working-theory/schema": "0.
|
|
32
|
-
"@working-theory/templates": "0.
|
|
31
|
+
"@working-theory/schema": "0.54.0",
|
|
32
|
+
"@working-theory/templates": "0.54.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
"matcher": "Edit|Write",
|
|
6
6
|
"command": "bash .claude/hooks/template-guard.sh"
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
"event": "PreToolUse",
|
|
10
|
+
"matcher": "Edit|Write",
|
|
11
|
+
"command": "bash .claude/hooks/content-source-guard.sh"
|
|
12
|
+
},
|
|
8
13
|
{
|
|
9
14
|
"event": "PostToolUse",
|
|
10
15
|
"matcher": "Edit|Write",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse (Edit|Write): refuse a new page file on a site whose pages come
|
|
3
|
+
# from the database. When CONTENT_ADAPTER=database — in the environment or in
|
|
4
|
+
# one of Next's eight env files under apps/web/ — a call whose file_path is a
|
|
5
|
+
# content/en/**/*.mdx that does not exist yet is refused (exit 2); the refusal
|
|
6
|
+
# on stderr names the MCP tools that change a page instead. An existing page
|
|
7
|
+
# file, any other path and a files site pass silently. The same rule and the
|
|
8
|
+
# same message as tools/check-content-source.ts, which the preflight runs.
|
|
9
|
+
# Pure bash and awk — nothing a scaffold has to install. Registered in
|
|
10
|
+
# .claude/settings.json and declared in .claude/cloudparity-repo-hooks.json;
|
|
11
|
+
# keep the two in step.
|
|
12
|
+
export LC_ALL=C
|
|
13
|
+
cd "$CLAUDE_PROJECT_DIR" || exit 0
|
|
14
|
+
|
|
15
|
+
IFS= read -r -d '' input
|
|
16
|
+
[[ $input =~ \"file_path\"[[:space:]]*:[[:space:]]*\"([^\"\\]*)\" ]] || exit 0
|
|
17
|
+
rel=${BASH_REMATCH[1]}
|
|
18
|
+
case $rel in
|
|
19
|
+
"$PWD"/*) rel=${rel#"$PWD"/} ;;
|
|
20
|
+
/*) exit 0 ;;
|
|
21
|
+
./*) rel=${rel#./} ;;
|
|
22
|
+
esac
|
|
23
|
+
case "/$rel/" in
|
|
24
|
+
*/./*|*/../*|*//*) exit 0 ;;
|
|
25
|
+
esac
|
|
26
|
+
# `*` crosses `/` in a case pattern, so this is content/en/**/*.mdx.
|
|
27
|
+
case $rel in
|
|
28
|
+
content/en/*.mdx) ;;
|
|
29
|
+
*) exit 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
[ ! -e "$rel" ] || exit 0
|
|
32
|
+
|
|
33
|
+
# The source rule: the environment, else the first env file whose last
|
|
34
|
+
# CONTENT_ADAPTER assignment reads exactly `database`.
|
|
35
|
+
source_name=
|
|
36
|
+
if [ "${CONTENT_ADAPTER-}" = database ]; then
|
|
37
|
+
source_name="the environment"
|
|
38
|
+
else
|
|
39
|
+
for name in .env .env.local .env.development .env.development.local \
|
|
40
|
+
.env.production .env.production.local .env.test .env.test.local; do
|
|
41
|
+
file=apps/web/$name
|
|
42
|
+
[ -f "$file" ] || continue
|
|
43
|
+
value=$(awk -v sq="'" '
|
|
44
|
+
/^[ \t]*(export[ \t]+)?CONTENT_ADAPTER[ \t]*=/ {
|
|
45
|
+
v = substr($0, index($0, "=") + 1)
|
|
46
|
+
n = length(v); quote = ""; cut = n
|
|
47
|
+
for (i = 1; i <= n; i++) {
|
|
48
|
+
c = substr(v, i, 1)
|
|
49
|
+
if (quote != "") { if (c == quote) quote = ""; continue }
|
|
50
|
+
if (c == "\"" || c == sq) { quote = c; continue }
|
|
51
|
+
if (c == "#" && i > 1 && substr(v, i - 1, 1) ~ /[ \t]/) { cut = i - 1; break }
|
|
52
|
+
}
|
|
53
|
+
v = substr(v, 1, cut)
|
|
54
|
+
sub(/^[ \t\r]+/, "", v); sub(/[ \t\r]+$/, "", v)
|
|
55
|
+
f = substr(v, 1, 1)
|
|
56
|
+
if (length(v) >= 2 && (f == "\"" || f == sq) && substr(v, length(v), 1) == f) {
|
|
57
|
+
v = substr(v, 2, length(v) - 2)
|
|
58
|
+
}
|
|
59
|
+
last = v
|
|
60
|
+
}
|
|
61
|
+
END { print last }
|
|
62
|
+
' "$file")
|
|
63
|
+
if [ "$value" = database ]; then
|
|
64
|
+
source_name=$file
|
|
65
|
+
break
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
fi
|
|
69
|
+
[ -n "$source_name" ] || exit 0
|
|
70
|
+
|
|
71
|
+
printf '%s\n' >&2 \
|
|
72
|
+
"content-source: $rel is a page file, but this site's pages come from the database (CONTENT_ADAPTER=database in $source_name), so the site never reads it." \
|
|
73
|
+
"Change a page through the site's MCP tools instead: save_draft writes the draft and publish_draft publishes it; read_content returns the current page." \
|
|
74
|
+
"Pages and layout: words live in the database, layout stays in code (CLAUDE.md § Content)."
|
|
75
|
+
exit 2
|
|
@@ -67,6 +67,8 @@ jobs:
|
|
|
67
67
|
# A hand-edit inside a template-owned section of CLAUDE.md — the same
|
|
68
68
|
# check `pnpm preflight` runs locally.
|
|
69
69
|
- run: pnpm claude-md:check
|
|
70
|
+
# A new page file on a site whose pages come from the database (#10660).
|
|
71
|
+
- run: pnpm content-source:check
|
|
70
72
|
|
|
71
73
|
- run: pnpm test
|
|
72
74
|
|
package/templates/CLAUDE.md
CHANGED
|
@@ -178,6 +178,11 @@ sections:
|
|
|
178
178
|
Copy an existing file in the same folder, then change it. Text in `{braces}`
|
|
179
179
|
is placeholder copy. Don't invent a `type` that `@working-theory/schema`
|
|
180
180
|
doesn't export.
|
|
181
|
+
|
|
182
|
+
Where a page lives depends on `CONTENT_ADAPTER` in `apps/web/.env.local`.
|
|
183
|
+
Unset or `mdx` (the default): the file above. `database`: the database —
|
|
184
|
+
change a page with the MCP tools `save_draft` then `publish_draft`; a new
|
|
185
|
+
file under `content/en/` is refused by a hook and by `pnpm preflight`.
|
|
181
186
|
<!-- /ailk:template id="content" -->
|
|
182
187
|
|
|
183
188
|
<!-- ailk:template id="hard-floors" -->
|
|
@@ -187,7 +187,7 @@ here. Spec: `docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md` D13–D16.
|
|
|
187
187
|
- **apps/mcp checks only the key's shape.** The key is attached as the SDK's `req.auth` for that
|
|
188
188
|
request alone and reaches a tool as `extra.authInfo.token`. apps/api is the one place it is
|
|
189
189
|
checked, and its tenant guard and the caller's clamped role decide every answer.
|
|
190
|
-
- **Thirty-
|
|
190
|
+
- **Thirty-nine tools, each a `twin`.** The catalog rows carry `surface: "workspace"`, `tier: "pro"`
|
|
191
191
|
and `modulesAnyOf` (the modules any one of which registers the tool — its twin route's gate):
|
|
192
192
|
`get_agent_context`, (#9683) `get_workspace_knowledge` — twin of `GET
|
|
193
193
|
/v1/workspaces/:workspaceId/knowledge`: the business's shown offerings, live magnets, listed
|
|
@@ -209,7 +209,7 @@ here. Spec: `docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md` D13–D16.
|
|
|
209
209
|
tools, `list_proposals` and `create_proposal`, twinning `GET`/`POST
|
|
210
210
|
/v1/workspaces/:workspaceId/people/:personId/proposals` with the same grant: a proposal is
|
|
211
211
|
private to one client, `payments` says whether approval leads to a Stripe page, and an agent's
|
|
212
|
-
price is refused `proposal_needs_person` (a person prices) — and (#9681) the four person-thread tools, `get_person_thread`, `set_person_thread_state`, `list_external_threads` and `add_external_thread`, twinning `GET`/`PATCH …/people/:personId/thread` and `GET`/`POST …/people/:personId/external-threads`: a client's whole thread page by page with its read/archived/resolved state, and pointers to email and Slack threads (never message text); reading needs the `people-read` grant, writing also `inbox-act`, and the client erasure route has no tool, by decision — and (#9682) the three person-memory tools, `list_person_memories`, `get_person_memory` and `propose_person_memory`, twinning `GET …/people/:personId/memories`, `GET …/memories/:path` and `POST …/memories`: an agent reads only live, unexpired memories (confirmed by a person, read within 180 days) and its proposal is never live — a new path is a draft, a confirmed one gets a pending version — needing the `people-read` or `people-memory-propose` grant to read and `people-memory-propose` to propose; the six person-only memory routes (edit, delete, confirm, redact, the versions list, restore) have no tool, by decision — and (#9749) `get_slack_connection_status`, twin of `GET /v1/workspaces/:workspaceId/slack-connection` (registered with `workspace`/`multi-tenant`, its route's `TENANCY` row: whether the Organization's Slack is connected, its team name and the date, never a credential; connect and disconnect are a person's act and have no tool, by decision), and `get_prospect_channel_settings` / `set_prospect_channel_settings`, twins of `GET`/`PUT …/prospect-channel` (registered with `scheduling`, their routes' module): the update replaces every field, needs the `meeting-types-manage` grant, is refused `slack_not_connected` while the Organization has no Slack connection, and an agent may change only `enabled`, `channelPrefix` and `archiveDays` — a change to `memberIds` or `welcomeTemplate` is refused `prospect_channel_needs_person` — and (#10218) the content-suggestion pair, `draft_page_suggestions` and `decide_page_suggestion` — and (#10336) the draft pair, `save_draft` and `read_report`, twinning `PATCH`/`GET …/content/:type/:slug` with the `content-edit` grant: `save_draft` takes the page's `content` whole as the PATCH body and its `version` as `X-Draft-Version`, writes a draft version that never goes live
|
|
212
|
+
price is refused `proposal_needs_person` (a person prices) — and (#9681) the four person-thread tools, `get_person_thread`, `set_person_thread_state`, `list_external_threads` and `add_external_thread`, twinning `GET`/`PATCH …/people/:personId/thread` and `GET`/`POST …/people/:personId/external-threads`: a client's whole thread page by page with its read/archived/resolved state, and pointers to email and Slack threads (never message text); reading needs the `people-read` grant, writing also `inbox-act`, and the client erasure route has no tool, by decision — and (#9682) the three person-memory tools, `list_person_memories`, `get_person_memory` and `propose_person_memory`, twinning `GET …/people/:personId/memories`, `GET …/memories/:path` and `POST …/memories`: an agent reads only live, unexpired memories (confirmed by a person, read within 180 days) and its proposal is never live — a new path is a draft, a confirmed one gets a pending version — needing the `people-read` or `people-memory-propose` grant to read and `people-memory-propose` to propose; the six person-only memory routes (edit, delete, confirm, redact, the versions list, restore) have no tool, by decision — and (#9749) `get_slack_connection_status`, twin of `GET /v1/workspaces/:workspaceId/slack-connection` (registered with `workspace`/`multi-tenant`, its route's `TENANCY` row: whether the Organization's Slack is connected, its team name and the date, never a credential; connect and disconnect are a person's act and have no tool, by decision), and `get_prospect_channel_settings` / `set_prospect_channel_settings`, twins of `GET`/`PUT …/prospect-channel` (registered with `scheduling`, their routes' module): the update replaces every field, needs the `meeting-types-manage` grant, is refused `slack_not_connected` while the Organization has no Slack connection, and an agent may change only `enabled`, `channelPrefix` and `archiveDays` — a change to `memberIds` or `welcomeTemplate` is refused `prospect_channel_needs_person` — and (#10218) the content-suggestion pair, `draft_page_suggestions` and `decide_page_suggestion` — and (#10336) the draft pair, `save_draft` and `read_report`, twinning `PATCH`/`GET …/content/:type/:slug` with the `content-edit` grant: `save_draft` takes the page's `content` whole as the PATCH body and its `version` as `X-Draft-Version`, writes a draft version that never goes live, and answers 409 `conflict` with `currentVersion` when the draft moved; `read_report` returns the draft tip, the version a save must send, its open suggestions and the route's check report (`ready`, `needs repair` or `needs your words`), computed by the route and never by the tool — and (#10661) `publish_draft`, twinning `POST …/content/:type/:slug/publish` with the `content-publish` grant: it publishes the draft tip (or a named version) exactly as a person holding that permission does, an agent without the grant is refused 403 `agent_refused` before anything is read, and it never widens Audience or re-enables a doc — and (#10345) the knowledge-base pair, `read_knowledge` and `read_knowledge_doc`, twinning `GET …/knowledge-base` and `GET …/knowledge-base/:type/:slug` with no grant (`docs-read` is every role's): the docs a named member may read (`userId`, by default the agent's own person), at the lower of the agent's person's role and that member's, so naming someone only narrows; Live does not gate them and a disabled doc never appears; `filter: "workspace-memory"` answers the always-loaded set with its `totalTokens` and an 8000-token `warning` — and (#10733) the five Language tools, `list_language_items`, `get_language_item`, `save_language_item`, `set_language_item_live` and `list_language_item_pages`, twinning `GET …/language`, `GET`/`PUT …/language/:kind/:slug`, `POST …/language/:kind/:slug/live` and `GET …/language/:kind/:slug/appears-on` (`mcp-workspace`'s `language.ts`): a workspace's glossary terms, FAQs and keywords; reading needs no grant, saving the `content-edit` grant (`version: null` creates, a tip id updates, 409 `slug_taken` / `stale` otherwise) and the Live switch `content-publish` with the `version` the person reviewed; tags are the hub's and never written by a tool; a keyword is vocabulary and is never shown to visitors; the delete route is a person's write (FEAT-101 Disable) and has no tool, by decision. No workspace tool widens Audience or re-enables a doc, and only `publish_draft` and `set_language_item_live`, with the `content-publish` grant, set Live — the access and Language delete routes answer every agent 403 `agent_refused`, and the publish and Live routes an agent without the grant, which reaches a tool as the code and status. Each takes its route's params, query and body and returns the route's
|
|
213
213
|
`data`. They are in the parity guard like every other tool, and never in
|
|
214
214
|
`/.well-known/mcp.json`.
|
|
215
215
|
- **The OAuth bearer branch (#10391).** When `BETTER_AUTH_URL` (the site's origin, its OAuth
|
|
@@ -230,7 +230,7 @@ here. Spec: `docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md` D13–D16.
|
|
|
230
230
|
a spoke has no authorization server, so it leaves `BETTER_AUTH_URL` unset. `/mcp` is untouched.
|
|
231
231
|
- **Handlers forward, nothing else.** `workspace-server.ts` resolves each tool from
|
|
232
232
|
`@working-theory/mcp-workspace`'s `workspaceTools` (`agent_context.ts`, `knowledge.ts`, `inbox_messages.ts`,
|
|
233
|
-
`inbox_submissions.ts`, `inbox_applications.ts`, `people_list.ts`, `person_notes.ts`, `person_thread.ts`, `person_memory.ts`, `proposals.ts`, `slack_connection.ts`, `page_suggestions.ts`, `content_draft.ts`, `knowledge_base.ts`, all
|
|
233
|
+
`inbox_submissions.ts`, `inbox_applications.ts`, `people_list.ts`, `person_notes.ts`, `person_thread.ts`, `person_memory.ts`, `proposals.ts`, `slack_connection.ts`, `page_suggestions.ts`, `content_draft.ts`, `content_publish.ts`, `knowledge_base.ts`, all
|
|
234
234
|
`// oss: false`) and, for the two prospect-channel settings tools, `@working-theory/mcp-scheduling`'s
|
|
235
235
|
`workspaceTools` (`prospect_channel_settings.ts`, #9749 — the pack the scheduling module carries, so a
|
|
236
236
|
scheduling-only build serves what it lists), loaded through the site's generated registry, as `server.ts` resolves the
|
|
@@ -313,7 +313,11 @@ Example (lead routing):
|
|
|
313
313
|
- Don't add a tool without the matching `apps/api` endpoint.
|
|
314
314
|
- Don't put business logic in tool handlers — they're adapters.
|
|
315
315
|
|
|
316
|
-
Last updated: 2026-
|
|
316
|
+
Last updated: 2026-11-02 — #10733 (D7, D8): the five Language twins (`mcp-workspace`'s `language.ts`) twin the Language collection's list, read, save, Live and appears-on routes; the person-only delete has no tool — the signed-in workspace surface gains five tools — verified against `packages/metadata/src/mcp-tool-catalog.ts`, `packages-paid/mcp-workspace/src/index.ts`, `packages-paid/api-workspace/src/routes/workspaces/language.ts` and `tests/parity/api-parity/kinds/site.ts`.
|
|
317
|
+
|
|
318
|
+
Prior: 2026-11-02 — #10661 (D1, D5–D8): `publish_draft` (`mcp-workspace`'s `content_publish.ts`) twins the publish route for an agent holding `content-publish`, whose blanket agent refusal narrows to agents without the grant — the signed-in workspace surface is thirty-nine tools — verified against `packages/metadata/src/mcp-tool-catalog.ts`, `packages-paid/mcp-workspace/src/index.ts`, `packages-paid/api-content-write/src/routes/workspaces/content.ts` and `tests/parity/api-parity/kinds/site.ts`.
|
|
319
|
+
|
|
320
|
+
Prior: 2026-10-31 — #10391 (D2–D6): `/mcp/workspace` gains the OAuth bearer branch beside the agent key — one `token-info` lookup per request, the `WWW-Authenticate` challenge and the RFC 9728 protected-resource document, on only with `BETTER_AUTH_URL` and `MCP_PUBLIC_HTTP_URL` — verified against `packages/mcp-server/src/{http-transport,oauth-bearer}.ts`.
|
|
317
321
|
|
|
318
322
|
Prior: 2026-10-31 — #10345 (D9, D12): `read_knowledge` and `read_knowledge_doc` (`mcp-workspace`'s `knowledge_base.ts`) twin the per-user knowledge-base read — the signed-in workspace surface is thirty-seven tools — verified against `packages/metadata/src/mcp-tool-catalog.ts`, `packages-paid/mcp-workspace/src/index.ts` and `tests/parity/api-parity/kinds/site.ts`.
|
|
319
323
|
|
|
@@ -17,8 +17,10 @@ CONTENT_ADAPTER=mdx
|
|
|
17
17
|
# Override the on-disk content root. Defaults to <repo-root>/content.
|
|
18
18
|
# AILK_CONTENT_ROOT=
|
|
19
19
|
|
|
20
|
-
# The
|
|
21
|
-
#
|
|
20
|
+
# The secret /api/revalidate verifies apps/api's publish webhook against
|
|
21
|
+
# (#5877). On a site a hub serves (a spoke), the value `spokes revalidate-url`
|
|
22
|
+
# printed for it — NEVER the hub's own secret (#10593). Otherwise, the SAME
|
|
23
|
+
# value as the API's CONTENT_REVALIDATE_SECRET.
|
|
22
24
|
# Needed when CONTENT_ADAPTER=database. Unset means the route returns 503 and
|
|
23
25
|
# the site is not push-revalidated: a publish changes nothing user-visible
|
|
24
26
|
# until a rebuild.
|