@zhama/tego-plugin-sdk 1.36.2 → 1.37.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 (93) hide show
  1. package/agent/RULES.md +123 -0
  2. package/agent/guides/access.md +39 -0
  3. package/agent/guides/backend.md +76 -0
  4. package/agent/guides/claw.md +18 -0
  5. package/agent/guides/desktop-web.md +48 -0
  6. package/agent/guides/frontend.md +83 -0
  7. package/agent/guides/host-api-versions.md +34 -0
  8. package/agent/guides/i18n.md +17 -0
  9. package/agent/guides/manifest.md +26 -0
  10. package/agent/guides/migrations.md +17 -0
  11. package/agent/guides/storage.md +18 -0
  12. package/agent/guides/testing.md +17 -0
  13. package/agent/guides/ui.md +22 -0
  14. package/agent/reference/plugin-sdk-api.md +561 -0
  15. package/bin/tego-plugin-pack.mjs +1 -1
  16. package/bin/tego-plugin.mjs +4 -2
  17. package/dist/chunk-2NTAWYCG.js +1 -0
  18. package/dist/chunk-2P6UEAPV.cjs +1 -0
  19. package/dist/chunk-45Y4CR4R.cjs +1 -0
  20. package/dist/{chunk-PWSTCQRN.cjs → chunk-4LKRXDYY.cjs} +13 -12
  21. package/dist/{chunk-7XHF3YSK.js → chunk-56QOZQTY.js} +1 -1
  22. package/dist/{chunk-C2OCOPZQ.cjs → chunk-6HNG5OZI.cjs} +1 -1
  23. package/dist/{chunk-2ORO36BY.js → chunk-DEGQDJF6.js} +1 -1
  24. package/dist/chunk-EIVA5BR6.js +1 -0
  25. package/dist/{chunk-P2Q2AKWP.js → chunk-FCYAAOHC.js} +17 -16
  26. package/dist/chunk-HZ4YWRYX.cjs +5 -0
  27. package/dist/chunk-I5Z3ECC4.js +5 -0
  28. package/dist/chunk-INCA3GFW.js +1 -0
  29. package/dist/chunk-IVJECPGG.js +1 -0
  30. package/dist/{chunk-KMN5RHLX.cjs → chunk-MWMGJJ74.cjs} +1 -1
  31. package/dist/chunk-N4SXZZPY.cjs +1 -0
  32. package/dist/chunk-SKNPNPJP.cjs +1 -0
  33. package/dist/dev.cjs +88 -5
  34. package/dist/dev.d.cts +37 -1
  35. package/dist/dev.d.ts +37 -1
  36. package/dist/dev.js +91 -8
  37. package/dist/doctor.cjs +8 -0
  38. package/dist/doctor.d.cts +62 -0
  39. package/dist/doctor.d.ts +62 -0
  40. package/dist/doctor.js +8 -0
  41. package/dist/frontend.d.cts +33 -1
  42. package/dist/frontend.d.ts +33 -1
  43. package/dist/hono.cjs +1 -0
  44. package/dist/hono.d.cts +84 -0
  45. package/dist/hono.d.ts +84 -0
  46. package/dist/hono.js +1 -0
  47. package/dist/index.cjs +1 -1
  48. package/dist/index.d.cts +6 -1
  49. package/dist/index.d.ts +6 -1
  50. package/dist/index.js +1 -1
  51. package/dist/manifest.cjs +1 -1
  52. package/dist/manifest.d.cts +55 -2
  53. package/dist/manifest.d.ts +55 -2
  54. package/dist/manifest.js +1 -1
  55. package/dist/migrations-sync.cjs +1 -0
  56. package/dist/migrations-sync.d.cts +20 -0
  57. package/dist/migrations-sync.d.ts +20 -0
  58. package/dist/migrations-sync.js +1 -0
  59. package/dist/pack.cjs +1 -1
  60. package/dist/pack.d.cts +5 -0
  61. package/dist/pack.d.ts +5 -0
  62. package/dist/pack.js +1 -1
  63. package/dist/publish.cjs +1 -1
  64. package/dist/publish.js +1 -1
  65. package/dist/react.cjs +1 -0
  66. package/dist/react.d.cts +101 -0
  67. package/dist/react.d.ts +101 -0
  68. package/dist/react.js +1 -0
  69. package/dist/scaffold.cjs +671 -17
  70. package/dist/scaffold.d.cts +42 -18
  71. package/dist/scaffold.d.ts +42 -18
  72. package/dist/scaffold.js +673 -19
  73. package/dist/server-runtime.cjs +1 -1
  74. package/dist/server-runtime.d.cts +27 -2
  75. package/dist/server-runtime.d.ts +27 -2
  76. package/dist/server-runtime.js +1 -1
  77. package/dist/testing.cjs +6 -2
  78. package/dist/testing.d.cts +19 -1
  79. package/dist/testing.d.ts +19 -1
  80. package/dist/testing.js +6 -2
  81. package/dist/verify-FF2RD2MV.cjs +1 -0
  82. package/dist/verify-W7ZTUCO4.js +1 -0
  83. package/dist/vite.cjs +1 -1
  84. package/dist/vite.d.cts +27 -1
  85. package/dist/vite.d.ts +27 -1
  86. package/dist/vite.js +1 -1
  87. package/package.json +31 -3
  88. package/dist/chunk-3WA3M347.cjs +0 -1
  89. package/dist/chunk-5CU33JVY.js +0 -1
  90. package/dist/chunk-7K37PVGV.js +0 -5
  91. package/dist/chunk-BTHXQWHT.js +0 -1
  92. package/dist/chunk-ROFUQLOZ.cjs +0 -1
  93. package/dist/chunk-W7C357A6.cjs +0 -5
