@construct-space/cli 1.7.6 → 1.8.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/dist/index.js +49 -5
- package/dist/templates/space/actions.ts.tmpl +42 -10
- package/dist/templates/space/config.md.tmpl +11 -4
- package/dist/templates/space/construct.md.tmpl +79 -6
- package/dist/templates/space/entry.ts.tmpl +1 -0
- package/dist/templates/space/full/entry.ts.tmpl +1 -0
- package/dist/templates/space/package.json.tmpl +2 -0
- package/dist/templates/space/style.css.tmpl +5 -0
- package/dist/templates/space/vite.config.ts.tmpl +4 -2
- package/package.json +1 -1
- package/templates/space/actions.ts.tmpl +42 -10
- package/templates/space/config.md.tmpl +11 -4
- package/templates/space/construct.md.tmpl +79 -6
- package/templates/space/entry.ts.tmpl +1 -0
- package/templates/space/full/entry.ts.tmpl +1 -0
- package/templates/space/package.json.tmpl +2 -0
- package/templates/space/style.css.tmpl +5 -0
- package/templates/space/vite.config.ts.tmpl +4 -2
package/dist/index.js
CHANGED
|
@@ -5312,6 +5312,7 @@ async function scaffold(nameArg, options) {
|
|
|
5312
5312
|
const files = {
|
|
5313
5313
|
"package.json.tmpl": join2(name, "package.json"),
|
|
5314
5314
|
"vite.config.ts.tmpl": join2(name, "vite.config.ts"),
|
|
5315
|
+
"style.css.tmpl": join2(name, "src", "style.css"),
|
|
5315
5316
|
"index.vue.tmpl": join2(name, "src", "pages", "index.vue"),
|
|
5316
5317
|
"config.md.tmpl": join2(name, "agent", "config.md"),
|
|
5317
5318
|
"skill.md.tmpl": join2(name, "agent", "skills", "default.md"),
|
|
@@ -8154,6 +8155,9 @@ function generate(root, m) {
|
|
|
8154
8155
|
"// Auto-generated entry \u2014 do not edit manually",
|
|
8155
8156
|
"// Generated from space.manifest.json"
|
|
8156
8157
|
];
|
|
8158
|
+
if (existsSync4(join4(root, "src", "style.css"))) {
|
|
8159
|
+
lines.push("import './style.css'");
|
|
8160
|
+
}
|
|
8157
8161
|
for (const p of pages)
|
|
8158
8162
|
lines.push(`import ${p.varName} from '${p.importPath}'`);
|
|
8159
8163
|
for (const w of widgets)
|
|
@@ -8490,6 +8494,13 @@ async function build(options) {
|
|
|
8490
8494
|
renameSync(oldCSS, newCSS);
|
|
8491
8495
|
}
|
|
8492
8496
|
}
|
|
8497
|
+
const expectedCSS = join6(distDir, `space-${m.id}.css`);
|
|
8498
|
+
if (!existsSync6(expectedCSS)) {
|
|
8499
|
+
const cssMatches = readdirSync3(distDir).filter((f) => f.endsWith(".css"));
|
|
8500
|
+
const renamedCSS = cssMatches.find((f) => f.startsWith("space-")) || (cssMatches.length === 1 ? cssMatches[0] : undefined);
|
|
8501
|
+
if (renamedCSS)
|
|
8502
|
+
renameSync(join6(distDir, renamedCSS), expectedCSS);
|
|
8503
|
+
}
|
|
8493
8504
|
const bundleData = readFileSync4(bundlePath);
|
|
8494
8505
|
const checksum = createHash("sha256").update(bundleData).digest("hex");
|
|
8495
8506
|
const raw = readRaw(root);
|
|
@@ -10337,7 +10348,10 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10337
10348
|
}
|
|
10338
10349
|
msg += `
|
|
10339
10350
|
Fork to a new space_id to publish your own version.`;
|
|
10340
|
-
|
|
10351
|
+
const err = new Error(msg);
|
|
10352
|
+
err.ownerKind = result.owner_kind;
|
|
10353
|
+
err.ownerOrgId = result.owner_org_id;
|
|
10354
|
+
throw err;
|
|
10341
10355
|
}
|
|
10342
10356
|
if (resp.status >= 400) {
|
|
10343
10357
|
const msg = result.error || result.errors?.join("; ") || `server returned ${resp.status}`;
|
|
@@ -10345,6 +10359,20 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10345
10359
|
}
|
|
10346
10360
|
return result;
|
|
10347
10361
|
}
|
|
10362
|
+
async function fetchOrgPublisherKey(portalURL, identityToken) {
|
|
10363
|
+
const root = portalURL.replace(/\/api\/developer\/?$/, "").replace(/\/+$/, "");
|
|
10364
|
+
try {
|
|
10365
|
+
const resp = await fetch(`${root}/api/publisher/org`, {
|
|
10366
|
+
headers: { Authorization: `Bearer ${identityToken}` }
|
|
10367
|
+
});
|
|
10368
|
+
if (!resp.ok)
|
|
10369
|
+
return null;
|
|
10370
|
+
const body = await resp.json();
|
|
10371
|
+
return body.publisher?.api_key || null;
|
|
10372
|
+
} catch {
|
|
10373
|
+
return null;
|
|
10374
|
+
}
|
|
10375
|
+
}
|
|
10348
10376
|
function setVersionInFiles(root, oldVer, newVer) {
|
|
10349
10377
|
const oldStr = `"version": "${oldVer}"`;
|
|
10350
10378
|
const newStr = `"version": "${newVer}"`;
|
|
@@ -10501,7 +10529,23 @@ async function publish(options) {
|
|
|
10501
10529
|
}
|
|
10502
10530
|
const uploadSpinner = ora("Uploading & building...").start();
|
|
10503
10531
|
try {
|
|
10504
|
-
const
|
|
10532
|
+
const explicitKey = options?.apiKey || process.env.CONSTRUCT_PUBLISHER_KEY;
|
|
10533
|
+
const initialKey = explicitKey || creds.publisherKey;
|
|
10534
|
+
let result;
|
|
10535
|
+
try {
|
|
10536
|
+
result = await uploadSource(creds.portal, creds.token, initialKey, tarballPath, m, { private: wantPrivate, public: wantPublic });
|
|
10537
|
+
} catch (e) {
|
|
10538
|
+
if (e?.ownerKind === "org" && !creds.publisherKey) {
|
|
10539
|
+
uploadSpinner.text = "Fetching org publisher key\u2026";
|
|
10540
|
+
const orgKey = await fetchOrgPublisherKey(creds.portal, creds.token);
|
|
10541
|
+
if (!orgKey)
|
|
10542
|
+
throw e;
|
|
10543
|
+
uploadSpinner.text = "Uploading & building (as org)\u2026";
|
|
10544
|
+
result = await uploadSource(creds.portal, creds.token, orgKey, tarballPath, m, { private: wantPrivate, public: wantPublic });
|
|
10545
|
+
} else {
|
|
10546
|
+
throw e;
|
|
10547
|
+
}
|
|
10548
|
+
}
|
|
10505
10549
|
unlinkSync2(tarballPath);
|
|
10506
10550
|
gitSafe(root, "tag", `v${m.version}`);
|
|
10507
10551
|
gitSafe(root, "push", "origin", `v${m.version}`);
|
|
@@ -11457,7 +11501,7 @@ function graphFork(newSpaceID) {
|
|
|
11457
11501
|
// package.json
|
|
11458
11502
|
var package_default = {
|
|
11459
11503
|
name: "@construct-space/cli",
|
|
11460
|
-
version: "1.
|
|
11504
|
+
version: "1.8.0",
|
|
11461
11505
|
description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
|
|
11462
11506
|
type: "module",
|
|
11463
11507
|
bin: {
|
|
@@ -11509,7 +11553,7 @@ program2.command("scaffold [name]").alias("new").alias("create").description("Cr
|
|
|
11509
11553
|
program2.command("build").description("Build the space (generate entry + run Vite)").option("--entry-only", "Only generate src/entry.ts").action(async (opts) => build(opts));
|
|
11510
11554
|
program2.command("dev").description("Start dev mode with file watching and live reload").action(async () => dev());
|
|
11511
11555
|
program2.command("install").alias("run").description("Install built space to Construct spaces directory").action(() => install());
|
|
11512
|
-
program2.command("publish").description("Publish a space to the Construct registry").option("-y, --yes", "Skip all confirmation prompts").option("--bump <type>", "Auto-bump version (patch, minor, major)").option("--private", "Publish as org-private (catalog-listed only inside the owning org). Requires an org publisher key.").option("--public", "Flip a previously-private space back to the public catalog on this publish. Without --private or --public, visibility is preserved on update (and defaults to public on first publish).").action(async (opts) => publish(opts));
|
|
11556
|
+
program2.command("publish").description("Publish a space to the Construct registry").option("-y, --yes", "Skip all confirmation prompts").option("--bump <type>", "Auto-bump version (patch, minor, major)").option("--private", "Publish as org-private (catalog-listed only inside the owning org). Requires an org publisher key.").option("--public", "Flip a previously-private space back to the public catalog on this publish. Without --private or --public, visibility is preserved on update (and defaults to public on first publish).").option("--api-key <key>", "Publisher API key (csk_live_\u2026). Overrides any key stored in the active profile. Also reads CONSTRUCT_PUBLISHER_KEY.").action(async (opts) => publish(opts));
|
|
11513
11557
|
program2.command("validate").description("Validate space.manifest.json").action(() => validate3());
|
|
11514
11558
|
program2.command("check").description("Run type-check (vue-tsc) and linter (eslint)").action(() => check());
|
|
11515
11559
|
program2.command("clean").description("Remove build artifacts").option("--all", "Also remove node_modules and lockfiles").action((opts) => clean(opts));
|
|
@@ -11540,7 +11584,7 @@ space.command("scaffold [name]").alias("new").alias("create").option("--with-tes
|
|
|
11540
11584
|
space.command("build").option("--entry-only").action(async (opts) => build(opts));
|
|
11541
11585
|
space.command("dev").action(async () => dev());
|
|
11542
11586
|
space.command("install").alias("run").action(() => install());
|
|
11543
|
-
space.command("publish").option("-y, --yes").option("--bump <type>").option("--private").option("--public").action(async (opts) => publish(opts));
|
|
11587
|
+
space.command("publish").option("-y, --yes").option("--bump <type>").option("--private").option("--public").option("--api-key <key>").action(async (opts) => publish(opts));
|
|
11544
11588
|
space.command("validate").action(() => validate3());
|
|
11545
11589
|
space.command("check").action(() => check());
|
|
11546
11590
|
space.command("clean").option("--all").action((opts) => clean(opts));
|
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Space Actions — exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
|
-
* Each action: { description, params, run }.
|
|
5
|
-
* - `params`
|
|
6
|
-
* - `run`
|
|
4
|
+
* Each action: { description, params, run, tier? }.
|
|
5
|
+
* - `params` JSON-schema-ish input shape; each: { type, description?, required? }.
|
|
6
|
+
* - `run` receives the validated payload, returns any JSON-serialisable value.
|
|
7
|
+
* - `tier` (optional) default model bucket for useBrain() calls inside `run`:
|
|
8
|
+
* 'small' fast/cheap — summarisation, classification, short Q&A
|
|
9
|
+
* 'medium' balanced — general help, edits, structured output
|
|
10
|
+
* 'large' reasoning — long-form writing, deep code, planning
|
|
11
|
+
* The host resolves tier → provider+model via the user's tier config
|
|
12
|
+
* (Settings → LLM Providers). Per-call `brain.complete({ tier })`
|
|
13
|
+
* wins over the action default.
|
|
14
|
+
*
|
|
15
|
+
* Permission for useBrain():
|
|
16
|
+
* - Declare `{{.ID}}:brain` in `permissions.catalog` (space.manifest.json).
|
|
17
|
+
* - Add the action to `permissions.actions` mapping to that id, e.g.
|
|
18
|
+
* "permissions": {
|
|
19
|
+
* "actions": { "summarize": "{{.ID}}:brain" },
|
|
20
|
+
* "catalog": [{ "id": "{{.ID}}:brain", "label": "Use AI summaries" }]
|
|
21
|
+
* }
|
|
7
22
|
*
|
|
8
23
|
* Pair with @construct-space/graph for typed multi-tenant data:
|
|
9
24
|
* import { useGraph } from '@construct-space/graph'
|
|
10
25
|
* import { Item } from './models'
|
|
11
26
|
* const items = useGraph(Item)
|
|
12
|
-
*
|
|
13
|
-
* listItems: {
|
|
14
|
-
* description: 'List items',
|
|
15
|
-
* params: { limit: { type: 'number', required: false } },
|
|
16
|
-
* run: async (p: any) => ({ items: await items.find({ limit: p.limit ?? 50 }) }),
|
|
17
|
-
* }
|
|
18
27
|
*/
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
import { useBrain } from '@construct-space/sdk'
|
|
30
|
+
import type { SpaceActions } from '@construct-space/sdk'
|
|
31
|
+
|
|
32
|
+
export const actions: SpaceActions = {
|
|
21
33
|
ping: {
|
|
22
34
|
description: 'Health check — returns pong with the space id.',
|
|
23
35
|
params: {},
|
|
24
36
|
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
25
37
|
},
|
|
38
|
+
|
|
39
|
+
// Example: a tier-routed action. Delete or adapt for your space.
|
|
40
|
+
// Requires '{{.ID}}:brain' in permissions.catalog and a row in
|
|
41
|
+
// permissions.actions mapping 'summarize' to that id.
|
|
42
|
+
//
|
|
43
|
+
// summarize: {
|
|
44
|
+
// description: 'Summarise the given text in 1-2 sentences.',
|
|
45
|
+
// tier: 'small',
|
|
46
|
+
// params: {
|
|
47
|
+
// text: { type: 'string', required: true, description: 'Text to summarise.' },
|
|
48
|
+
// },
|
|
49
|
+
// async run({ text }: { text: string }) {
|
|
50
|
+
// const brain = useBrain()
|
|
51
|
+
// if (!brain) return { error: 'brain unavailable' }
|
|
52
|
+
// const { text: summary } = await brain.complete({
|
|
53
|
+
// prompt: `Summarise in 1-2 sentences:\n\n${text}`,
|
|
54
|
+
// })
|
|
55
|
+
// return { summary }
|
|
56
|
+
// },
|
|
57
|
+
// },
|
|
26
58
|
}
|
|
@@ -4,7 +4,12 @@ name: {{.DisplayName}}
|
|
|
4
4
|
category: space
|
|
5
5
|
description: AI agent for the {{.DisplayName}} space
|
|
6
6
|
maxIterations: 15
|
|
7
|
-
tools
|
|
7
|
+
# Whitelist of tools the agent can call. Each action in src/actions.ts is
|
|
8
|
+
# exposed as `<space-id>.<actionName>`. Add yours here as you build them;
|
|
9
|
+
# space_list_actions + space_run_action remain available as the fallback
|
|
10
|
+
# discovery bridge, but explicit listing gives the model proper tool schemas.
|
|
11
|
+
tools:
|
|
12
|
+
- {{.ID}}.ping
|
|
8
13
|
canInvokeAgents: []
|
|
9
14
|
---
|
|
10
15
|
|
|
@@ -12,12 +17,14 @@ You are Construct's {{.DisplayName}} agent. You help users work within the {{.Di
|
|
|
12
17
|
|
|
13
18
|
## Context
|
|
14
19
|
|
|
15
|
-
Use
|
|
16
|
-
|
|
20
|
+
Use the listed tools above first. If you need an action that isn't whitelisted,
|
|
21
|
+
call space_list_actions to discover what else this space exposes, then
|
|
22
|
+
space_run_action to invoke it.
|
|
23
|
+
|
|
17
24
|
Do NOT call get_project_context — you work with space content, not project files.
|
|
18
25
|
|
|
19
26
|
## Behavior
|
|
20
27
|
|
|
21
|
-
- Start by
|
|
28
|
+
- Start by understanding what the user wants in this space
|
|
22
29
|
- Be concise and action-oriented
|
|
23
30
|
- Focus on tasks relevant to this space
|
|
@@ -162,7 +162,7 @@ export const Task = defineModel('task', {
|
|
|
162
162
|
labels: field.json(),
|
|
163
163
|
assignee_id: field.string().index(),
|
|
164
164
|
}, {
|
|
165
|
-
|
|
165
|
+
scopes: ['org'], // partitions schema per organization
|
|
166
166
|
access: {
|
|
167
167
|
read: access.authenticated(),
|
|
168
168
|
create: access.authenticated(),
|
|
@@ -188,10 +188,12 @@ await tasks.remove(id)
|
|
|
188
188
|
|
|
189
189
|
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context — use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
190
190
|
|
|
191
|
-
**Scopes
|
|
192
|
-
- `app` —
|
|
193
|
-
- `org` —
|
|
194
|
-
- `
|
|
191
|
+
**Scopes** (must match `scopes` in space.manifest.json):
|
|
192
|
+
- `'app'` — per-user bucket (personal install)
|
|
193
|
+
- `'org'` — shared across the active organization (org install)
|
|
194
|
+
- both: `scopes: ['app', 'org']` — host picks bucket at install time
|
|
195
|
+
|
|
196
|
+
`'project'` was removed in SDK 1.0. Older docs that show it are stale.
|
|
195
197
|
|
|
196
198
|
**Push models to backend after editing**: `construct graph push`.
|
|
197
199
|
|
|
@@ -219,7 +221,9 @@ await tasks.remove(id)
|
|
|
219
221
|
`src/actions.ts` exports an `actions` object — each entry is exposed to the space's agent as a first-class tool.
|
|
220
222
|
|
|
221
223
|
```ts
|
|
222
|
-
|
|
224
|
+
import type { SpaceActions } from '@construct-space/sdk'
|
|
225
|
+
|
|
226
|
+
export const actions: SpaceActions = {
|
|
223
227
|
createTask: {
|
|
224
228
|
description: 'Create a task',
|
|
225
229
|
params: {
|
|
@@ -231,6 +235,75 @@ export const actions = {
|
|
|
231
235
|
}
|
|
232
236
|
```
|
|
233
237
|
|
|
238
|
+
### Tier-routed brain calls (LLM from inside an action)
|
|
239
|
+
|
|
240
|
+
Actions can call the model mid-execution via `useBrain()`. The action picks
|
|
241
|
+
the **cost bucket** (small / medium / large), the **user** picks the slot
|
|
242
|
+
in Settings → LLM Providers. The host maps tier → provider + model.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { useBrain } from '@construct-space/sdk'
|
|
246
|
+
import type { SpaceActions } from '@construct-space/sdk'
|
|
247
|
+
|
|
248
|
+
export const actions: SpaceActions = {
|
|
249
|
+
summarizeThread: {
|
|
250
|
+
description: 'Summarise one email thread in 1-2 sentences.',
|
|
251
|
+
tier: 'small', // default for brain calls in `run`
|
|
252
|
+
params: { threadId: { type: 'string', required: true } },
|
|
253
|
+
async run({ threadId }) {
|
|
254
|
+
const brain = useBrain()
|
|
255
|
+
if (!brain) return { error: 'brain unavailable' }
|
|
256
|
+
const thread = await loadThread(threadId as string)
|
|
257
|
+
const { text } = await brain.complete({ prompt: `Summarise:\n${thread.body}` })
|
|
258
|
+
return { summary: text }
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
composeReply: {
|
|
263
|
+
description: 'Draft a polished reply to the given message.',
|
|
264
|
+
tier: 'large', // long-form writing → opus-class
|
|
265
|
+
params: {
|
|
266
|
+
original: { type: 'string', required: true },
|
|
267
|
+
style: { type: 'string', required: false, description: 'e.g. "warm", "formal"' },
|
|
268
|
+
},
|
|
269
|
+
async run({ original, style }) {
|
|
270
|
+
const brain = useBrain()
|
|
271
|
+
if (!brain) return { error: 'brain unavailable' }
|
|
272
|
+
const { text } = await brain.chat({
|
|
273
|
+
system: 'You write replies that feel like a thoughtful human wrote them.',
|
|
274
|
+
messages: [{ role: 'user', content: `${style ? `Tone: ${style}.\n\n` : ''}Reply to:\n${original}` }],
|
|
275
|
+
})
|
|
276
|
+
return { draft: text }
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Per-call overrides win: `brain.complete({ prompt, tier: 'large' })` upgrades
|
|
283
|
+
even a small-tier action when the user asked for "detailed".
|
|
284
|
+
|
|
285
|
+
**Permission required.** A space whose actions call brain must declare
|
|
286
|
+
`{{.ID}}:brain` in `permissions.catalog` and map each brain-using action
|
|
287
|
+
to it under `permissions.actions`. The user grants this at install time.
|
|
288
|
+
|
|
289
|
+
```jsonc
|
|
290
|
+
// space.manifest.json
|
|
291
|
+
{
|
|
292
|
+
"permissions": {
|
|
293
|
+
"actions": {
|
|
294
|
+
"summarizeThread": "{{.ID}}:brain",
|
|
295
|
+
"composeReply": "{{.ID}}:brain"
|
|
296
|
+
},
|
|
297
|
+
"catalog": [
|
|
298
|
+
{ "id": "{{.ID}}:brain", "label": "Use AI for summaries & drafts" }
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Without the grant, `useBrain()` throws `BrainPermissionDenied` synchronously
|
|
305
|
+
on the first method call so the action can fall back gracefully.
|
|
306
|
+
|
|
234
307
|
### Wiring (REQUIRED — easy to miss)
|
|
235
308
|
|
|
236
309
|
Each action becomes a tool named **`{spaceID}.{actionName}`** (dot, not underscore). The operator only pre-registers actions that are **explicitly whitelisted** in the agent config. An empty `tools: []` means the agent sees zero action tools and will report "unknown tool".
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Space entry — exports pages, widgets, and actions for the host loader.
|
|
2
2
|
// `construct dev` regenerates this from space.manifest.json on changes.
|
|
3
|
+
import './style.css'
|
|
3
4
|
import IndexPage from './pages/index.vue'
|
|
4
5
|
import SettingsPage from './pages/settings.vue'
|
|
5
6
|
import { actions } from './actions'
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
"@construct-space/cli": "latest",
|
|
21
21
|
"@construct-space/sdk": "latest",
|
|
22
22
|
"@eslint/js": "^10.0.1",
|
|
23
|
+
"@tailwindcss/vite": "^4.1.17",
|
|
23
24
|
"@vitejs/plugin-vue": "^6.0.6",
|
|
24
25
|
"eslint": "^10.2.1",
|
|
25
26
|
"eslint-plugin-vue": "^10.0.0",
|
|
26
27
|
"lucide-vue-next": "^1.0.0",
|
|
28
|
+
"tailwindcss": "^4.1.17",
|
|
27
29
|
"typescript": "^5.9.3",
|
|
28
30
|
"typescript-eslint": "^8.0.0",
|
|
29
31
|
"vite": "^8.0.12",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
2
3
|
import vue from '@vitejs/plugin-vue'
|
|
3
4
|
import { resolve } from 'path'
|
|
4
5
|
|
|
@@ -19,8 +20,9 @@ const hostExternals = [
|
|
|
19
20
|
'dexie',
|
|
20
21
|
'zod',
|
|
21
22
|
'@construct-space/ui',
|
|
22
|
-
'@construct/sdk',
|
|
23
23
|
'@construct-space/sdk',
|
|
24
|
+
'@construct-space/graph',
|
|
25
|
+
'@construct/sdk', // legacy alias — keep during cutover
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
function makeGlobals(externals: string[]): Record<string, string> {
|
|
@@ -37,7 +39,7 @@ function makeGlobals(externals: string[]): Record<string, string> {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export default defineConfig({
|
|
40
|
-
plugins: [vue()],
|
|
42
|
+
plugins: [tailwindcss(), vue()],
|
|
41
43
|
build: {
|
|
42
44
|
lib: {
|
|
43
45
|
entry: resolve(__dirname, 'src/entry.ts'),
|
package/package.json
CHANGED
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Space Actions — exposed to the AI agent via space_run_action.
|
|
3
3
|
*
|
|
4
|
-
* Each action: { description, params, run }.
|
|
5
|
-
* - `params`
|
|
6
|
-
* - `run`
|
|
4
|
+
* Each action: { description, params, run, tier? }.
|
|
5
|
+
* - `params` JSON-schema-ish input shape; each: { type, description?, required? }.
|
|
6
|
+
* - `run` receives the validated payload, returns any JSON-serialisable value.
|
|
7
|
+
* - `tier` (optional) default model bucket for useBrain() calls inside `run`:
|
|
8
|
+
* 'small' fast/cheap — summarisation, classification, short Q&A
|
|
9
|
+
* 'medium' balanced — general help, edits, structured output
|
|
10
|
+
* 'large' reasoning — long-form writing, deep code, planning
|
|
11
|
+
* The host resolves tier → provider+model via the user's tier config
|
|
12
|
+
* (Settings → LLM Providers). Per-call `brain.complete({ tier })`
|
|
13
|
+
* wins over the action default.
|
|
14
|
+
*
|
|
15
|
+
* Permission for useBrain():
|
|
16
|
+
* - Declare `{{.ID}}:brain` in `permissions.catalog` (space.manifest.json).
|
|
17
|
+
* - Add the action to `permissions.actions` mapping to that id, e.g.
|
|
18
|
+
* "permissions": {
|
|
19
|
+
* "actions": { "summarize": "{{.ID}}:brain" },
|
|
20
|
+
* "catalog": [{ "id": "{{.ID}}:brain", "label": "Use AI summaries" }]
|
|
21
|
+
* }
|
|
7
22
|
*
|
|
8
23
|
* Pair with @construct-space/graph for typed multi-tenant data:
|
|
9
24
|
* import { useGraph } from '@construct-space/graph'
|
|
10
25
|
* import { Item } from './models'
|
|
11
26
|
* const items = useGraph(Item)
|
|
12
|
-
*
|
|
13
|
-
* listItems: {
|
|
14
|
-
* description: 'List items',
|
|
15
|
-
* params: { limit: { type: 'number', required: false } },
|
|
16
|
-
* run: async (p: any) => ({ items: await items.find({ limit: p.limit ?? 50 }) }),
|
|
17
|
-
* }
|
|
18
27
|
*/
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
import { useBrain } from '@construct-space/sdk'
|
|
30
|
+
import type { SpaceActions } from '@construct-space/sdk'
|
|
31
|
+
|
|
32
|
+
export const actions: SpaceActions = {
|
|
21
33
|
ping: {
|
|
22
34
|
description: 'Health check — returns pong with the space id.',
|
|
23
35
|
params: {},
|
|
24
36
|
run: () => ({ pong: true, space: '{{.ID}}' }),
|
|
25
37
|
},
|
|
38
|
+
|
|
39
|
+
// Example: a tier-routed action. Delete or adapt for your space.
|
|
40
|
+
// Requires '{{.ID}}:brain' in permissions.catalog and a row in
|
|
41
|
+
// permissions.actions mapping 'summarize' to that id.
|
|
42
|
+
//
|
|
43
|
+
// summarize: {
|
|
44
|
+
// description: 'Summarise the given text in 1-2 sentences.',
|
|
45
|
+
// tier: 'small',
|
|
46
|
+
// params: {
|
|
47
|
+
// text: { type: 'string', required: true, description: 'Text to summarise.' },
|
|
48
|
+
// },
|
|
49
|
+
// async run({ text }: { text: string }) {
|
|
50
|
+
// const brain = useBrain()
|
|
51
|
+
// if (!brain) return { error: 'brain unavailable' }
|
|
52
|
+
// const { text: summary } = await brain.complete({
|
|
53
|
+
// prompt: `Summarise in 1-2 sentences:\n\n${text}`,
|
|
54
|
+
// })
|
|
55
|
+
// return { summary }
|
|
56
|
+
// },
|
|
57
|
+
// },
|
|
26
58
|
}
|
|
@@ -4,7 +4,12 @@ name: {{.DisplayName}}
|
|
|
4
4
|
category: space
|
|
5
5
|
description: AI agent for the {{.DisplayName}} space
|
|
6
6
|
maxIterations: 15
|
|
7
|
-
tools
|
|
7
|
+
# Whitelist of tools the agent can call. Each action in src/actions.ts is
|
|
8
|
+
# exposed as `<space-id>.<actionName>`. Add yours here as you build them;
|
|
9
|
+
# space_list_actions + space_run_action remain available as the fallback
|
|
10
|
+
# discovery bridge, but explicit listing gives the model proper tool schemas.
|
|
11
|
+
tools:
|
|
12
|
+
- {{.ID}}.ping
|
|
8
13
|
canInvokeAgents: []
|
|
9
14
|
---
|
|
10
15
|
|
|
@@ -12,12 +17,14 @@ You are Construct's {{.DisplayName}} agent. You help users work within the {{.Di
|
|
|
12
17
|
|
|
13
18
|
## Context
|
|
14
19
|
|
|
15
|
-
Use
|
|
16
|
-
|
|
20
|
+
Use the listed tools above first. If you need an action that isn't whitelisted,
|
|
21
|
+
call space_list_actions to discover what else this space exposes, then
|
|
22
|
+
space_run_action to invoke it.
|
|
23
|
+
|
|
17
24
|
Do NOT call get_project_context — you work with space content, not project files.
|
|
18
25
|
|
|
19
26
|
## Behavior
|
|
20
27
|
|
|
21
|
-
- Start by
|
|
28
|
+
- Start by understanding what the user wants in this space
|
|
22
29
|
- Be concise and action-oriented
|
|
23
30
|
- Focus on tasks relevant to this space
|
|
@@ -162,7 +162,7 @@ export const Task = defineModel('task', {
|
|
|
162
162
|
labels: field.json(),
|
|
163
163
|
assignee_id: field.string().index(),
|
|
164
164
|
}, {
|
|
165
|
-
|
|
165
|
+
scopes: ['org'], // partitions schema per organization
|
|
166
166
|
access: {
|
|
167
167
|
read: access.authenticated(),
|
|
168
168
|
create: access.authenticated(),
|
|
@@ -188,10 +188,12 @@ await tasks.remove(id)
|
|
|
188
188
|
|
|
189
189
|
**Access helpers**: `authenticated()`, `owner()`, `admin()`, `member()`. `member()` requires org context — use `authenticated()` for org-scoped models that allow any logged-in member.
|
|
190
190
|
|
|
191
|
-
**Scopes
|
|
192
|
-
- `app` —
|
|
193
|
-
- `org` —
|
|
194
|
-
- `
|
|
191
|
+
**Scopes** (must match `scopes` in space.manifest.json):
|
|
192
|
+
- `'app'` — per-user bucket (personal install)
|
|
193
|
+
- `'org'` — shared across the active organization (org install)
|
|
194
|
+
- both: `scopes: ['app', 'org']` — host picks bucket at install time
|
|
195
|
+
|
|
196
|
+
`'project'` was removed in SDK 1.0. Older docs that show it are stale.
|
|
195
197
|
|
|
196
198
|
**Push models to backend after editing**: `construct graph push`.
|
|
197
199
|
|
|
@@ -219,7 +221,9 @@ await tasks.remove(id)
|
|
|
219
221
|
`src/actions.ts` exports an `actions` object — each entry is exposed to the space's agent as a first-class tool.
|
|
220
222
|
|
|
221
223
|
```ts
|
|
222
|
-
|
|
224
|
+
import type { SpaceActions } from '@construct-space/sdk'
|
|
225
|
+
|
|
226
|
+
export const actions: SpaceActions = {
|
|
223
227
|
createTask: {
|
|
224
228
|
description: 'Create a task',
|
|
225
229
|
params: {
|
|
@@ -231,6 +235,75 @@ export const actions = {
|
|
|
231
235
|
}
|
|
232
236
|
```
|
|
233
237
|
|
|
238
|
+
### Tier-routed brain calls (LLM from inside an action)
|
|
239
|
+
|
|
240
|
+
Actions can call the model mid-execution via `useBrain()`. The action picks
|
|
241
|
+
the **cost bucket** (small / medium / large), the **user** picks the slot
|
|
242
|
+
in Settings → LLM Providers. The host maps tier → provider + model.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { useBrain } from '@construct-space/sdk'
|
|
246
|
+
import type { SpaceActions } from '@construct-space/sdk'
|
|
247
|
+
|
|
248
|
+
export const actions: SpaceActions = {
|
|
249
|
+
summarizeThread: {
|
|
250
|
+
description: 'Summarise one email thread in 1-2 sentences.',
|
|
251
|
+
tier: 'small', // default for brain calls in `run`
|
|
252
|
+
params: { threadId: { type: 'string', required: true } },
|
|
253
|
+
async run({ threadId }) {
|
|
254
|
+
const brain = useBrain()
|
|
255
|
+
if (!brain) return { error: 'brain unavailable' }
|
|
256
|
+
const thread = await loadThread(threadId as string)
|
|
257
|
+
const { text } = await brain.complete({ prompt: `Summarise:\n${thread.body}` })
|
|
258
|
+
return { summary: text }
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
composeReply: {
|
|
263
|
+
description: 'Draft a polished reply to the given message.',
|
|
264
|
+
tier: 'large', // long-form writing → opus-class
|
|
265
|
+
params: {
|
|
266
|
+
original: { type: 'string', required: true },
|
|
267
|
+
style: { type: 'string', required: false, description: 'e.g. "warm", "formal"' },
|
|
268
|
+
},
|
|
269
|
+
async run({ original, style }) {
|
|
270
|
+
const brain = useBrain()
|
|
271
|
+
if (!brain) return { error: 'brain unavailable' }
|
|
272
|
+
const { text } = await brain.chat({
|
|
273
|
+
system: 'You write replies that feel like a thoughtful human wrote them.',
|
|
274
|
+
messages: [{ role: 'user', content: `${style ? `Tone: ${style}.\n\n` : ''}Reply to:\n${original}` }],
|
|
275
|
+
})
|
|
276
|
+
return { draft: text }
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Per-call overrides win: `brain.complete({ prompt, tier: 'large' })` upgrades
|
|
283
|
+
even a small-tier action when the user asked for "detailed".
|
|
284
|
+
|
|
285
|
+
**Permission required.** A space whose actions call brain must declare
|
|
286
|
+
`{{.ID}}:brain` in `permissions.catalog` and map each brain-using action
|
|
287
|
+
to it under `permissions.actions`. The user grants this at install time.
|
|
288
|
+
|
|
289
|
+
```jsonc
|
|
290
|
+
// space.manifest.json
|
|
291
|
+
{
|
|
292
|
+
"permissions": {
|
|
293
|
+
"actions": {
|
|
294
|
+
"summarizeThread": "{{.ID}}:brain",
|
|
295
|
+
"composeReply": "{{.ID}}:brain"
|
|
296
|
+
},
|
|
297
|
+
"catalog": [
|
|
298
|
+
{ "id": "{{.ID}}:brain", "label": "Use AI for summaries & drafts" }
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Without the grant, `useBrain()` throws `BrainPermissionDenied` synchronously
|
|
305
|
+
on the first method call so the action can fall back gracefully.
|
|
306
|
+
|
|
234
307
|
### Wiring (REQUIRED — easy to miss)
|
|
235
308
|
|
|
236
309
|
Each action becomes a tool named **`{spaceID}.{actionName}`** (dot, not underscore). The operator only pre-registers actions that are **explicitly whitelisted** in the agent config. An empty `tools: []` means the agent sees zero action tools and will report "unknown tool".
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Space entry — exports pages, widgets, and actions for the host loader.
|
|
2
2
|
// `construct dev` regenerates this from space.manifest.json on changes.
|
|
3
|
+
import './style.css'
|
|
3
4
|
import IndexPage from './pages/index.vue'
|
|
4
5
|
import SettingsPage from './pages/settings.vue'
|
|
5
6
|
import { actions } from './actions'
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
"@construct-space/cli": "latest",
|
|
21
21
|
"@construct-space/sdk": "latest",
|
|
22
22
|
"@eslint/js": "^10.0.1",
|
|
23
|
+
"@tailwindcss/vite": "^4.1.17",
|
|
23
24
|
"@vitejs/plugin-vue": "^6.0.6",
|
|
24
25
|
"eslint": "^10.2.1",
|
|
25
26
|
"eslint-plugin-vue": "^10.0.0",
|
|
26
27
|
"lucide-vue-next": "^1.0.0",
|
|
28
|
+
"tailwindcss": "^4.1.17",
|
|
27
29
|
"typescript": "^5.9.3",
|
|
28
30
|
"typescript-eslint": "^8.0.0",
|
|
29
31
|
"vite": "^8.0.12",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
2
3
|
import vue from '@vitejs/plugin-vue'
|
|
3
4
|
import { resolve } from 'path'
|
|
4
5
|
|
|
@@ -19,8 +20,9 @@ const hostExternals = [
|
|
|
19
20
|
'dexie',
|
|
20
21
|
'zod',
|
|
21
22
|
'@construct-space/ui',
|
|
22
|
-
'@construct/sdk',
|
|
23
23
|
'@construct-space/sdk',
|
|
24
|
+
'@construct-space/graph',
|
|
25
|
+
'@construct/sdk', // legacy alias — keep during cutover
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
function makeGlobals(externals: string[]): Record<string, string> {
|
|
@@ -37,7 +39,7 @@ function makeGlobals(externals: string[]): Record<string, string> {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export default defineConfig({
|
|
40
|
-
plugins: [vue()],
|
|
42
|
+
plugins: [tailwindcss(), vue()],
|
|
41
43
|
build: {
|
|
42
44
|
lib: {
|
|
43
45
|
entry: resolve(__dirname, 'src/entry.ts'),
|