@zhama/tego-plugin-sdk 0.0.0-stage → 1.36.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 (98) hide show
  1. package/LICENSE +172 -0
  2. package/README.md +846 -2
  3. package/access-catalog.schema.json +304 -0
  4. package/access-fixtures/resolve.json +1359 -0
  5. package/bin/tego-plugin-pack.mjs +2 -0
  6. package/bin/tego-plugin.mjs +3 -0
  7. package/dist/access-drizzle.cjs +1 -0
  8. package/dist/access-drizzle.d.cts +726 -0
  9. package/dist/access-drizzle.d.ts +726 -0
  10. package/dist/access-drizzle.js +1 -0
  11. package/dist/access-hono.cjs +3 -0
  12. package/dist/access-hono.d.cts +108 -0
  13. package/dist/access-hono.d.ts +108 -0
  14. package/dist/access-hono.js +3 -0
  15. package/dist/access-implications.cjs +1 -0
  16. package/dist/access-implications.d.cts +25 -0
  17. package/dist/access-implications.d.ts +25 -0
  18. package/dist/access-implications.js +1 -0
  19. package/dist/access.cjs +26 -0
  20. package/dist/access.d.cts +884 -0
  21. package/dist/access.d.ts +884 -0
  22. package/dist/access.js +26 -0
  23. package/dist/catalog-CSSbrfEf.d.cts +361 -0
  24. package/dist/catalog-CSSbrfEf.d.ts +361 -0
  25. package/dist/chunk-3KUOBGJN.js +19 -0
  26. package/dist/chunk-3WA3M347.cjs +1 -0
  27. package/dist/chunk-5CU33JVY.js +1 -0
  28. package/dist/chunk-7K37PVGV.js +5 -0
  29. package/dist/chunk-7XHF3YSK.js +1 -0
  30. package/dist/chunk-AJY7ZHXK.cjs +3 -0
  31. package/dist/chunk-BN6TGHSB.cjs +1 -0
  32. package/dist/chunk-BTHXQWHT.js +1 -0
  33. package/dist/chunk-C2OCOPZQ.cjs +1 -0
  34. package/dist/chunk-IMAVYQ4U.cjs +4 -0
  35. package/dist/chunk-OP64Y4IN.cjs +1 -0
  36. package/dist/chunk-QKSPPMVN.js +1 -0
  37. package/dist/chunk-ROFUQLOZ.cjs +1 -0
  38. package/dist/chunk-SNUCO2MK.js +1 -0
  39. package/dist/chunk-THMWRUAT.cjs +19 -0
  40. package/dist/chunk-VMCGDDGP.js +3 -0
  41. package/dist/chunk-W7C357A6.cjs +5 -0
  42. package/dist/chunk-WLTASJF6.js +4 -0
  43. package/dist/egress.cjs +7 -0
  44. package/dist/egress.d.cts +9 -0
  45. package/dist/egress.d.ts +9 -0
  46. package/dist/egress.js +7 -0
  47. package/dist/frontend.cjs +2 -0
  48. package/dist/frontend.d.cts +353 -0
  49. package/dist/frontend.d.ts +353 -0
  50. package/dist/frontend.js +2 -0
  51. package/dist/gateway-signature.cjs +1 -0
  52. package/dist/gateway-signature.d.cts +112 -0
  53. package/dist/gateway-signature.d.ts +112 -0
  54. package/dist/gateway-signature.js +1 -0
  55. package/dist/index.cjs +1 -0
  56. package/dist/index.d.cts +528 -0
  57. package/dist/index.d.ts +528 -0
  58. package/dist/index.js +1 -0
  59. package/dist/manifest-types-DQARCQJt.d.cts +627 -0
  60. package/dist/manifest-types-DQARCQJt.d.ts +627 -0
  61. package/dist/manifest.cjs +1 -0
  62. package/dist/manifest.d.cts +2824 -0
  63. package/dist/manifest.d.ts +2824 -0
  64. package/dist/manifest.js +1 -0
  65. package/dist/mcp-identity.cjs +2 -0
  66. package/dist/mcp-identity.d.cts +106 -0
  67. package/dist/mcp-identity.d.ts +106 -0
  68. package/dist/mcp-identity.js +2 -0
  69. package/dist/pack.cjs +1 -0
  70. package/dist/pack.d.cts +169 -0
  71. package/dist/pack.d.ts +169 -0
  72. package/dist/pack.js +1 -0
  73. package/dist/publish.cjs +1 -0
  74. package/dist/publish.d.cts +96 -0
  75. package/dist/publish.d.ts +96 -0
  76. package/dist/publish.js +1 -0
  77. package/dist/scaffold.cjs +523 -0
  78. package/dist/scaffold.d.cts +182 -0
  79. package/dist/scaffold.d.ts +182 -0
  80. package/dist/scaffold.js +523 -0
  81. package/dist/server-runtime.cjs +1 -0
  82. package/dist/server-runtime.d.cts +63 -0
  83. package/dist/server-runtime.d.ts +63 -0
  84. package/dist/server-runtime.js +1 -0
  85. package/dist/server.cjs +1 -0
  86. package/dist/server.d.cts +1808 -0
  87. package/dist/server.d.ts +1808 -0
  88. package/dist/server.js +1 -0
  89. package/dist/testing.cjs +2 -0
  90. package/dist/testing.d.cts +92 -0
  91. package/dist/testing.d.ts +92 -0
  92. package/dist/testing.js +2 -0
  93. package/dist/vite.cjs +1 -0
  94. package/dist/vite.d.cts +133 -0
  95. package/dist/vite.d.ts +133 -0
  96. package/dist/vite.js +1 -0
  97. package/package.json +160 -4
  98. package/sql/access.sql +73 -0