package/agent/RULES.md ADDED
@@ -0,0 +1,123 @@
1
+ # Tego plugin rules
2
+
3
+ A Tego plugin runs inside the Tego platform: its frontend inside the **web app** and the
4
+ **desktop app** (Tauri), its backend in a platform-managed container, its claw part inside an
5
+ employee's container. These rules are what makes a plugin load, pass upload, and behave the same
6
+ in both shells. Read the guide named in each section before writing that kind of code.
7
+
8
+ Rules with an id are checked by `tego-plugin doctor` (`--json` for tools). A line can opt out with
9
+ `tego-doctor-ignore <RULE-ID>: <reason>` on it or on the line above; give a real reason.
10
+
11
+ ## Working loop
12
+
13
+ 1. Read the relevant guide (`agent/guides/*.md`) and the code you change.
14
+ 2. Make focused changes; reuse the SDK's helpers (`@tego/plugin-sdk/react`, `/hono`, `/testing`)
15
+ instead of writing your own versions.
16
+ 3. Run the gates: `tego-plugin doctor`, then lint, type-check, test, build. Fix everything they
17
+ report; never weaken a check or a test to make it pass.
18
+ 4. Every new user-visible string goes into all ten locales.
19
+
20
+ ## Manifest and packaging — guides/manifest.md, guides/host-api-versions.md
21
+
22
+ - **TP-MAN-001** `tego.plugin.json` is valid: `manifestVersion: 1`, stable `id`, semver `version`
23
+ (never lower it), `scope`, at least one part, non-overlapping part directories.
24
+ - **TP-MAN-002** `hostApiRange` (`^X.Y.Z`) admits every host capability the code uses. Declare the
25
+ lowest version you need, and raise it when you use something newer (table in the guide) — or
26
+ feature-detect it (`if (ctx.download) … else …`) to keep supporting older shells.
27
+ - In-plugin routes have no dotted segment (`/p/x/report.pdf`): the desktop app serves a reloaded
28
+ URL like that as a file, not your page. (Nav paths are already refused by the manifest schema.)
29
+ - **TP-PKG-001** SDK packages are installed as npm aliases (`"@tego/ui": "npm:@zhama/tego-ui@^1.37.0"`)
30
+ or workspace links. On npm, a plain `@tego/*` name belongs to another project.
31
+ - **TP-PKG-002** `package.json` has the four gates: `lint`, `type-check` (tsgo), `test` (vitest),
32
+ `build` (`tego-plugin-pack`).
33
+ - Never import `@tego/shared`, host app code, or anything not published to plugins.
34
+
35
+ ## Running in the web app and the desktop app — guides/desktop-web.md
36
+
37
+ The two shells share one frontend but differ in CSP, origin, network proxy and file handling. Code
38
+ that works in a browser tab can fail on desktop, and the reverse.
39
+
40
+ - **TP-DESK-001** No `eval` / `new Function` (desktop CSP: `script-src 'self' 'wasm-unsafe-eval'`),
41
+ including in libraries you add.
42
+ - **TP-DESK-002** Never build addresses from `location.origin` (it is `tauri://localhost` on
43
+ desktop). Use `ctx.hostUrl(path)` for host paths and `ctx.publicPagesUrl` for share links.
44
+ - **TP-DESK-003** Open external links with `ctx.openExternal(url)`, not `window.open`.
45
+ - **TP-DESK-004** Save files with `ctx.download({ path | blob, fileName })`, not `<a download>`.
46
+ - **TP-DESK-005** Do not stream from the frontend: desktop buffers `ctx.api` responses whole and caps
47
+ them at 100 MB. Large files go through storage paths; live updates come from `ctx.events`.
48
+ - **TP-DESK-006** Copy text with `ctx.copyText(text)`.
49
+ - **TP-WEB-001** The frontend calls no third-party URL (the web CSP only allows the platform). The
50
+ backend calls it, with the host declared in `parts.backend.egress`.
51
+ - **TP-WEB-002** No remote or injected scripts; all code ships in the bundle.
52
+ - No cookie-based auth: `ctx.api` carries the credentials in both shells.
53
+
54
+ ## Frontend — guides/frontend.md
55
+
56
+ - **TP-UI-004** The frontend has its own error boundary (`PluginErrorBoundary`); the shell's cannot
57
+ see into your React root.
58
+ - Root order: `I18nextProvider` (plugin namespace) → error boundary → `QueryClientProvider` →
59
+ `AccessProvider` (with access) → `HostProvider`; the root element carries the scope class.
60
+ - Route with `ctx.route` / `ctx.navigate` under `ctx.basePath`; the page is not remounted when the
61
+ in-plugin path changes.
62
+ - Call your backend only through `ctx.api` (via `createApiClient`), keeping the error body.
63
+
64
+ ## UI and design — guides/ui.md
65
+
66
+ - **TP-UI-001** Colors only through token classes (`bg-card`, `text-muted-foreground`, `bg-primary`…):
67
+ no hex, `rgb()`, palette classes (`bg-blue-500`, `text-white`) or `dark:` overrides.
68
+ - **TP-UI-002** Use `@tego/ui` components; never import `@radix-ui/*` directly.
69
+ - **TP-UI-003** With `tegoPluginTailwind`, the root element (and portalled content, or use
70
+ `tegoPluginScopeRoot`) carries `className="<pluginId>"`, or none of your utilities apply.
71
+ - Status uses text or an icon, not color alone; machine data (ids, numbers, times) uses
72
+ `font-mono tabular-nums`.
73
+
74
+ ## i18n — guides/i18n.md
75
+
76
+ - **TP-I18N-001** All ten locales — `zh-CN zh-TW en ja ko de es fr it pt` — in
77
+ `frontend/i18n/<locale>.json`, with exactly the keys of `zh-CN`; every nav `labelKey` exists.
78
+ - **TP-I18N-002** Every value keeps the `{{placeholders}}` of `zh-CN` and is not empty.
79
+ - **TP-I18N-003** Every literal `t('…')` key exists.
80
+ - **TP-I18N-004** Say 数字员工 / Employee through `{{avatarTerm}}`; never 数字分身 / digital avatar.
81
+ - Keys live under one root named after the plugin (`notes.home.title`); don't copy `tegoAccess.*`.
82
+
83
+ ## Backend — guides/backend.md, guides/storage.md
84
+
85
+ - **TP-BE-001** The caller is the verified `ctx` that `serveBackend` hands you. Never read the raw
86
+ `x-tego-plugin-*` headers; a user id in a path or body names a target, never the caller.
87
+ - **TP-BE-005** Log through `host.log`, never `console`.
88
+ - Every error becomes JSON (`pluginErrorHandler`): coded 4xx for the caller's mistakes, a generic
89
+ 500 that never echoes SQL; host 429s pass through with `Retry-After`.
90
+ - Validate every body and query with zod (`readBody` / `readQuery`).
91
+ - Data lives in your schema only (`host.db`); configuration in `host.config.current()`, never env.
92
+ - Storage: `put` up to 25 MiB; larger files upload from the browser to `uploadPath` and are
93
+ verified with `head`. Download paths are `signedPath` / `managedPath`, opened via `ctx.hostUrl`.
94
+ - Background work in `start()` never acts as a user.
95
+
96
+ ## Migrations — guides/migrations.md
97
+
98
+ - **TP-BE-003** `backend/migrations/<ordinal>_<name>.sql`, unique ordinals.
99
+ - **TP-BE-004** Committed migrations are append-only: never edit or delete one; add a new file.
100
+ - Generate them from the drizzle schema (`tego-plugin migrations sync`); hand-write only what
101
+ drizzle cannot express (functions, triggers, RLS).
102
+
103
+ ## Access control — guides/access.md
104
+
105
+ - **TP-BE-002** Never decide by host role names (`roles.includes('admin')`); declare permissions in
106
+ the catalog (`hostRoleBindings`) and ask the engine.
107
+ - **TP-ACC-001** With `access.catalog`, every route carries exactly one policy, and a test calls
108
+ `assertRouteCoverage(app)`.
109
+ - **TP-ACC-002** With `access.catalog`, `start()` calls `access.reconcile(host)`.
110
+ - Lists use `session.filter`, single records `policy.resource`; what the frontend hides is
111
+ presentation only.
112
+
113
+ ## Claw part — guides/claw.md
114
+
115
+ - **TP-CLAW-002** Each `claw/skills/<name>/SKILL.md` declares `name: <name>`.
116
+ - Tools are listed in `contracts.tools`; the claw config schema lives in `openclaw.plugin.json`,
117
+ never in the manifest.
118
+
119
+ ## Tests — guides/testing.md
120
+
121
+ - **TP-TEST-001** The plugin has tests: backend routes against a real database
122
+ (`createTestPluginHost`), pure frontend logic in `.ts` files. No component tests.
123
+ - Each test file starts with a Business Cases list; each `it` title starts with its case id.
@@ -0,0 +1,39 @@
1
+ # Access control
2
+
3
+ Accounts come from the host; what someone may do _inside_ the plugin is declared in your catalog,
4
+ stored in your own tables and decided by the SDK. Design: `docs/design/plugin-access.md` in tego-os.
5
+
6
+ Add it as soon as some users may do something others may not:
7
+
8
+ 1. `"access": { "catalog": "shared/access.json" }` in the manifest; `hostApiRange` ≥ `^1.22.0`;
9
+ `directory.read` permission when you use `unit` principals.
10
+ 2. Catalog: permissions (with `implies`, and `agent.requires` for what employees may use), scopes,
11
+ built-in roles, `hostRoleBindings` (how host roles map to yours), default role, `menus`
12
+ (nav key → permissions).
13
+ 3. Backend:
14
+ ```ts
15
+ export const access = createAccess({
16
+ catalog: accessCatalogFromJson(catalogJson),
17
+ });
18
+ const { policy, routes } = accessHono({ session: c => c.env.access });
19
+ app.get('/records', policy.permission('record.view'), list);
20
+ app.delete(
21
+ '/records/:id',
22
+ policy.resource('record.delete', loadRecord, { conceal: true }),
23
+ remove
24
+ );
25
+ app.route('/', routes()); // /access/me, /access/roles, …
26
+ // start(host): await access.reconcile(host) (TP-ACC-002)
27
+ ```
28
+ - **Exactly one policy per route** — `public()` (only for `publicRoutes`), `authenticated()`,
29
+ `permission()`, `resource()` — and a test: `assertRouteCoverage(app, { publicRoutes })` (TP-ACC-001).
30
+ - Lists: `session.filter('record.view', { alias: 'r' })` gives the SQL condition; single records:
31
+ `policy.resource` / `authorize`. Never write a second copy of scope logic in SQL.
32
+ - Tables: re-export `@tego/plugin-sdk/access/drizzle` in your schema and generate migrations.
33
+ - Derived data (compiled rules, caches) updates in `hooks.onChange`, in the same transaction.
34
+ 4. Frontend: `<AccessProvider api={ctx.api}>` under your `QueryClientProvider`; gate with
35
+ `useAccess()`, `<Can>`, `filterNav`; embed `<AccessSettings />` in your settings page;
36
+ install `@tego/plugin-access-ui` under the npm alias.
37
+
38
+ Never (TP-BE-002): check host role names (`ctx.roles.includes('admin')`), invent `isAdmin`, keep a
39
+ parallel role table, let background jobs act as a user, or treat frontend gating as the boundary.
@@ -0,0 +1,76 @@
1
+ # Backend part
2
+
3
+ ```ts
4
+ // backend/src/index.ts
5
+ import { serveBackend } from '@tego/plugin-sdk/server-runtime';
6
+ import { backend } from './app';
7
+ serveBackend(backend);
8
+ ```
9
+
10
+ `definePluginBackend({ fetch, start?, stop?, fetchPublic?, authorizeStorage? })`. `fetch(request, ctx, host)`
11
+ receives a request whose identity the gateway signed and `serveBackend` verified: `ctx.userId`,
12
+ `ctx.tenantId`, `ctx.roles`, and for claw calls `ctx.avatarId` / `ctx.delegation`.
13
+
14
+ ## Identity (TP-BE-001)
15
+
16
+ The caller is `ctx`, always. A `userId` in a path or body names a _target_ (whose role to change),
17
+ never who is asking. Do not parse `x-tego-plugin-*` headers yourself.
18
+
19
+ ## Routes with Hono
20
+
21
+ ```ts
22
+ import { Hono } from 'hono';
23
+ import {
24
+ HttpError,
25
+ jsonNotFound,
26
+ pluginErrorHandler,
27
+ readBody,
28
+ } from '@tego/plugin-sdk/hono';
29
+ import { z } from 'zod';
30
+
31
+ type Env = {
32
+ Bindings: {
33
+ host: PluginHostApi;
34
+ ctx: PluginUserContext;
35
+ access: AccessSession;
36
+ };
37
+ };
38
+ const app = new Hono<Env>();
39
+ app.onError(pluginErrorHandler()); // access denials, HttpError, host 429 → JSON; other errors logged via host.log, generic 500
40
+ app.notFound(jsonNotFound);
41
+
42
+ const NoteInput = z.object({ body: z.string().trim().min(1).max(4000) });
43
+ app.post('/notes', policy.permission('note.create'), async c => {
44
+ const { body } = await readBody(c, NoteInput); // 400 { error: { code: 'invalid_body', path } }
45
+ const [row] = await c.env.host.db.query<{ id: string }>(
46
+ 'INSERT INTO notes (author_id, body) VALUES ($1, $2) RETURNING id',
47
+ [c.env.ctx.userId, body]
48
+ );
49
+ return c.json({ id: row!.id }, 201);
50
+ });
51
+
52
+ export const backend = definePluginBackend({
53
+ fetch: (request, ctx, host) =>
54
+ app.fetch(request, { host, ctx, access: access.forRequest(ctx, host) }),
55
+ start: async host => {
56
+ await access.reconcile(host);
57
+ maintenance.start(host);
58
+ },
59
+ stop: () => maintenance.stop(),
60
+ });
61
+ ```
62
+
63
+ - Throw `new HttpError(409, 'note_locked', 'This note is locked')` for the caller's mistakes; the
64
+ frontend translates `code`.
65
+ - Never return SQL or stack traces; `pluginErrorHandler` logs them with `host.log.error` (TP-BE-005).
66
+ - Query only the columns you need; parameterize everything.
67
+ - Data: only your schema through `host.db` (one schema per install). Config: `host.config.current()`;
68
+ `host.config.refresh()` after a credential rotation. Never read config from env.
69
+ - Changes users should see live: `host.events.publish('changed', { kind })` (no personal data — the
70
+ stream is tenant-wide); `changedEventsMiddleware()` does it for every successful write.
71
+ - `start()` runs installation-level maintenance (timers, queues) as trusted system code: it never
72
+ acts as a user. Keep each step isolated so one failure does not stop the others
73
+ (`createMaintenance` from `@tego/plugin-sdk/server-runtime`).
74
+ - Outbound HTTP only to hosts in `parts.backend.egress`.
75
+
76
+ See storage.md for files and access.md for permissions.
@@ -0,0 +1,18 @@
1
+ # Claw part
2
+
3
+ Anything inside a conversation (tools, skills, cards) is the claw part, running inside the employee's
4
+ OpenClaw container.
5
+
6
+ - `claw/openclaw.plugin.json`: `id`, `contracts.tools` (every tool you register), `configSchema`
7
+ (closed object; integer min/max/default; `uiHints` labels in all ten locales), audit notes for
8
+ tools. The packer stamps the plugin's version into it and `claw/package.json`.
9
+ - `claw/src/index.ts`: `definePluginEntry`; register tools only when the Tego API is installed;
10
+ `register()` must be idempotent and tolerate early-phase config. `api.logger` takes one string.
11
+ - Call your backend from a tool with the Tego API's `request({ path: '/api/v1/tego-os/plugin-api/<id>/…',
12
+ executionContext: 'current-user' })`; return 400/404/409/422 as values the model can read, throw
13
+ for anything else with a message written for the model.
14
+ - Tool results: JSON in both `content` and `details`.
15
+ - Skills (TP-CLAW-002): `claw/skills/<prefix>-<name>/SKILL.md` with `name: <prefix>-<name>` and a
16
+ `description`; every tool a skill mentions exists. `assertClawSkills(skillsDir, toolNames)` from
17
+ `@tego/plugin-sdk/testing` tests it.
18
+ - Config marked secret comes back as `__OPENCLAW_REDACTED__` in some reads; treat it as unusable.
@@ -0,0 +1,48 @@
1
+ # Running in both shells: web and desktop
2
+
3
+ The plugin's frontend is loaded by two shells built from one codebase:
4
+
5
+ | | Web app (browser) | Desktop app (Tauri v2) |
6
+ | --------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | Origin | the platform's https origin | `tauri://localhost` (macOS), `https://tauri.localhost` (Windows) |
8
+ | Script CSP | nonce + `'strict-dynamic'` | `'self' 'wasm-unsafe-eval'` — no `eval`, no `new Function`, no `blob:` scripts |
9
+ | `connect-src` | the platform only (plus its fixed CDNs) | any http(s) |
10
+ | `ctx.api` transport | browser fetch to the platform | a Rust protocol proxy that **buffers the whole response**, caps it at **100 MB** (413), strips `accept-encoding`, and answers 403 `LICENSE_INVALID` when the desktop licence lapses |
11
+ | Reloaded URL ending in `name.ext` | the SPA page | proxied to the server as a file |
12
+ | `window.open`, `<a download>` | browser behaviour | not reliably handled by the webview |
13
+ | Cookies | not used | not available |
14
+
15
+ `ctx.platform` tells you which one you run in (host API 1.37), but **write code that is correct in
16
+ both** rather than branching on it.
17
+
18
+ ## Do / don't
19
+
20
+ | Instead of | Use | Rule |
21
+ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------- |
22
+ | `eval`, `new Function`, libraries that compile strings (some template engines, older chart libs) | precompiled code; check a new dependency with `tego-plugin doctor` and the preview's desktop mode | TP-DESK-001 |
23
+ | `` `${location.origin}/…` `` | `ctx.hostUrl(path)` for host paths, `ctx.publicPagesUrl` for share/QR links | TP-DESK-002 |
24
+ | `window.open(url)` | `ctx.openExternal(url)` | TP-DESK-003 |
25
+ | `<a download>`, `a.download = …`, `URL.createObjectURL` + click | `ctx.download({ path })` for a stored file, `ctx.download({ blob, fileName })` for generated content | TP-DESK-004 |
26
+ | `response.body.getReader()`, `EventSource`, NDJSON over `ctx.api` | `ctx.events.subscribe` for pushed changes; storage paths for big files | TP-DESK-005 |
27
+ | `navigator.clipboard.writeText` | `ctx.copyText(text)` | TP-DESK-006 |
28
+ | `fetch('https://api.example.com/…')` from the frontend | a backend route that calls it, with `parts.backend.egress: ["api.example.com"]` | TP-WEB-001 |
29
+ | `<script src>`, `import('https://…')`, CDN loaders | bundle the dependency | TP-WEB-002 |
30
+ | `/p/<id>/report.pdf` style routes | `/p/<id>/report?file=…` | (manifest schema for nav) |
31
+
32
+ ## Files
33
+
34
+ - **Upload**: ask your backend for a grant (`host.storage.uploadPath(key, { maxBytes, ttlSeconds })`),
35
+ `fetch(ctx.hostUrl(grant.path), { method: 'PUT', body: file })`, then tell the backend to register it
36
+ (it checks with `host.storage.head(key)`). Never send large files through `ctx.api`.
37
+ - **Download**: the backend returns `signedPath` / `managedPath`; the frontend calls
38
+ `ctx.download({ path, fileName })`.
39
+ - **Generated content** (CSV, a template): `ctx.download({ blob, fileName })`.
40
+
41
+ Before host API 1.37 (plugins that cannot raise `hostApiRange` yet), feature-detect:
42
+ `ctx.download ? ctx.download(...) : legacyDownload(...)`, and mark the legacy line with
43
+ `tego-doctor-ignore TP-DESK-004: <reason>`.
44
+
45
+ ## Checking
46
+
47
+ `tego-plugin dev --target desktop` serves the preview with the desktop CSP and a buffering proxy, and
48
+ reports CSP violations; try every page there before releasing.
@@ -0,0 +1,83 @@
1
+ # Frontend part
2
+
3
+ The entry (`frontend/src/index.tsx`) default-exports `definePluginFrontend({ mount(el, ctx) })` and
4
+ returns a function that fully tears down. The shell shares `react`, `react-dom`, `@tego/ui`,
5
+ `react-i18next` and `lucide-react` with you (`SHARED_MODULE_IDS`); everything else you bundle.
6
+
7
+ ## Root
8
+
9
+ ```tsx
10
+ import { getI18n, I18nextProvider } from 'react-i18next';
11
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
12
+ import { pluginI18nNamespace } from '@tego/plugin-sdk';
13
+ import { HostProvider, PluginErrorBoundary } from '@tego/plugin-sdk/react';
14
+
15
+ export function App({ ctx }: { ctx: PluginHostContext }) {
16
+ const [queryClient] = useState(
17
+ () =>
18
+ new QueryClient({ defaultOptions: { queries: { staleTime: 10_000 } } })
19
+ );
20
+ return (
21
+ <I18nextProvider
22
+ i18n={getI18n()}
23
+ defaultNS={pluginI18nNamespace(ctx.pluginId)}
24
+ >
25
+ <PluginErrorBoundary
26
+ fallback={(_error, reset) => <Crashed onRetry={reset} />}
27
+ >
28
+ <QueryClientProvider client={queryClient}>
29
+ {/* <AccessProvider api={ctx.api}> when the plugin has an access catalog */}
30
+ <HostProvider value={ctx}>
31
+ <div className={`${ctx.pluginId} flex h-full min-h-0 flex-col`}>
32
+ <Routes />
33
+ </div>
34
+ </HostProvider>
35
+ </QueryClientProvider>
36
+ </PluginErrorBoundary>
37
+ </I18nextProvider>
38
+ );
39
+ }
40
+ ```
41
+
42
+ - **Error boundary (TP-UI-004)**: the shell's boundary is outside your React root; without your own,
43
+ a render error is a blank page. `Crashed` is your translated `EmptyState` with a retry button.
44
+ - **Namespace**: the shell merged your locale files under `plugin.<id>`; name it once at the root.
45
+ - **Scope class (TP-UI-003)**: `tegoPluginTailwind` compiles your utilities under `.<pluginId>`.
46
+
47
+ ## Routing
48
+
49
+ `ctx.basePath` is `/p/<id>`. The shell does not remount the page when the in-plugin path changes:
50
+
51
+ ```ts
52
+ const route = usePluginRoute(); // { path: '/deals/42', search: '?tab=x' }
53
+ const host = useHost();
54
+ host.navigate('/deals/42'); // relative to basePath; absolute /p/... also fine
55
+ ```
56
+
57
+ Keep route parsing in a pure `lib/route.ts` (`parseRoute(path, search)` returning a union) and test it.
58
+ `ctx.can(navKey)` only says whether the manifest declares that page; real permissions come from the
59
+ access catalog (`useAccess`).
60
+
61
+ ## Calling the backend
62
+
63
+ ```ts
64
+ import { createApiClient, PluginApiError } from '@tego/plugin-sdk/react';
65
+ export const api = createApiClient(ctx.api);
66
+ const notes = await api.get<{ notes: Note[] }>('/notes');
67
+ await api.post('/notes', { body: text });
68
+ // errors: PluginApiError { status, code, message, details } — both the backend's {error:{code,message}}
69
+ // and the gateway's {success:false,error:{message}} shapes are understood
70
+ ```
71
+
72
+ Never use `ctx.api.json` for calls whose errors you show (it drops the body). Show errors with
73
+ `describeApiError(error, '<ns>')` → `{ key, message }`: `key ? t(key, { defaultValue: message }) : message`.
74
+
75
+ ## Live data
76
+
77
+ `useLiveUpdates(() => queryClient.invalidateQueries({ queryKey: ['<ns>'] }))` subscribes to
78
+ `ctx.events` (your backend's `host.events.publish`), batching bursts; React Query's `refetchInterval` should be slow
79
+ (120 s) while `ctx.events.connected()` and faster (15 s) when not.
80
+
81
+ ## Optional host capabilities
82
+
83
+ `ctx.office`, `ctx.ai`, `ctx.delegation` may be absent: feature-detect and keep a fallback.
@@ -0,0 +1,34 @@
1
+ # Host API versions
2
+
3
+ `hostApiRange` (`^X.Y.Z` or exact `X.Y.Z`) says which shells can run the plugin. Declare the
4
+ **lowest** version whose features you use; the shell refuses a plugin whose range it does not satisfy,
5
+ and upload refuses a manifest field newer than the range (TP-MAN-001), as does doctor for code
6
+ (TP-MAN-002). New plugins with an access catalog start at `^1.22.0`; the scaffold uses `^1.37.0`.
7
+
8
+ | Host API | Adds |
9
+ | --------------- | -------------------------------------------------------------------------------- |
10
+ | 1.2.0 | frontend route store, user display name |
11
+ | 1.3.0 | frontend event stream (`ctx.events`) |
12
+ | 1.4.0 | plugin configuration |
13
+ | 1.5.0 | `host.directory` |
14
+ | 1.9.0 | AI form filling (`ctx.ai`) |
15
+ | 1.10.0 | Python backends, `egress`, public routes and pages, plugin knowledge collections |
16
+ | 1.12.0 | `storage.uploadPath`, `get(key, { firstBytes })`, `ctx.hostUrl` |
17
+ | 1.14.0 | `ctx.layout.collapseSidebar` |
18
+ | 1.15.0 | managed downloads (`storage.managedPath`, `authorizeStorage`) |
19
+ | 1.17.0 / 1.18.0 | account provisioning / password reset |
20
+ | 1.19.0 | `storage.copy`, streams |
21
+ | 1.20.0 | Mermaid and HTML diagram rendering |
22
+ | 1.21.0 | `ctx.office` |
23
+ | 1.22.0 | access catalog, `@tego/plugin-sdk/access`, `host.tenantId` |
24
+ | 1.23.0 | `knowledge.manage` |
25
+ | 1.26.0 | `proactive.emit` |
26
+ | 1.29.0 | `mail.send` |
27
+ | 1.30.0 | `agentReadiness`, `settingsRoute` |
28
+ | 1.33.0 | `workDelegation` |
29
+ | 1.34.0 | `attention.request` |
30
+ | 1.37.0 | `ctx.platform`, `ctx.openExternal`, `ctx.download`, `ctx.copyText` |
31
+
32
+ Optional members (`ctx.office`, `ctx.ai`, `ctx.delegation`, `ctx.download` before 1.37) must be
33
+ feature-detected when the range admits an older shell. The full API is in
34
+ `agent/reference/plugin-sdk-api.md`.
@@ -0,0 +1,17 @@
1
+ # i18n
2
+
3
+ - **Ten locales (TP-I18N-001)**: `zh-CN zh-TW en ja ko de es fr it pt` in
4
+ `frontend/i18n/<locale>.json`. `zh-CN` is the reference: every other file has exactly its keys.
5
+ Upload refuses a missing locale, a missing key, or a nav `labelKey` that no file defines.
6
+ - **One root**: every key under the plugin's own root (`notes.home.title`, `notes.nav.home`,
7
+ `notes.errors.<code>`). No flat keys with dots in them.
8
+ - **Placeholders (TP-I18N-002)**: `{{name}}` sets match `zh-CN` in every locale; no empty strings.
9
+ - **Literal keys (TP-I18N-003)**: `t('notes.home.title')` must exist. For keys built at runtime
10
+ (`t(\`notes.status.${status}\`)`) list the variants in a test.
11
+ - **Terms (TP-I18N-004)**: 数字员工 / Employee via `{{avatarTerm}}` (`{{avatarTermShort}}`); 空间
12
+ Workspace; 技能 Skill; 模板 Template. Never 数字分身 / digital avatar.
13
+ - **Access strings**: with `@tego/plugin-access-ui`, `tego-plugin-pack` merges `tegoAccess.*` into
14
+ your files; don't copy them.
15
+ - **Test**: `assertLocaleParity(fileURLToPath(new URL('../i18n', import.meta.url)))` from
16
+ `@tego/plugin-sdk/testing` in a frontend test file runs these checks in `pnpm test`.
17
+ - Translate everything you add in the same change: adding a key to `zh-CN` only fails the build.
@@ -0,0 +1,26 @@
1
+ # Manifest and packaging
2
+
3
+ `tego.plugin.json` at the package root (TP-MAN-001):
4
+
5
+ - `manifestVersion: 1`; `id` `^[a-z0-9][a-z0-9._-]*$`, stable forever; `version` semver — upgrades
6
+ install over the same id, downgrades are refused.
7
+ - `displayName` / `description` per locale; `scope`: `avatar` (a capability of chosen employees;
8
+ required for a claw part) or `tenant` (a workspace app for everyone).
9
+ - `parts.frontend` — `icon` from the closed `PLUGIN_NAV_ICONS` list; `nav` (≤ 20 entries, unique
10
+ `key`, `labelKey` in your locales, optional `path` of letters, digits, `/_-` only). No `nav` = no page.
11
+ - `parts.backend` — `limits`, `egress` (hosts the backend may call; a bare host means 443),
12
+ `publicRoutes` (served by `fetchPublic`, no user), `migrationsDir`.
13
+ - `parts.claw` — `claw/openclaw.plugin.json` + `claw/package.json`; the packer stamps the version.
14
+ - `declares` (`database`, `storage`, `events`, `notifications`) are disclosures shown at install and
15
+ need a backend part.
16
+ - Configuration: `parts.frontend.config` / `parts.backend.config` hold a flat schema (scalars,
17
+ string enums, string arrays; ≤ 50 fields). `sensitive` only in the backend section. Read it with
18
+ `host.config.current()` (backend) or `ctx.config` (frontend). Claw config is
19
+ `configSchema` in `openclaw.plugin.json`, never in the manifest.
20
+ - `hostApiRange`: see host-api-versions.md.
21
+
22
+ Packaging: `tego-plugin-pack` (the `build` script) builds every part, merges access UI strings,
23
+ runs the upload-blocking doctor checks and writes `build/<id>-<version>.zip`. Never hand-roll the
24
+ SystemJS output. Dependencies (TP-PKG-001): SDK packages under npm aliases —
25
+ `"@tego/plugin-sdk": "npm:@zhama/tego-plugin-sdk@^1.37.0"`, likewise `@tego/ui`,
26
+ `@tego/plugin-access-ui` — or `workspace:*` inside a tego-os checkout.
@@ -0,0 +1,17 @@
1
+ # Database migrations
2
+
3
+ Each install has its own Postgres schema and a restricted role. The host applies
4
+ `backend/migrations/*.sql` in ordinal order, one transaction per file, with `search_path` set to your
5
+ schema only — so nothing is schema-qualified and nothing references a platform table.
6
+
7
+ - **Names (TP-BE-003)**: `0001_initial.sql`, `0002_add_tags.sql` — numeric ordinal, unique.
8
+ - **Append-only (TP-BE-004)**: a committed migration may already have run on an install; never edit or
9
+ delete it. Fix forward with a new file.
10
+ - **Source of truth**: the drizzle schema in `backend/schema/*.ts`. `pnpm db:generate` runs
11
+ `drizzle-kit generate` into `backend/.drizzle/` (committed), then `tego-plugin migrations sync` copies
12
+ new files into `backend/migrations/` with the next free ordinal and strips `"public".`.
13
+ `tego-plugin migrations check` (part of `build`) fails when they are out of sync.
14
+ - Hand-write only what drizzle cannot express: functions, triggers, RLS policies.
15
+ - Never `drizzle-kit push`.
16
+ - Test SQL against a real database with `createTestPluginHost({ pluginId, migrationsDir })`
17
+ (`TEST_DATABASE_URL`); it applies your migrations into a throwaway schema.
@@ -0,0 +1,18 @@
1
+ # Files and storage
2
+
3
+ `declares.storage: true`. Keys are relative to your install's prefix.
4
+
5
+ | Need | Backend | Frontend |
6
+ | ------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
7
+ | Small file (≤ 25 MiB) you produce | `host.storage.put(key, bytes, contentType)` | — |
8
+ | User uploads a file | `uploadPath(key, { maxBytes, ttlSeconds: 900 })` → return `{ path }` | `fetch(ctx.hostUrl(path), { method: 'PUT', body: file })`, then POST `/files/register` |
9
+ | Check an upload | `head(key)` (size, type), `get(key, { firstBytes: 8 })` (magic bytes) | — |
10
+ | Let a user download | `signedPath(key, 300)` or `managedPath(key, policy, 300)` | `ctx.download({ path, fileName })` |
11
+ | Re-check permission on every download | `managedPath` + `authorizeStorage` (needs `storage.managed`) | same |
12
+ | Copy / stream | `copy`, `getStream`, `putStream` (needs `storage.transfer`, host 1.19) | — |
13
+
14
+ - Never move large files through `ctx.api`: the desktop app buffers it and caps it at 100 MB.
15
+ - Validate type and size on the client first (shared limits in `shared/`), and again on the backend.
16
+ - `authorizeStorage` decides with the access engine:
17
+ `access.forSubject(await access.subjectFor(userId, host), host)`; deny on any error, and test
18
+ both allow and deny.
@@ -0,0 +1,17 @@
1
+ # Tests
2
+
3
+ Gates (TP-PKG-002): `lint` (eslint), `type-check` (`tsgo --noEmit` per part), `test` (`vitest run`),
4
+ `build` (`tego-plugin-pack`). Run them, plus `tego-plugin doctor`, after every change.
5
+
6
+ - **Backend**: routes against a real Postgres with `createTestPluginHost({ pluginId, migrationsDir })`
7
+ (`TEST_DATABASE_URL`); call `backend.fetch(new Request(...), CALLER, harness.host)`. A missing
8
+ database fails the run, it never silently skips.
9
+ - **Access**: `assertRouteCoverage(app)`; allow _and_ deny cases for every permission you add.
10
+ - **Frontend**: pure logic in `.ts` files (route parser, formatters, API error mapping) — no
11
+ component tests.
12
+ - **i18n**: `assertLocaleParity(dir)`.
13
+ - **Claw**: `assertClawSkills(skillsDir, toolNames)`; registered tools equal `contracts.tools`.
14
+ - **Style**: each test file starts with a Business Cases comment (domain, then one line per case
15
+ `NOTE-001 posting a note → 201 and visible on the next list`); every `it` title starts with
16
+ `[NOTE-001]`. Assert exact status codes and values; at least one failure path per file; no
17
+ conditional assertions.
@@ -0,0 +1,22 @@
1
+ # UI and design
2
+
3
+ Your page sits inside the shell next to its own UI; it must look like part of it in light and dark.
4
+
5
+ - **Components (TP-UI-002)**: `@tego/ui` first — Button, Input, Select, Dialog, Popover, Tooltip,
6
+ Tabs, Table, Badge, EmptyState, Loading, Skeleton, toast/Toaster, charts. Never import
7
+ `@radix-ui/*`; never rebuild a primitive locally.
8
+ - **Colors (TP-UI-001)**: token classes only — surfaces `bg-background bg-card bg-sunken bg-inset
9
+ bg-popover`, text `text-foreground text-muted-foreground`, lines `border-border border-border-strong`,
10
+ semantic `bg-X text-X-foreground` / `bg-X-soft text-X` with X in
11
+ `primary signal success warning destructive info`. No hex, `rgb()`, palette classes
12
+ (`bg-slate-50`, `text-white`, `bg-black/40`) or `dark:` — tokens carry both themes.
13
+ `primary` means selection/focus, `signal` means "happening now".
14
+ - **Scope (TP-UI-003)**: `tegoPluginTailwind` scopes utilities under `.<pluginId>` (no preflight).
15
+ Put the class on your root element; portalled content (dialogs, popovers, selects) renders outside
16
+ it — add `tegoPluginScopeRoot(pluginId)` to PostCSS, or put the class on each `*Content`.
17
+ - **Status** is text or an icon plus color, never color alone. Machine data (ids, amounts, times,
18
+ versions) uses `font-mono tabular-nums`.
19
+ - **Shapes**: `rounded-control rounded-card rounded-container`; cards `shadow-card`; no card inside
20
+ a card. Motion: `duration-hover duration-enter`, no decorative infinite animation.
21
+ - **Copy**: short and specific; no explanatory paragraphs on pages; an empty state is one line plus
22
+ one action. Chinese copy follows the platform terms (空间、数字员工、技能、模板).