@construct-space/cli 1.6.0 → 1.6.3

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 CHANGED
@@ -5354,7 +5354,7 @@ async function scaffold(nameArg, options) {
5354
5354
  // src/commands/build.ts
5355
5355
  init_source();
5356
5356
  import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync3, renameSync, statSync as statSync3 } from "fs";
5357
- import { join as join6 } from "path";
5357
+ import { extname as extname3, join as join6 } from "path";
5358
5358
  import { createHash } from "crypto";
5359
5359
 
5360
5360
  // node_modules/ora/index.js
@@ -8026,6 +8026,11 @@ function readRaw(dir) {
8026
8026
  const data = readFileSync2(path, "utf-8");
8027
8027
  return JSON.parse(data);
8028
8028
  }
8029
+ function write(dir, m) {
8030
+ const path = join3(dir, MANIFEST_FILE);
8031
+ writeFileSync2(path, JSON.stringify(m, null, 2) + `
8032
+ `);
8033
+ }
8029
8034
  function writeWithBuild(dir, raw, build) {
8030
8035
  raw.build = build;
8031
8036
  writeFileSync2(join3(dir, "manifest.json"), JSON.stringify(raw, null, 2) + `
@@ -8109,7 +8114,7 @@ function resolvePages(m, root, prefix) {
8109
8114
  if (existsSync4(legacyFullPath)) {
8110
8115
  let varName = "IndexPage";
8111
8116
  if (p.path) {
8112
- varName = capitalize(p.path.replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
8117
+ varName = capitalize(p.path.replace(/\[([^\]]+)\]/g, "$1").replace(/[\/:]/g, "-").replace(/-+/g, "-")) + "Page";
8113
8118
  }
8114
8119
  return { varName, importPath: prefix + legacyComponent, path: p.path };
8115
8120
  }
@@ -8243,41 +8248,245 @@ function bundleAgentDir(srcDir, distDir) {
8243
8248
  }
8244
8249
 
8245
8250
  // src/commands/build.ts
8251
+ function stripComments(source) {
8252
+ let out = "";
8253
+ let i = 0;
8254
+ let quote = null;
8255
+ let escaped = false;
8256
+ while (i < source.length) {
8257
+ const ch = source[i];
8258
+ const next = source[i + 1];
8259
+ if (quote) {
8260
+ out += ch;
8261
+ if (escaped) {
8262
+ escaped = false;
8263
+ } else if (ch === "\\") {
8264
+ escaped = true;
8265
+ } else if (ch === quote) {
8266
+ quote = null;
8267
+ }
8268
+ i++;
8269
+ continue;
8270
+ }
8271
+ if (ch === '"' || ch === "'" || ch === "`") {
8272
+ quote = ch;
8273
+ out += ch;
8274
+ i++;
8275
+ continue;
8276
+ }
8277
+ if (ch === "/" && next === "/") {
8278
+ while (i < source.length && source[i] !== `
8279
+ `) {
8280
+ out += " ";
8281
+ i++;
8282
+ }
8283
+ continue;
8284
+ }
8285
+ if (ch === "/" && next === "*") {
8286
+ out += " ";
8287
+ i += 2;
8288
+ while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) {
8289
+ out += source[i] === `
8290
+ ` ? `
8291
+ ` : " ";
8292
+ i++;
8293
+ }
8294
+ if (i < source.length) {
8295
+ out += " ";
8296
+ i += 2;
8297
+ }
8298
+ continue;
8299
+ }
8300
+ out += ch;
8301
+ i++;
8302
+ }
8303
+ return out;
8304
+ }
8305
+ function findMatchingBrace(source, openIndex) {
8306
+ let depth = 0;
8307
+ let quote = null;
8308
+ let escaped = false;
8309
+ for (let i = openIndex;i < source.length; i++) {
8310
+ const ch = source[i];
8311
+ if (quote) {
8312
+ if (escaped)
8313
+ escaped = false;
8314
+ else if (ch === "\\")
8315
+ escaped = true;
8316
+ else if (ch === quote)
8317
+ quote = null;
8318
+ continue;
8319
+ }
8320
+ if (ch === '"' || ch === "'" || ch === "`") {
8321
+ quote = ch;
8322
+ continue;
8323
+ }
8324
+ if (ch === "{")
8325
+ depth++;
8326
+ else if (ch === "}") {
8327
+ depth--;
8328
+ if (depth === 0)
8329
+ return i;
8330
+ }
8331
+ }
8332
+ return -1;
8333
+ }
8334
+ function objectLiteralAfter(source, start) {
8335
+ const open = source.indexOf("{", start);
8336
+ if (open < 0)
8337
+ return null;
8338
+ const close = findMatchingBrace(source, open);
8339
+ if (close < 0)
8340
+ return null;
8341
+ return source.slice(open + 1, close);
8342
+ }
8343
+ function actionsObject(source) {
8344
+ const stripped = stripComments(source);
8345
+ const match = /export\s+const\s+actions\s*=/.exec(stripped);
8346
+ if (!match)
8347
+ return null;
8348
+ return objectLiteralAfter(stripped, match.index + match[0].length);
8349
+ }
8350
+ function objectEntries(body) {
8351
+ const entries = [];
8352
+ let i = 0;
8353
+ while (i < body.length) {
8354
+ const match = /\s*,?\s*([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(i));
8355
+ if (!match)
8356
+ break;
8357
+ const name = match[1];
8358
+ const valueStart = i + match.index + match[0].length;
8359
+ const open = body.indexOf("{", valueStart);
8360
+ if (open < 0)
8361
+ break;
8362
+ if (body.slice(valueStart, open).trim()) {
8363
+ i = valueStart;
8364
+ continue;
8365
+ }
8366
+ const close = findMatchingBrace(body, open);
8367
+ if (close < 0)
8368
+ break;
8369
+ entries.push({ name, body: body.slice(open + 1, close) });
8370
+ i = close + 1;
8371
+ }
8372
+ return entries;
8373
+ }
8374
+ function stringProperty(body, name) {
8375
+ const match = new RegExp(`${name}\\s*:\\s*['"\`]([^'"\`]*)['"\`]`).exec(body);
8376
+ return match?.[1];
8377
+ }
8378
+ function booleanProperty(body, name) {
8379
+ const match = new RegExp(`${name}\\s*:\\s*(true|false)`).exec(body);
8380
+ if (!match)
8381
+ return;
8382
+ return match[1] === "true";
8383
+ }
8384
+ function propertyObject(body, name) {
8385
+ const match = new RegExp(`${name}\\s*:`).exec(body);
8386
+ if (!match)
8387
+ return null;
8388
+ return objectLiteralAfter(body, match.index + match[0].length);
8389
+ }
8246
8390
  function extractActionMetadata(actionsPath) {
8247
8391
  try {
8248
- const source = readFileSync4(actionsPath, "utf-8");
8392
+ const actionsBody = actionsObject(readFileSync4(actionsPath, "utf-8"));
8393
+ if (!actionsBody)
8394
+ return null;
8249
8395
  const result = {};
8250
- const actionPattern = /(\w+)\s*:\s*\{[^}]*description\s*:\s*['"`]([^'"`]+)['"`]/g;
8251
- let match;
8252
- while ((match = actionPattern.exec(source)) !== null) {
8253
- const actionId = match[1];
8254
- const description = match[2];
8255
- result[actionId] = { description };
8256
- }
8257
- for (const actionId of Object.keys(result)) {
8258
- const paramBlockPattern = new RegExp(`${actionId}\\s*:\\s*\\{[\\s\\S]*?params\\s*:\\s*\\{([\\s\\S]*?)\\}\\s*,?\\s*(?:run|\\})`);
8259
- const paramMatch = source.match(paramBlockPattern);
8260
- if (paramMatch?.[1]) {
8396
+ for (const action of objectEntries(actionsBody)) {
8397
+ const description = stringProperty(action.body, "description");
8398
+ if (!description)
8399
+ continue;
8400
+ const metadata = { description };
8401
+ const paramsBody = propertyObject(action.body, "params");
8402
+ if (paramsBody) {
8261
8403
  const params = {};
8262
- const paramEntryPattern = /(\w+)\s*:\s*\{\s*type\s*:\s*['"`](\w+)['"`](?:\s*,\s*description\s*:\s*['"`]([^'"`]*)['"`])?(?:\s*,\s*required\s*:\s*(true|false))?\s*\}/g;
8263
- let pm;
8264
- while ((pm = paramEntryPattern.exec(paramMatch[1])) !== null) {
8265
- params[pm[1]] = {
8266
- type: pm[2],
8267
- ...pm[3] ? { description: pm[3] } : {},
8268
- ...pm[4] === "true" ? { required: true } : {}
8404
+ for (const param of objectEntries(paramsBody)) {
8405
+ const type = stringProperty(param.body, "type");
8406
+ if (!type)
8407
+ continue;
8408
+ const required = booleanProperty(param.body, "required");
8409
+ params[param.name] = {
8410
+ type,
8411
+ ...stringProperty(param.body, "description") ? { description: stringProperty(param.body, "description") } : {},
8412
+ ...required === true ? { required: true } : {}
8269
8413
  };
8270
8414
  }
8271
- if (Object.keys(params).length > 0) {
8272
- result[actionId].params = params;
8273
- }
8415
+ if (Object.keys(params).length > 0)
8416
+ metadata.params = params;
8274
8417
  }
8418
+ result[action.name] = metadata;
8275
8419
  }
8276
8420
  return Object.keys(result).length > 0 ? result : null;
8277
8421
  } catch {
8278
8422
  return null;
8279
8423
  }