@@ -0,0 +1,523 @@
1
+ "use strict";var u=Object.defineProperty;var i=(e,t)=>u(e,"name",{value:t,configurable:!0});var chunkC2OCOPZQ_cjs=require("./chunk-C2OCOPZQ.cjs"),fs=require("fs"),path=require("path"),PUBLISHED_SDK_PACKAGES={"@tego/plugin-access-ui":"@zhama/tego-plugin-access-ui","@tego/plugin-sdk":"@zhama/tego-plugin-sdk","@tego/ui":"@zhama/tego-ui"};function sdkSpec(e,t,n){if(n==="workspace")return"workspace:*";const o=PUBLISHED_SDK_PACKAGES[e];if(!o)throw new Error(`${e} is not a published SDK package`);return`npm:${o}@${t.hostApiRange}`}i(sdkSpec,"sdkSpec");var STANDALONE_PRETTIER={semi:!0,trailingComma:"es5",singleQuote:!0,printWidth:80,tabWidth:2,useTabs:!1,bracketSpacing:!0,bracketSameLine:!1,arrowParens:"avoid",endOfLine:"lf",quoteProps:"as-needed",jsxSingleQuote:!0,proseWrap:"preserve"},STANDALONE_GITIGNORE=`node_modules/
2
+ build/
3
+ dist/
4
+ frontend/dist/
5
+ backend/dist/
6
+ claw/dist/
7
+ coverage/
8
+ .turbo/
9
+ `,STANDALONE_PNPM_WORKSPACE=`allowBuilds:
10
+ esbuild: true
11
+ `,STANDALONE_TSCONFIG_FILES={"tsconfig.base.json":{$schema:"https://json.schemastore.org/tsconfig",compilerOptions:{strict:!0,useUnknownInCatchVariables:!0,allowJs:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,resolveJsonModule:!0,isolatedModules:!0,verbatimModuleSyntax:!0,declaration:!0,declarationMap:!0,esModuleInterop:!0,incremental:!1,noUncheckedIndexedAccess:!0,moduleDetection:"force",exactOptionalPropertyTypes:!0,target:"ES2022",lib:["ES2022","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler"},exclude:["node_modules"]},"tsconfig.frontend.json":{$schema:"https://json.schemastore.org/tsconfig",extends:"./tsconfig.base.json",compilerOptions:{target:"ES2020",lib:["ES2020","DOM","DOM.Iterable"],module:"ESNext",moduleResolution:"bundler",jsx:"react-jsx",noEmit:!1,declaration:!0,declarationMap:!0,outDir:"dist"}},"tsconfig.backend.json":{$schema:"https://json.schemastore.org/tsconfig",extends:"./tsconfig.base.json",compilerOptions:{target:"ES2022",lib:["ES2022"],module:"ESNext",moduleResolution:"bundler",types:["node"],noEmit:!1,declaration:!0,declarationMap:!0,outDir:"dist"}}};function tsconfigPreset(e,t){return t==="workspace"?`@tego/tsconfig/plugin-${e}.json`:`../tsconfig.${e}.json`}i(tsconfigPreset,"tsconfigPreset");var STANDALONE_ESLINT_CONFIG=`import js from '@eslint/js';
12
+ import prettierConfig from 'eslint-config-prettier';
13
+ import prettier from 'eslint-plugin-prettier';
14
+ import reactHooks from 'eslint-plugin-react-hooks';
15
+ import globals from 'globals';
16
+ import tseslint from 'typescript-eslint';
17
+
18
+ export default tseslint.config(
19
+ {
20
+ ignores: [
21
+ 'build/**',
22
+ 'dist/**',
23
+ '**/dist/**',
24
+ '.turbo/**',
25
+ 'node_modules/**',
26
+ ],
27
+ },
28
+ js.configs.recommended,
29
+ ...tseslint.configs.recommended,
30
+ {
31
+ files: ['**/*.{ts,tsx,js,mjs}'],
32
+ languageOptions: {
33
+ globals: { ...globals.browser, ...globals.node },
34
+ },
35
+ plugins: { 'react-hooks': reactHooks, prettier },
36
+ rules: {
37
+ ...reactHooks.configs.recommended.rules,
38
+ 'prettier/prettier': 'warn',
39
+ },
40
+ },
41
+ prettierConfig
42
+ );
43
+ `,STANDALONE_LINT_DEPENDENCIES={"@eslint/js":"^9.16.0","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.4","eslint-plugin-react-hooks":"^5.2.0",globals:"^15.13.0","typescript-eslint":"^8.17.0"};function standaloneRootFiles(){return[{path:".prettierrc.json",contents:json(STANDALONE_PRETTIER)},{path:".gitignore",contents:STANDALONE_GITIGNORE},{path:"pnpm-workspace.yaml",contents:STANDALONE_PNPM_WORKSPACE},...Object.entries(STANDALONE_TSCONFIG_FILES).map(([e,t])=>({path:e,contents:json(t)}))]}i(standaloneRootFiles,"standaloneRootFiles");var STANDALONE_TOOLCHAIN={"@typescript/native-preview":"7.0.0-dev.20260519.1",eslint:"^9.39.1",prettier:"^3.7.4",typescript:"^5.9.3"},SUPPORTED_LOCALES=["zh-CN","zh-TW","en","ja","ko","de","es","fr","it","pt"];function packageNameFor(e){return`@tego/plugin-${e.replace(/^tego-/,"")}`}i(packageNameFor,"packageNameFor");function keyNamespaceFor(e){return e.replace(/^tego-/,"")}i(keyNamespaceFor,"keyNamespaceFor");function json(e){return`${JSON.stringify(e,null,2)}
44
+ `}i(json,"json");function manifest(e){const t=keyNamespaceFor(e.id),n=e.parts.includes("frontend"),o=e.parts.includes("backend"),a=e.parts.includes("claw"),s={};n&&(s.frontend={icon:"puzzle",nav:[{key:"home",labelKey:`${t}.nav.home`,icon:"layout-dashboard",order:0,path:"/home"}]}),o&&(s.backend={limits:{memoryMb:256,requestTimeoutMs:15e3}}),a&&(s.claw={hooks:{}});const r={manifestVersion:1,id:e.id,version:"0.1.0",displayName:{"zh-CN":e.displayNameZh,en:e.displayNameEn},description:{"zh-CN":e.descriptionZh,en:e.descriptionEn},author:"Tego"};return(n||o)&&(r.hostApiRange=e.hostApiRange),r.scope=e.scope,o&&(r.declares={database:!0}),r.parts=s,{path:"tego.plugin.json",contents:json(r)}}i(manifest,"manifest");function rootPackageJson(e,t){const n=e.parts.includes("frontend"),o=e.parts.includes("backend"),a=e.parts.includes("claw"),s=["tsgo --noEmit -p shared/tsconfig.json"];n&&s.push("tsgo --noEmit -p frontend/tsconfig.json"),o&&s.push("tsgo --noEmit -p backend/tsconfig.json"),a&&s.push("tsgo --noEmit -p claw/tsconfig.json");const r={"@tego/plugin-sdk":sdkSpec("@tego/plugin-sdk",e,t),"@types/node":"^22.10.1",vitest:"^4.1.10"};t==="workspace"&&Object.assign(r,{"@tego/eslint-config":"workspace:*","@tego/tsconfig":"workspace:*"}),n&&Object.assign(r,{"@tanstack/react-query":"^5.62.7","@tego/ui":sdkSpec("@tego/ui",e,t),"@types/react":"^19.2.1","@types/react-dom":"^19.2.1","lucide-react":"^0.469.0",react:"^19.0.0","react-dom":"^19.0.0","react-i18next":"15.4.1",vite:"^6.3.5"}),o&&Object.assign(r,{"@types/pg":"^8.11.10",hono:"^4.10.7",pg:"^8.13.1"}),a&&(r.typebox="1.3.16"),t==="npm"&&Object.assign(r,STANDALONE_TOOLCHAIN,STANDALONE_LINT_DEPENDENCIES);const c={name:packageNameFor(e.id),version:"0.1.0",private:!0,type:"module",description:e.descriptionEn,scripts:{build:"tego-plugin-pack",watch:"tego-plugin-pack --watch","type-check":s.join(" && "),lint:"eslint .","lint:fix":"eslint . --fix",test:"vitest run"},devDependencies:sortedRecord(r)};return{path:"package.json",contents:json(c)}}i(rootPackageJson,"rootPackageJson");function sortedRecord(e){const t={};for(const n of Object.keys(e).sort())t[n]=e[n];return t}i(sortedRecord,"sortedRecord");function eslintConfig(e){return e==="npm"?{path:"eslint.config.js",contents:STANDALONE_ESLINT_CONFIG}:{path:"eslint.config.js",contents:`import baseConfig from '@tego/eslint-config';
45
+
46
+ export default [
47
+ ...baseConfig,
48
+ {
49
+ ignores: ['build/**', 'dist/**', '**/dist/**', '.turbo/**'],
50
+ },
51
+ ];
52
+ `}}i(eslintConfig,"eslintConfig");function sharedFiles(e){return[{path:"shared/tsconfig.json",contents:json({extends:tsconfigPreset("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1},include:["**/*.ts"]})},{path:"shared/index.ts",contents:`export {};
53
+ `}]}i(sharedFiles,"sharedFiles");function vitestConfig(){return{path:"vitest.config.ts",contents:`import { defineConfig } from 'vitest/config';
54
+
55
+ /**
56
+ * Node environment, no jsdom: components are not unit tested here by policy
57
+ * (\`ui-no-unit-tests.mdc\`). Pure logic in shared/frontend/claw is tested along
58
+ * with backend SQL against a throwaway schema through \`createTestPluginHost\`.
59
+ *
60
+ * \`fileParallelism: false\` because each database-backed suite creates and drops
61
+ * a schema in one shared Postgres; running them concurrently works until two
62
+ * pick the same clock-derived name.
63
+ */
64
+ export default defineConfig({
65
+ test: {
66
+ environment: 'node',
67
+ include: [
68
+ 'shared/**/*.test.ts',
69
+ 'backend/src/**/*.test.ts',
70
+ 'claw/src/**/*.test.ts',
71
+ 'frontend/src/**/*.test.ts',
72
+ ],
73
+ fileParallelism: false,
74
+ testTimeout: 20_000,
75
+ hookTimeout: 30_000,
76
+ },
77
+ });
78
+ `}}i(vitestConfig,"vitestConfig");function readme(e,t){if(t==="npm")return standaloneReadme(e);const n=packageNameFor(e.id);return{path:"README.md",contents:`# ${e.displayNameEn}
79
+
80
+ ${e.descriptionEn}
81
+
82
+ Generated by \`pnpm plugin:new\`. The workflow, gates and local-dev loop are in
83
+ [\`plugins/README.md\`](../README.md); the manifest and host API contracts are in
84
+ [\`packages/plugin-sdk/README.md\`](../../packages/plugin-sdk/README.md).
85
+
86
+ ## Gates
87
+
88
+ \`\`\`bash
89
+ pnpm -F ${n} lint
90
+ pnpm -F ${n} type-check
91
+ pnpm -F ${n} test
92
+ pnpm -F ${n} build
93
+ \`\`\`
94
+
95
+ Run \`pnpm install\` once after generating, so the new package links into the
96
+ workspace and \`@tego/plugin-sdk\` resolves.
97
+ `}}i(readme,"readme");function standaloneReadme(e){return{path:"README.md",contents:`# ${e.displayNameEn}
98
+
99
+ ${e.descriptionEn}
100
+
101
+ A Tego plugin. The SDK packages are installed from npm under their \`@tego/*\`
102
+ names (see \`devDependencies\`), so code reads the same as plugins built inside
103
+ the Tego repository. The manifest, host API contracts and local-dev loop are
104
+ documented in \`node_modules/@tego/plugin-sdk/README.md\`.
105
+
106
+ ## Gates
107
+
108
+ \`\`\`bash
109
+ npm install
110
+ npm run lint
111
+ npm run type-check
112
+ npm test
113
+ npm run build # writes the uploadable archive
114
+ \`\`\`
115
+
116
+ \`hostApiRange\` in \`tego.plugin.json\` and the SDK ranges in \`package.json\`
117
+ move together: raise both when the plugin starts using a newer host API.
118
+ `}}i(standaloneReadme,"standaloneReadme");function frontendFiles(e,t){const n=keyNamespaceFor(e.id),o=`import { readFile, readdir } from 'node:fs/promises';
119
+ import { resolve } from 'node:path';
120
+
121
+ import { tegoPluginFrontend } from '@tego/plugin-sdk/vite';
122
+
123
+ const config = tegoPluginFrontend({
124
+ entry: 'frontend/src/index.tsx',
125
+ outDir: 'frontend/dist',
126
+ pluginId: '${e.id}',
127
+ });
128
+
129
+ // The local override must serve translations from the same build as the UI.
130
+ config.plugins!.push({
131
+ name: '${e.id}-locales',
132
+ async buildStart() {
133
+ const directory = resolve(import.meta.dirname, 'i18n');
134
+ for (const name of await readdir(directory)) {
135
+ if (!name.endsWith('.json')) continue;
136
+ const file = resolve(directory, name);
137
+ this.addWatchFile(file);
138
+ this.emitFile({
139
+ type: 'asset',
140
+ fileName: \`i18n/\${name}\`,
141
+ source: await readFile(file, 'utf8'),
142
+ });
143
+ }
144
+ },
145
+ });
146
+ export default config;
147
+ `,a=json({extends:tsconfigPreset("frontend",t),compilerOptions:{rootDir:"..",noEmit:!0,declaration:!1,declarationMap:!1,types:["node"]},include:["src/**/*","vite.config.ts"],exclude:["node_modules"]}),s=`import { definePluginFrontend } from '@tego/plugin-sdk/frontend';
148
+ import { createRoot } from 'react-dom/client';
149
+
150
+ import { App } from './app';
151
+
152
+ /**
153
+ * The frontend part's entry.
154
+ *
155
+ * React comes from the shell (\`SHARED_MODULE_IDS\`), so this root shares the
156
+ * host's instance \u2014 but not its providers: a root mounted here sits outside the
157
+ * shell's tree, so anything provider-shaped this plugin needs it establishes
158
+ * itself. \`App\` is where that happens.
159
+ */
160
+ export default definePluginFrontend({
161
+ mount(el, ctx) {
162
+ const root = createRoot(el);
163
+ root.render(<App ctx={ctx} />);
164
+ return () => root.unmount();
165
+ },
166
+ });
167
+ `,r=`import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
168
+ import { pluginI18nNamespace } from '@tego/plugin-sdk';
169
+ import type { PluginHostContext } from '@tego/plugin-sdk/frontend';
170
+ import { EmptyState } from '@tego/ui';
171
+ import { Puzzle } from 'lucide-react';
172
+ import { Component, type ReactNode, useMemo } from 'react';
173
+ import { getI18n, I18nextProvider, useTranslation } from 'react-i18next';
174
+
175
+ /**
176
+ * The plugin's own error boundary.
177
+ *
178
+ * The shell's boundary sits outside this React root and never sees an error
179
+ * thrown inside it, so without one here a screen that throws renders as a blank
180
+ * rectangle that reads like the platform lost the page.
181
+ */
182
+ class PluginErrorBoundary extends Component<
183
+ { children: ReactNode; fallback: ReactNode },
184
+ { failed: boolean }
185
+ > {
186
+ state = { failed: false };
187
+ static getDerivedStateFromError() {
188
+ return { failed: true };
189
+ }
190
+ render() {
191
+ return this.state.failed ? this.props.fallback : this.props.children;
192
+ }
193
+ }
194
+
195
+ function Home() {
196
+ const { t } = useTranslation();
197
+ return (
198
+ <EmptyState
199
+ icon={<Puzzle />}
200
+ title={t('${n}.home.title')}
201
+ description={t('${n}.home.description')}
202
+ />
203
+ );
204
+ }
205
+
206
+ /**
207
+ * The plugin's root.
208
+ *
209
+ * Two things the shell would normally provide have to be re-established here,
210
+ * because context does not cross React roots: a query client, and the i18n
211
+ * namespace this plugin's bundle was merged under. The theme is *not*
212
+ * re-established \u2014 it rides on the document's root class that \`@tego/ui\` reads.
213
+ */
214
+ export function App({ ctx }: { ctx: PluginHostContext }) {
215
+ const queryClient = useMemo(() => new QueryClient(), []);
216
+ return (
217
+ <PluginErrorBoundary fallback={<EmptyState icon={<Puzzle />} title='\u2014' />}>
218
+ <QueryClientProvider client={queryClient}>
219
+ <I18nextProvider
220
+ i18n={getI18n()}
221
+ defaultNS={pluginI18nNamespace(ctx.pluginId)}
222
+ >
223
+ <Home />
224
+ </I18nextProvider>
225
+ </QueryClientProvider>
226
+ </PluginErrorBoundary>
227
+ );
228
+ }
229
+ `,c=[{path:"frontend/vite.config.ts",contents:o},{path:"frontend/tsconfig.json",contents:a},{path:"frontend/src/index.tsx",contents:s},{path:"frontend/src/app.tsx",contents:r}];for(const l of SUPPORTED_LOCALES){const d=l==="zh-CN",p={[n]:{nav:{home:d?"\u9996\u9875":"Home"},home:{title:d?e.displayNameZh:e.displayNameEn,description:d?e.descriptionZh:e.descriptionEn}}};c.push({path:`frontend/i18n/${l}.json`,contents:json(p)})}return c}i(frontendFiles,"frontendFiles");function backendFiles(e){return[{path:"backend/tsconfig.json",contents:json({extends:tsconfigPreset("backend",e),compilerOptions:{noEmit:!0,declaration:!1,declarationMap:!1,rootDir:".."},include:["src/**/*"],exclude:["node_modules"]})},{path:"backend/src/index.ts",contents:`import { serveBackend } from '@tego/plugin-sdk/server-runtime';
230
+
231
+ import { backend } from './app';
232
+
233
+ /**
234
+ * The backend part's entry, run as \`node index.js\` by the plugin runner.
235
+ *
236
+ * \`serveBackend\` owns the socket, the health path and the caller-context
237
+ * headers; everything below it is a plain fetch handler.
238
+ */
239
+ serveBackend(backend);
240
+ `},{path:"backend/src/app.ts",contents:`import { definePluginBackend } from '@tego/plugin-sdk/server';
241
+ import type { PluginHostApi, PluginUserContext } from '@tego/plugin-sdk/server';
242
+ import { Hono } from 'hono';
243
+
244
+ /**
245
+ * The backend's HTTP surface, reached at \`/plugin-api/<id>/\`.
246
+ *
247
+ * The gateway authenticates the caller, strips their credentials and forwards
248
+ * what is left here, so there is no auth code below: \`ctx\` is already a
249
+ * resolved identity. The host capabilities (\`db\`, \`storage\`, \`events\`, \`log\`)
250
+ * arrive per request and are handed to handlers through \`c.env\`.
251
+ */
252
+ interface Env {
253
+ Bindings: { host: PluginHostApi; user: PluginUserContext };
254
+ }
255
+
256
+ const app = new Hono<Env>();
257
+
258
+ app.get('/ping', c => c.json({ ok: true }));
259
+
260
+ app.get('/notes', async c => {
261
+ const notes = await c.env.host.db.query<{ id: string; body: string }>(
262
+ 'SELECT id, body FROM notes ORDER BY created_at DESC'
263
+ );
264
+ return c.json({ notes });
265
+ });
266
+
267
+ app.post('/notes', async c => {
268
+ const { body } = await c.req.json<{ body?: unknown }>();
269
+ if (typeof body !== 'string' || body.trim() === '') {
270
+ return c.json({ error: 'body is required' }, 400);
271
+ }
272
+ const [row] = await c.env.host.db.query<{ id: string }>(
273
+ 'INSERT INTO notes (author_id, body) VALUES ($1, $2) RETURNING id',
274
+ [c.env.user.userId, body]
275
+ );
276
+ return c.json({ id: row!.id }, 201);
277
+ });
278
+
279
+ export const backend = definePluginBackend({
280
+ async fetch(request, ctx, host) {
281
+ return app.fetch(request, { host, user: ctx });
282
+ },
283
+ });
284
+ `},{path:"backend/src/app.test.ts",contents:`import { dirname, join } from 'node:path';
285
+ import { fileURLToPath } from 'node:url';
286
+
287
+ import {
288
+ createTestPluginHost,
289
+ type TestPluginHost,
290
+ } from '@tego/plugin-sdk/testing';
291
+ import { afterAll, beforeEach, describe, expect, it } from 'vitest';
292
+
293
+ import { backend } from './app';
294
+
295
+ /**
296
+ * Business Cases
297
+ *
298
+ * The scaffold's backend is a starting point, so these guard the two things a
299
+ * generated plugin must get right before its author changes anything.
300
+ *
301
+ * SCAF-001 GET /ping answers 200 with { ok: true } \u2014 the liveness contract the
302
+ * platform's health check depends on.
303
+ * SCAF-002 POST a note then GET the list round-trips through the plugin's own
304
+ * schema: the write is visible on the next read, which only holds if
305
+ * the migration applied and the scoped db writes where it reads.
306
+ * SCAF-003 POST with an empty body is refused with 400, not written \u2014 the one
307
+ * negative branch the handler encodes.
308
+ */
309
+
310
+ const migrationsDir = join(
311
+ dirname(fileURLToPath(import.meta.url)),
312
+ '..',
313
+ 'migrations'
314
+ );
315
+
316
+ const CALLER = { userId: 'user_test', tenantId: 'tenant_test', roles: [] };
317
+
318
+ function request(
319
+ host: TestPluginHost['host'],
320
+ method: string,
321
+ path: string,
322
+ body?: unknown
323
+ ): Promise<Response> {
324
+ // Built conditionally rather than \`body: undefined\`: RequestInit.body has no
325
+ // undefined member, so spelling it out fails the plugin's own type-check.
326
+ const init: RequestInit =
327
+ body === undefined
328
+ ? { method }
329
+ : {
330
+ method,
331
+ headers: { 'content-type': 'application/json' },
332
+ body: JSON.stringify(body),
333
+ };
334
+ return backend.fetch(
335
+ new Request(\`http://plugin.internal\${path}\`, init),
336
+ CALLER,
337
+ host
338
+ );
339
+ }
340
+
341
+ describe('scaffold backend', () => {
342
+ let harness: TestPluginHost;
343
+
344
+ beforeEach(async () => {
345
+ harness = await createTestPluginHost({
346
+ pluginId: 'scaffold',
347
+ migrationsDir,
348
+ });
349
+ });
350
+ afterAll(() => harness?.close());
351
+
352
+ it('[SCAF-001] answers /ping with 200 and { ok: true }', async () => {
353
+ const res = await request(harness.host, 'GET', '/ping');
354
+ expect(res.status).toBe(200);
355
+ expect(await res.json()).toEqual({ ok: true });
356
+ });
357
+
358
+ it('[SCAF-002] a posted note is visible on the next read', async () => {
359
+ const created = await request(harness.host, 'POST', '/notes', {
360
+ body: 'first note',
361
+ });
362
+ expect(created.status).toBe(201);
363
+ const { id } = (await created.json()) as { id: string };
364
+ expect(id).toMatch(/./);
365
+
366
+ const listed = await request(harness.host, 'GET', '/notes');
367
+ expect(listed.status).toBe(200);
368
+ expect(await listed.json()).toEqual({
369
+ notes: [{ id, body: 'first note' }],
370
+ });
371
+ });
372
+
373
+ it('[SCAF-003] refuses an empty body with 400 and writes nothing', async () => {
374
+ const res = await request(harness.host, 'POST', '/notes', { body: ' ' });
375
+ expect(res.status).toBe(400);
376
+
377
+ const listed = await request(harness.host, 'GET', '/notes');
378
+ expect(await listed.json()).toEqual({ notes: [] });
379
+ });
380
+ });
381
+ `},{path:"backend/migrations/0001_init.sql",contents:`-- The backend part's schema.
382
+ --
383
+ -- Applied by the host once, inside a transaction whose \`search_path\` is this
384
+ -- install's own schema and nothing else. So nothing here is schema-qualified,
385
+ -- nothing references a platform table, and there is no \`tenant_id\`: one schema
386
+ -- per (tenant, plugin) install is the isolation boundary.
387
+ --
388
+ -- \`gen_random_uuid()\` is in \`pg_catalog\`, always on the path, so it needs no
389
+ -- extension. Migrations are append-only \u2014 correct a past one with a new file.
390
+
391
+ CREATE TABLE notes (
392
+ id text PRIMARY KEY DEFAULT gen_random_uuid(),
393
+ author_id text NOT NULL,
394
+ body text NOT NULL,
395
+ created_at timestamptz NOT NULL DEFAULT now()
396
+ );
397
+ `}]}i(backendFiles,"backendFiles");function clawFiles(e,t){const n=json({id:e.id,name:e.displayNameEn,description:e.descriptionEn,enabledByDefault:!0,activation:{onStartup:!0},configSchema:{type:"object",additionalProperties:!1,properties:{}}}),o=json({name:e.id,private:!0,type:"module",openclaw:{extensions:["./index.js"]},version:"0.1.0"}),a=`{
398
+ "extends": "${tsconfigPreset("backend",t)}",
399
+ "compilerOptions": {
400
+ "rootDir": "..",
401
+ "noEmit": true,
402
+ "declaration": false,
403
+ "declarationMap": false,
404
+ /*
405
+ * The claw part imports \`openclaw/plugin-sdk/*\`, resolved at runtime by
406
+ * claw's own alias map inside the container and not installable here. Types
407
+ * for those specifiers come from the local \`openclaw-plugin-sdk.d.ts\` shim.
408
+ */
409
+ "paths": {}
410
+ },
411
+ "include": ["src/**/*", "types/**/*"],
412
+ "exclude": ["node_modules"]
413
+ }
414
+ `,s=`/**
415
+ * The slice of claw's plugin SDK this extension uses.
416
+ *
417
+ * Declared here rather than imported because a plugin builds outside claw's
418
+ * workspace: \`openclaw/plugin-sdk/*\` is resolved at runtime by claw's own alias
419
+ * map inside the container, and claw ships no declaration files. Keep it narrow
420
+ * \u2014 only what \`claw/src\` calls \u2014 so it drifts on a small surface. Grow it as
421
+ * the plugin registers more; the full surface lives in \`plugins/tego-crm\`.
422
+ */
423
+
424
+ declare module 'openclaw/plugin-sdk/tego' {
425
+ /**
426
+ * \`api.logger\` takes one string. A second structured argument throws inside
427
+ * claw (\`message.startsWith is not a function\`) \u2014 serialize context into it.
428
+ */
429
+ export interface OpenClawPluginLogger {
430
+ info(message: string): void;
431
+ warn(message: string): void;
432
+ error(message: string): void;
433
+ debug?(message: string): void;
434
+ }
435
+
436
+ /** A tool's parameter schema \u2014 claw's own \`TSchema\` from typebox. */
437
+ export type OpenClawToolSchema = import('typebox').TSchema;
438
+
439
+ export interface OpenClawToolResult<TDetails = unknown> {
440
+ content: { type: 'text'; text: string }[];
441
+ details: TDetails;
442
+ }
443
+
444
+ export interface OpenClawAgentTool {
445
+ name: string;
446
+ label: string;
447
+ description: string;
448
+ parameters: OpenClawToolSchema;
449
+ execute(
450
+ toolCallId: string,
451
+ params: unknown,
452
+ signal?: AbortSignal
453
+ ): Promise<OpenClawToolResult<unknown>>;
454
+ }
455
+
456
+ export interface OpenClawPluginApi {
457
+ readonly id: string;
458
+ /**
459
+ * Absent or partial during claw's early registration phases \u2014 only fields
460
+ * carrying a \`default\` in \`configSchema\` are present then. Always guard.
461
+ */
462
+ readonly pluginConfig?: Record<string, unknown>;
463
+ readonly logger: OpenClawPluginLogger;
464
+ /**
465
+ * Refused unless every registered name appears in the manifest's
466
+ * \`contracts.tools\` \u2014 and the refusal happens inside the container, at
467
+ * startup, where nobody is watching.
468
+ */
469
+ registerTool(tool: OpenClawAgentTool): void;
470
+ }
471
+
472
+ export interface DefinePluginEntryOptions {
473
+ id: string;
474
+ name: string;
475
+ description: string;
476
+ register(api: OpenClawPluginApi): void | Promise<void>;
477
+ }
478
+
479
+ export function definePluginEntry(
480
+ options: DefinePluginEntryOptions
481
+ ): DefinePluginEntryOptions;
482
+ }
483
+
484
+ declare module 'openclaw/plugin-sdk/tego-api' {
485
+ /**
486
+ * The hub is installed by claw's own tego plugin at startup. A plugin that
487
+ * registers tools before it exists produces tool calls that always fail, so
488
+ * check this in \`register()\` and skip registration instead.
489
+ */
490
+ export function isTegoApiInstalled(): boolean;
491
+ }
492
+ `,r=`import {
493
+ definePluginEntry,
494
+ type OpenClawPluginApi,
495
+ } from 'openclaw/plugin-sdk/tego';
496
+ import { isTegoApiInstalled } from 'openclaw/plugin-sdk/tego-api';
497
+
498
+ /**
499
+ * The claw part's entry.
500
+ *
501
+ * \`register()\` runs once per agent during startup (dozens of times), so
502
+ * everything below it must be idempotent and free of global side effects. Tools
503
+ * reach the plugin's own backend through the tego API hub, which claw installs
504
+ * at startup \u2014 so if it is absent, skip registration rather than register tools
505
+ * that would fail on every call.
506
+ */
507
+ export default definePluginEntry({
508
+ id: '${e.id}',
509
+ name: '${e.displayNameEn}',
510
+ description: '${e.descriptionEn}',
511
+ register(api: OpenClawPluginApi) {
512
+ if (!isTegoApiInstalled()) {
513
+ api.logger.warn(
514
+ '${e.id}: tego-api hub not installed \u2014 tools will not be registered'
515
+ );
516
+ return;
517
+ }
518
+ // Register tools here with \`api.registerTool(...)\`. Each tool's name must
519
+ // also appear in \`contracts.tools\` in \`openclaw.plugin.json\`, or claw
520
+ // refuses the registration.
521
+ },
522
+ });
523
+ `;return[{path:"claw/openclaw.plugin.json",contents:n},{path:"claw/package.json",contents:o},{path:"claw/tsconfig.json",contents:a},{path:"claw/types/openclaw-plugin-sdk.d.ts",contents:s},{path:"claw/src/index.ts",contents:r}]}i(clawFiles,"clawFiles");function buildPluginFiles(e,{dependencies:t="workspace"}={}){const n=[manifest(e),rootPackageJson(e,t),eslintConfig(t),...sharedFiles(t),vitestConfig(),readme(e,t)];return t==="npm"&&n.push(...standaloneRootFiles()),e.parts.includes("frontend")&&n.push(...frontendFiles(e,t)),e.parts.includes("backend")&&n.push(...backendFiles(t)),e.parts.includes("claw")&&n.push(...clawFiles(e,t)),n}i(buildPluginFiles,"buildPluginFiles");var PLUGIN_ID_PATTERN=/^[a-z0-9][a-z0-9._-]*$/,PLUGIN_PARTS=["frontend","backend","claw"],PLUGIN_SCOPES=["avatar","tenant"],PLUGIN_ID_HINT="lowercase letters and digits, may contain . _ -, starting with a letter or digit";function validatePluginAnswers(e,t){if(typeof e!="object"||e===null||Array.isArray(e))throw new Error("answers must be a JSON object");const n=e,o=i(c=>{const l=n[c];if(typeof l!="string"||l.trim()==="")throw new Error(`${c} is required`);return l.trim()},"text"),a=o("id");if(!PLUGIN_ID_PATTERN.test(a))throw new Error(`id must be ${PLUGIN_ID_HINT}`);const s=n.parts;if(!Array.isArray(s)||s.length===0||s.some(c=>!PLUGIN_PARTS.includes(c)))throw new Error(`parts must be a non-empty list of ${PLUGIN_PARTS.join(", ")}`);const r=n.scope??"avatar";if(!PLUGIN_SCOPES.includes(r))throw new Error(`scope must be one of ${PLUGIN_SCOPES.join(", ")}`);return{id:a,displayNameZh:o("displayNameZh"),displayNameEn:o("displayNameEn"),descriptionZh:o("descriptionZh"),descriptionEn:o("descriptionEn"),parts:[...new Set(s)],scope:r,hostApiRange:t}}i(validatePluginAnswers,"validatePluginAnswers");function writePlugin(e,t){return writePluginDirectory(path.join(e,"plugins"),t,buildPluginFiles(t))}i(writePlugin,"writePlugin");function writeStandalonePlugin(e,t){return writePluginDirectory(e,t,buildPluginFiles(t,{dependencies:"npm"}))}i(writeStandalonePlugin,"writeStandalonePlugin");function writePluginDirectory(e,t,n){const o=path.join(e,t.id);if(fs.existsSync(o))throw new Error(`${o} already exists \u2014 pick another id or remove it first`);fs.mkdirSync(e,{recursive:!0});const a=fs.mkdtempSync(path.join(e,`.${t.id}-`));try{for(const s of n){const r=path.join(a,s.path);fs.mkdirSync(path.dirname(r),{recursive:!0}),fs.writeFileSync(r,s.contents)}fs.renameSync(a,o)}catch(s){throw fs.rmSync(a,{recursive:!0,force:!0}),s}return{directory:o,files:n.map(s=>s.path)}}i(writePluginDirectory,"writePluginDirectory");function createStandalonePlugin(e,t){let n;try{n=JSON.parse(e)}catch(a){throw new Error(`answers must be JSON: ${a instanceof Error?a.message:String(a)}`)}const o=validatePluginAnswers(n,`^${chunkC2OCOPZQ_cjs.HOST_API_VERSION}`);return writeStandalonePlugin(path.resolve(t.cwd,t.parent),o)}i(createStandalonePlugin,"createStandalonePlugin"),exports.PLUGIN_ID_HINT=PLUGIN_ID_HINT,exports.PLUGIN_ID_PATTERN=PLUGIN_ID_PATTERN,exports.PLUGIN_PARTS=PLUGIN_PARTS,exports.PLUGIN_SCOPES=PLUGIN_SCOPES,exports.PUBLISHED_SDK_PACKAGES=PUBLISHED_SDK_PACKAGES,exports.STANDALONE_TSCONFIG_FILES=STANDALONE_TSCONFIG_FILES,exports.buildPluginFiles=buildPluginFiles,exports.createStandalonePlugin=createStandalonePlugin,exports.packageNameFor=packageNameFor,exports.validatePluginAnswers=validatePluginAnswers,exports.writePlugin=writePlugin,exports.writeStandalonePlugin=writeStandalonePlugin;