8280
8424
  }
8425
+ var ICON_MIME = {
8426
+ ".svg": "image/svg+xml",
8427
+ ".png": "image/png",
8428
+ ".jpg": "image/jpeg",
8429
+ ".jpeg": "image/jpeg",
8430
+ ".webp": "image/webp",
8431
+ ".gif": "image/gif",
8432
+ ".ico": "image/x-icon"
8433
+ };
8434
+ function isInlineableIcon(value) {
8435
+ if (typeof value !== "string" || !value)
8436
+ return false;
8437
+ if (value.startsWith("i-") || value.startsWith("lucide:"))
8438
+ return false;
8439
+ if (value.startsWith("data:"))
8440
+ return false;
8441
+ if (/^https?:\/\//.test(value))
8442
+ return false;
8443
+ return extname3(value).toLowerCase() in ICON_MIME;
8444
+ }
8445
+ function resolveIconPath(root, ref) {
8446
+ const rel = ref.replace(/^\.?\//, "");
8447
+ for (const candidate of [join6(root, rel), join6(root, "src", rel)]) {
8448
+ if (existsSync6(candidate))
8449
+ return candidate;
8450
+ }
8451
+ return null;
8452
+ }
8453
+ function inlineIcon(root, ref) {
8454
+ const abs = resolveIconPath(root, ref);
8455
+ if (!abs)
8456
+ return null;
8457
+ const mime = ICON_MIME[extname3(abs).toLowerCase()] || "application/octet-stream";
8458
+ const b64 = readFileSync4(abs).toString("base64");
8459
+ return `data:${mime};base64,${b64}`;
8460
+ }
8461
+ function inlineManifestIcons(root, raw) {
8462
+ if (isInlineableIcon(raw.icon)) {
8463
+ const data = inlineIcon(root, raw.icon);
8464
+ if (data)
8465
+ raw.icon = data;
8466
+ else
8467
+ console.warn(source_default.yellow(` Icon not found: ${raw.icon}`));
8468
+ }
8469
+ const nav = raw.navigation;
8470
+ if (nav && isInlineableIcon(nav.icon)) {
8471
+ const data = inlineIcon(root, nav.icon);
8472
+ if (data)
8473
+ nav.icon = data;
8474
+ else
8475
+ console.warn(source_default.yellow(` navigation.icon not found: ${nav.icon}`));
8476
+ }
8477
+ const pages = raw.pages;
8478
+ if (Array.isArray(pages)) {
8479
+ for (const p of pages) {
8480
+ if (isInlineableIcon(p.icon)) {
8481
+ const data = inlineIcon(root, p.icon);
8482
+ if (data)
8483
+ p.icon = data;
8484
+ else
8485
+ console.warn(source_default.yellow(` page icon not found: ${p.icon}`));
8486
+ }
8487
+ }
8488
+ }
8489
+ }
8281
8490
  async function build(options) {
8282
8491
  const root = process.cwd();
8283
8492
  if (!exists(root)) {
@@ -8326,6 +8535,7 @@ async function build(options) {
8326
8535
  const bundleData = readFileSync4(bundlePath);
8327
8536
  const checksum = createHash("sha256").update(bundleData).digest("hex");
8328
8537
  const raw = readRaw(root);
8538
+ inlineManifestIcons(root, raw);
8329
8539
  const actionsPath = join6(root, "src", "actions.ts");
8330
8540
  if (existsSync6(actionsPath)) {
8331
8541
  const actionMeta = extractActionMetadata(actionsPath);
@@ -10319,6 +10529,22 @@ async function publish(options) {
10319
10529
  init_source();
10320
10530
  import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
10321
10531
  import { join as join15 } from "path";
10532
+
10533
+ // src/lib/pagePaths.ts
10534
+ function pageComponentFromPath(path) {
10535
+ const clean = path.replace(/^\/+|\/+$/g, "");
10536
+ if (!clean)
10537
+ return "pages/index.vue";
10538
+ const segments = clean.split("/").map((segment) => {
10539
+ if (segment.startsWith(":") && segment.length > 1) {
10540
+ return `[${segment.slice(1)}]`;
10541
+ }
10542
+ return segment;
10543
+ });
10544
+ return `pages/${segments.join("/")}.vue`;
10545
+ }
10546
+
10547
+ // src/commands/validate.ts
10322
10548
  function validate3() {
10323
10549
  const root = process.cwd();
10324
10550
  if (!exists(root)) {
@@ -10335,7 +10561,7 @@ function validate3() {
10335
10561
  }
10336
10562
  let warnings = 0;
10337
10563
  for (const page of m.pages) {
10338
- const component = page.component || (page.path === "" ? "pages/index.vue" : `pages/${page.path}.vue`);
10564
+ const component = page.component || pageComponentFromPath(page.path);
10339
10565
  const fullPath = join15(root, "src", component);
10340
10566
  if (!existsSync12(fullPath)) {
10341
10567
  console.log(source_default.yellow(` \u26A0 Page component not found: src/${component}`));
@@ -10386,7 +10612,7 @@ function check() {
10386
10612
  }
10387
10613
  let warnings = 0;
10388
10614
  for (const page of m.pages) {
10389
- const component = page.component || (page.path === "" ? "pages/index.vue" : `pages/${page.path}.vue`);
10615
+ const component = page.component || pageComponentFromPath(page.path);
10390
10616
  if (!existsSync13(join16(root, "src", component))) {
10391
10617
  console.log(source_default.yellow(` \u26A0 Page not found: src/${component}`));
10392
10618
  warnings++;
@@ -10949,6 +11175,33 @@ async function graphPush() {
10949
11175
  process.exit(1);
10950
11176
  }
10951
11177
  }
11178
+ function parseDefaultValue(raw) {
11179
+ const value = raw.trim();
11180
+ try {
11181
+ return JSON.parse(value);
11182
+ } catch {}
11183
+ const quote = value[0];
11184
+ if ((quote === "'" || quote === '"') && value.endsWith(quote)) {
11185
+ return value.slice(1, -1).replace(/\\(['"\\bfnrt])/g, (_, ch) => {
11186
+ switch (ch) {
11187
+ case "b":
11188
+ return "\b";
11189
+ case "f":
11190
+ return "\f";
11191
+ case "n":
11192
+ return `
11193
+ `;
11194
+ case "r":
11195
+ return "\r";
11196
+ case "t":
11197
+ return "\t";
11198
+ default:
11199
+ return ch;
11200
+ }
11201
+ });
11202
+ }
11203
+ return value;
11204
+ }
10952
11205
  function parseModelFile(content, fileName) {
10953
11206
  const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
10954
11207
  if (!modelMatch)
@@ -10976,11 +11229,7 @@ function parseModelFile(content, fileName) {
10976
11229
  field.validation = "url";
10977
11230
  const defaultMatch = modifiers.match(/\.default\((.+?)\)/);
10978
11231
  if (defaultMatch) {
10979
- try {
10980
- field.default = JSON.parse(defaultMatch[1]);
10981
- } catch {
10982
- field.default = defaultMatch[1];
10983
- }
11232
+ field.default = parseDefaultValue(defaultMatch[1]);
10984
11233
  }
10985
11234
  }
10986
11235
  fields.push(field);
@@ -11162,10 +11411,41 @@ function parseModelFields(content, fileName) {
11162
11411
  }
11163
11412
  return { name: modelMatch[1], fields };
11164
11413
  }
11414
+
11415
+ // src/commands/graph/fork.ts
11416
+ init_source();
11417
+ var spaceIDRegex = /^[a-z][a-z0-9-]*$/;
11418
+ function forkManifest(root, newSpaceID) {
11419
+ const id = newSpaceID.trim();
11420
+ if (!spaceIDRegex.test(id)) {
11421
+ throw new Error("space id must be lowercase alphanumeric with hyphens, starting with a letter");
11422
+ }
11423
+ if (!exists(root)) {
11424
+ throw new Error("No space.manifest.json found in current directory");
11425
+ }
11426
+ const m = read(root);
11427
+ if (m.id === id) {
11428
+ throw new Error(`space.manifest.json already uses id "${id}"`);
11429
+ }
11430
+ const oldSpaceID = m.id;
11431
+ m.id = id;
11432
+ write(root, m);
11433
+ return { oldSpaceID, newSpaceID: id };
11434
+ }
11435
+ function graphFork(newSpaceID) {
11436
+ try {
11437
+ const result = forkManifest(process.cwd(), newSpaceID);
11438
+ console.log(source_default.green(`Forked graph space id: ${result.oldSpaceID} -> ${result.newSpaceID}`));
11439
+ console.log(source_default.dim("Run `construct graph push` to register the forked schema."));
11440
+ } catch (err) {
11441
+ console.error(source_default.red(err.message));
11442
+ process.exit(1);
11443
+ }
11444
+ }
11165
11445
  // package.json
11166
11446
  var package_default = {
11167
11447
  name: "@construct-space/cli",
11168
- version: "1.6.0",
11448
+ version: "1.6.3",
11169
11449
  description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
11170
11450
  type: "module",
11171
11451
  bin: {
@@ -11229,6 +11509,7 @@ var graph = program2.command("graph").description("Construct Graph \u2014 data m
11229
11509
  graph.command("init").description("Initialize Graph in a space project").action(() => graphInit());
11230
11510
  graph.command("generate <model> [fields...]").alias("g").description("Generate a data model").option("--access <rules>", "Access rules (e.g. read:member,create:member,update:owner,delete:admin)").action((model, fields, opts) => generate2(model, fields, opts));
11231
11511
  graph.command("push").description("Register models with the Graph service").action(async () => graphPush());
11512
+ graph.command("fork <new-space-id>").description("Rewrite this space manifest to a new graph space id").action((newSpaceID) => graphFork(newSpaceID));
11232
11513
  graph.command("migrate").description("Compare local models with server schema and apply changes").option("--apply", "Apply destructive changes (drop columns, alter constraints)").action(async (opts) => graphMigrate(opts));
11233
11514
  graph.command("spaces").alias("list").alias("ls").description("List spaces published by your org").option("--org <id>", "Override org id").option("--bundle <id>", "Filter to a single bundle").option("--json", "Output JSON").action(async (opts) => (await Promise.resolve().then(() => (init_spaces(), exports_spaces))).spacesList(opts));
11234
11515
  var bundles = graph.command("bundles").description("Manage space bundles (publisher grouping)");
@@ -100,6 +100,17 @@ All exported from the package root:
100
100
 
101
101
  **Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
102
102
 
103
+ ### Component gotchas
104
+
105
+ - **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
106
+ - **`Select`** doesn't accept `multiple` — render a chip-toggle list yourself for multi-select.
107
+ - **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
108
+ - **`Table` rowKey** is a string field name on the row object, not a function.
109
+ - **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
110
+ - **`Card`** slots: `header | accessory | default | footer | footer-end`. Use `title`/`description` props for the header to get standard styling; put right-side actions in `accessory`.
111
+ - **`ConfirmationModal`** emits `@confirm` and `@cancel` — wire both, otherwise dismissal does nothing.
112
+ - **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` — use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
113
+
103
114
  ```vue
104
115
  <script setup lang="ts">
105
116
  import { Card, Button, Modal, Badge } from '@construct-space/ui'
@@ -185,15 +196,26 @@ await tasks.remove(id)
185
196
 
186
197
  ### Pitfalls
187
198
 
188
- - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`.
199
+ - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`. Pattern:
200
+ ```ts
201
+ function parseLabels(raw: unknown): string[] {
202
+ if (Array.isArray(raw)) return raw as string[]
203
+ if (typeof raw === 'string' && raw.trim()) {
204
+ try { const v = JSON.parse(raw); return Array.isArray(v) ? v : [] } catch { return [] }
205
+ }
206
+ return []
207
+ }
208
+ ```
189
209
  - **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
190
210
  - **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
211
+ - **Schema ownership is sticky**: the first profile to `construct graph push` becomes the schema owner. Subsequent pushes from a different profile fail with "Ownership check failed". Bump the space `id` to fork, or push from the original owner profile.
212
+ - **`access.member()` requires org context** — use `access.authenticated()` for org-scoped models that any logged-in member should be able to read/write. `member()` is for stricter membership checks.
191
213
 
192
214
  ---
193
215
 
194
216
  ## Actions (agent surface)
195
217
 
196
- `src/actions.ts` exports an `actions` object — each entry becomes an agent-callable tool via `space_run_action`.
218
+ `src/actions.ts` exports an `actions` object — each entry is exposed to the space's agent as a first-class tool.
197
219
 
198
220
  ```ts
199
221
  export const actions = {
@@ -208,7 +230,24 @@ export const actions = {
208
230
  }
209
231
  ```
210
232
 
211
- The agent (configured in `agent/config.md`) calls `space_list_actions` then `space_run_action`. Keep descriptions tight and specific they're the only docstring the agent sees.
233
+ ### Wiring (REQUIREDeasy to miss)
234
+
235
+ 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".
236
+
237
+ ```yaml
238
+ # agent/config.md frontmatter
239
+ ---
240
+ id: {{.ID}}
241
+ tools:
242
+ - {{.ID}}.createTask
243
+ - {{.ID}}.listTasks
244
+ - {{.ID}}.updateTask
245
+ ---
246
+ ```
247
+
248
+ Fallback: agents can also discover/invoke actions via the generic `space_list_actions` + `space_run_action` bridge tools. Prefer the whitelist — it gives the model proper tool definitions instead of stringly-typed action names.
249
+
250
+ Keep action descriptions tight and specific — they're the only docstring the agent sees.
212
251
 
213
252
  ---
214
253
 
@@ -25,12 +25,12 @@
25
25
  "eslint-plugin-vue": "^10.0.0",
26
26
  "lucide-vue-next": "^1.0.0",
27
27
  "typescript": "^5.9.3",
28
- "typescript-eslint": "^9.0.0",
28
+ "typescript-eslint": "^8.0.0",
29
29
  "vite": "^6.3.5",
30
30
  "vue-tsc": "^3.2.5"
31
31
  },
32
32
  "dependencies": {
33
- "@construct-space/graph": "^0.5.0",
34
- "@construct-space/ui": "^0.5.2"
33
+ "@construct-space/graph": "^0.6.2",
34
+ "@construct-space/ui": "^0.6.1"
35
35
  }
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@construct-space/cli",
3
- "version": "1.6.0",
3
+ "version": "1.6.3",
4
4
  "description": "Construct CLI — scaffold, build, develop, and publish spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,6 +100,17 @@ All exported from the package root:
100
100
 
101
101
  **Composables**: `useNotification`, `notify`, `useAuth`, `useTheme`, `useClipboard`, `useMediaQuery`, `useFormValidation`, `useKeyboard`/`useHotkey`, `useClickOutside`, `useEscapeKey`, `useLocalStorage`, `useAsync`, `useDebounce`/`useThrottle`, `useToggle`, `useSearch`, `useIntersectionObserver`
102
102
 
103
+ ### Component gotchas
104
+
105
+ - **`Avatar` sizes**: only `sm | md | lg` (no `xs`).
106
+ - **`Select`** doesn't accept `multiple` — render a chip-toggle list yourself for multi-select.
107
+ - **`Table`** column `align` literals must be `as const` to typecheck (`align: 'right' as const`).
108
+ - **`Table` rowKey** is a string field name on the row object, not a function.
109
+ - **`Modal`** uses `v-model:open` (not `v-model`); slots: `header | body | footer | accessory`.
110
+ - **`Card`** slots: `header | accessory | default | footer | footer-end`. Use `title`/`description` props for the header to get standard styling; put right-side actions in `accessory`.
111
+ - **`ConfirmationModal`** emits `@confirm` and `@cancel` — wire both, otherwise dismissal does nothing.
112
+ - **Settings pages**: never raw `<select>`/`<input>`/`<textarea>` — use `Select`, `Switch`, `Input`, `Textarea` with `FormField` labels. Persist with `localStorage` (key prefix per space).
113
+
103
114
  ```vue
104
115
  <script setup lang="ts">
105
116
  import { Card, Button, Modal, Badge } from '@construct-space/ui'
@@ -185,15 +196,26 @@ await tasks.remove(id)
185
196
 
186
197
  ### Pitfalls
187
198
 
188
- - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`.
199
+ - **`field.json()` columns** must receive a JSON-stringified value on write: `labels: JSON.stringify(arr)`. On read, parse back if it returns a string. Sending a JS array directly triggers `column is of type jsonb but expression is of type record`. Pattern:
200
+ ```ts
201
+ function parseLabels(raw: unknown): string[] {
202
+ if (Array.isArray(raw)) return raw as string[]
203
+ if (typeof raw === 'string' && raw.trim()) {
204
+ try { const v = JSON.parse(raw); return Array.isArray(v) ? v : [] } catch { return [] }
205
+ }
206
+ return []
207
+ }
208
+ ```
189
209
  - **Topological sort on push**: avoid `relation.belongsTo()` if push fails — use a `field.string()` foreign-key column instead and join manually in composables.
190
210
  - **Lazy provisioning**: first query for a new tenant auto-creates the schema; expect a one-time slower request.
211
+ - **Schema ownership is sticky**: the first profile to `construct graph push` becomes the schema owner. Subsequent pushes from a different profile fail with "Ownership check failed". Bump the space `id` to fork, or push from the original owner profile.
212
+ - **`access.member()` requires org context** — use `access.authenticated()` for org-scoped models that any logged-in member should be able to read/write. `member()` is for stricter membership checks.
191
213
 
192
214
  ---
193
215
 
194
216
  ## Actions (agent surface)
195
217
 
196
- `src/actions.ts` exports an `actions` object — each entry becomes an agent-callable tool via `space_run_action`.
218
+ `src/actions.ts` exports an `actions` object — each entry is exposed to the space's agent as a first-class tool.
197
219
 
198
220
  ```ts
199
221
  export const actions = {
@@ -208,7 +230,24 @@ export const actions = {
208
230
  }
209
231
  ```
210
232
 
211
- The agent (configured in `agent/config.md`) calls `space_list_actions` then `space_run_action`. Keep descriptions tight and specific they're the only docstring the agent sees.
233
+ ### Wiring (REQUIREDeasy to miss)
234
+
235
+ 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".
236
+
237
+ ```yaml
238
+ # agent/config.md frontmatter
239
+ ---
240
+ id: {{.ID}}
241
+ tools:
242
+ - {{.ID}}.createTask
243
+ - {{.ID}}.listTasks
244
+ - {{.ID}}.updateTask
245
+ ---
246
+ ```
247
+
248
+ Fallback: agents can also discover/invoke actions via the generic `space_list_actions` + `space_run_action` bridge tools. Prefer the whitelist — it gives the model proper tool definitions instead of stringly-typed action names.
249
+
250
+ Keep action descriptions tight and specific — they're the only docstring the agent sees.
212
251
 
213
252
  ---
214
253
 
@@ -25,12 +25,12 @@
25
25
  "eslint-plugin-vue": "^10.0.0",
26
26
  "lucide-vue-next": "^1.0.0",
27
27
  "typescript": "^5.9.3",
28
- "typescript-eslint": "^9.0.0",
28
+ "typescript-eslint": "^8.0.0",
29
29
  "vite": "^6.3.5",
30
30
  "vue-tsc": "^3.2.5"
31
31
  },
32
32
  "dependencies": {
33
- "@construct-space/graph": "^0.5.0",
34
- "@construct-space/ui": "^0.5.2"
33
+ "@construct-space/graph": "^0.6.2",
34
+ "@construct-space/ui": "^0.6.1"
35
35
  }
36
36
  }