@construct-space/cli 1.7.7 → 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 +101 -109
- 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/vite.config.ts.tmpl +2 -1
- 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/vite.config.ts.tmpl +2 -1
package/dist/index.js
CHANGED
|
@@ -8527,16 +8527,16 @@ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
|
8527
8527
|
import { join as join9 } from "path";
|
|
8528
8528
|
import { createHash as createHash2 } from "crypto";
|
|
8529
8529
|
|
|
8530
|
-
// node_modules/chokidar/index.js
|
|
8530
|
+
// node_modules/chokidar/esm/index.js
|
|
8531
|
+
import { stat as statcb } from "fs";
|
|
8532
|
+
import { stat as stat3, readdir as readdir2 } from "fs/promises";
|
|
8531
8533
|
import { EventEmitter } from "events";
|
|
8532
|
-
import
|
|
8533
|
-
import { readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
8534
|
-
import * as sp2 from "path";
|
|
8534
|
+
import * as sysPath2 from "path";
|
|
8535
8535
|
|
|
8536
|
-
// node_modules/readdirp/index.js
|
|
8537
|
-
import { lstat, readdir, realpath
|
|
8538
|
-
import { join as pjoin, relative as prelative, resolve as presolve, sep as psep } from "path";
|
|
8536
|
+
// node_modules/readdirp/esm/index.js
|
|
8537
|
+
import { stat, lstat, readdir, realpath } from "fs/promises";
|
|
8539
8538
|
import { Readable } from "stream";
|
|
8539
|
+
import { resolve as presolve, relative as prelative, join as pjoin, sep as psep } from "path";
|
|
8540
8540
|
var EntryTypes = {
|
|
8541
8541
|
FILE_TYPE: "files",
|
|
8542
8542
|
DIR_TYPE: "directories",
|
|
@@ -8592,20 +8592,6 @@ var normalizeFilter = (filter) => {
|
|
|
8592
8592
|
};
|
|
8593
8593
|
|
|
8594
8594
|
class ReaddirpStream extends Readable {
|
|
8595
|
-
parents;
|
|
8596
|
-
reading;
|
|
8597
|
-
parent;
|
|
8598
|
-
_stat;
|
|
8599
|
-
_maxDepth;
|
|
8600
|
-
_wantsDir;
|
|
8601
|
-
_wantsFile;
|
|
8602
|
-
_wantsEverything;
|
|
8603
|
-
_root;
|
|
8604
|
-
_isDirent;
|
|
8605
|
-
_statsProp;
|
|
8606
|
-
_rdOptions;
|
|
8607
|
-
_fileFilter;
|
|
8608
|
-
_directoryFilter;
|
|
8609
8595
|
constructor(options = {}) {
|
|
8610
8596
|
super({
|
|
8611
8597
|
objectMode: true,
|
|
@@ -8622,7 +8608,7 @@ class ReaddirpStream extends Readable {
|
|
|
8622
8608
|
} else {
|
|
8623
8609
|
this._stat = statMethod;
|
|
8624
8610
|
}
|
|
8625
|
-
this._maxDepth = opts.depth
|
|
8611
|
+
this._maxDepth = opts.depth ?? defaultOptions.depth;
|
|
8626
8612
|
this._wantsDir = type ? DIR_TYPES.has(type) : false;
|
|
8627
8613
|
this._wantsFile = type ? FILE_TYPES.has(type) : false;
|
|
8628
8614
|
this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE;
|
|
@@ -8767,11 +8753,11 @@ function readdirp(root, options = {}) {
|
|
|
8767
8753
|
return new ReaddirpStream(options);
|
|
8768
8754
|
}
|
|
8769
8755
|
|
|
8770
|
-
// node_modules/chokidar/handler.js
|
|
8771
|
-
import { watch as fs_watch
|
|
8772
|
-
import {
|
|
8756
|
+
// node_modules/chokidar/esm/handler.js
|
|
8757
|
+
import { watchFile, unwatchFile, watch as fs_watch } from "fs";
|
|
8758
|
+
import { open, stat as stat2, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
|
|
8759
|
+
import * as sysPath from "path";
|
|
8773
8760
|
import { type as osType } from "os";
|
|
8774
|
-
import * as sp from "path";
|
|
8775
8761
|
var STR_DATA = "data";
|
|
8776
8762
|
var STR_END = "end";
|
|
8777
8763
|
var STR_CLOSE = "close";
|
|
@@ -9063,7 +9049,7 @@ var binaryExtensions = new Set([
|
|
|
9063
9049
|
"zip",
|
|
9064
9050
|
"zipx"
|
|
9065
9051
|
]);
|
|
9066
|
-
var isBinaryPath = (filePath) => binaryExtensions.has(
|
|
9052
|
+
var isBinaryPath = (filePath) => binaryExtensions.has(sysPath.extname(filePath).slice(1).toLowerCase());
|
|
9067
9053
|
var foreach = (val, fn) => {
|
|
9068
9054
|
if (val instanceof Set) {
|
|
9069
9055
|
val.forEach(fn);
|
|
@@ -9101,7 +9087,7 @@ function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
|
|
|
9101
9087
|
listener(path);
|
|
9102
9088
|
emitRaw(rawEvent, evPath, { watchedPath: path });
|
|
9103
9089
|
if (evPath && path !== evPath) {
|
|
9104
|
-
fsWatchBroadcast(
|
|
9090
|
+
fsWatchBroadcast(sysPath.resolve(path, evPath), KEY_LISTENERS, sysPath.join(path, evPath));
|
|
9105
9091
|
}
|
|
9106
9092
|
};
|
|
9107
9093
|
try {
|
|
@@ -9216,19 +9202,17 @@ var setFsWatchFileListener = (path, fullPath, options, handlers) => {
|
|
|
9216
9202
|
};
|
|
9217
9203
|
|
|
9218
9204
|
class NodeFsHandler {
|
|
9219
|
-
fsw;
|
|
9220
|
-
_boundHandleError;
|
|
9221
9205
|
constructor(fsW) {
|
|
9222
9206
|
this.fsw = fsW;
|
|
9223
9207
|
this._boundHandleError = (error2) => fsW._handleError(error2);
|
|
9224
9208
|
}
|
|
9225
9209
|
_watchWithNodeFs(path, listener) {
|
|
9226
9210
|
const opts = this.fsw.options;
|
|
9227
|
-
const directory =
|
|
9228
|
-
const basename4 =
|
|
9211
|
+
const directory = sysPath.dirname(path);
|
|
9212
|
+
const basename4 = sysPath.basename(path);
|
|
9229
9213
|
const parent = this.fsw._getWatchedDir(directory);
|
|
9230
9214
|
parent.add(basename4);
|
|
9231
|
-
const absolutePath =
|
|
9215
|
+
const absolutePath = sysPath.resolve(path);
|
|
9232
9216
|
const options = {
|
|
9233
9217
|
persistent: opts.persistent
|
|
9234
9218
|
};
|
|
@@ -9255,8 +9239,8 @@ class NodeFsHandler {
|
|
|
9255
9239
|
if (this.fsw.closed) {
|
|
9256
9240
|
return;
|
|
9257
9241
|
}
|
|
9258
|
-
const dirname3 =
|
|
9259
|
-
const basename4 =
|
|
9242
|
+
const dirname3 = sysPath.dirname(file);
|
|
9243
|
+
const basename4 = sysPath.basename(file);
|
|
9260
9244
|
const parent = this.fsw._getWatchedDir(dirname3);
|
|
9261
9245
|
let prevStats = stats;
|
|
9262
9246
|
if (parent.has(basename4))
|
|
@@ -9339,9 +9323,8 @@ class NodeFsHandler {
|
|
|
9339
9323
|
this.fsw._symlinkPaths.set(full, true);
|
|
9340
9324
|
}
|
|
9341
9325
|
_handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
|
|
9342
|
-
directory =
|
|
9343
|
-
|
|
9344
|
-
throttler = this.fsw._throttle("readdir", throttleKey, 1000);
|
|
9326
|
+
directory = sysPath.join(directory, "");
|
|
9327
|
+
throttler = this.fsw._throttle("readdir", directory, 1000);
|
|
9345
9328
|
if (!throttler)
|
|
9346
9329
|
return;
|
|
9347
9330
|
const previous = this.fsw._getWatchedDir(wh.path);
|
|
@@ -9358,7 +9341,7 @@ class NodeFsHandler {
|
|
|
9358
9341
|
return;
|
|
9359
9342
|
}
|
|
9360
9343
|
const item = entry.path;
|
|
9361
|
-
let path =
|
|
9344
|
+
let path = sysPath.join(directory, item);
|
|
9362
9345
|
current.add(item);
|
|
9363
9346
|
if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path, item)) {
|
|
9364
9347
|
return;
|
|
@@ -9369,7 +9352,7 @@ class NodeFsHandler {
|
|
|
9369
9352
|
}
|
|
9370
9353
|
if (item === target || !target && !previous.has(item)) {
|
|
9371
9354
|
this.fsw._incrReadyCount();
|
|
9372
|
-
path =
|
|
9355
|
+
path = sysPath.join(dir, sysPath.relative(dir, path));
|
|
9373
9356
|
this._addToNodeFs(path, initialAdd, wh, depth + 1);
|
|
9374
9357
|
}
|
|
9375
9358
|
}).on(EV.ERROR, this._boundHandleError);
|
|
@@ -9395,12 +9378,12 @@ class NodeFsHandler {
|
|
|
9395
9378
|
});
|
|
9396
9379
|
}
|
|
9397
9380
|
async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath2) {
|
|
9398
|
-
const parentDir = this.fsw._getWatchedDir(
|
|
9399
|
-
const tracked = parentDir.has(
|
|
9381
|
+
const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir));
|
|
9382
|
+
const tracked = parentDir.has(sysPath.basename(dir));
|
|
9400
9383
|
if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
|
|
9401
9384
|
this.fsw._emit(EV.ADD_DIR, dir, stats);
|
|
9402
9385
|
}
|
|
9403
|
-
parentDir.add(
|
|
9386
|
+
parentDir.add(sysPath.basename(dir));
|
|
9404
9387
|
this.fsw._getWatchedDir(dir);
|
|
9405
9388
|
let throttler;
|
|
9406
9389
|
let closer;
|
|
@@ -9441,7 +9424,7 @@ class NodeFsHandler {
|
|
|
9441
9424
|
const follow = this.fsw.options.followSymlinks;
|
|
9442
9425
|
let closer;
|
|
9443
9426
|
if (stats.isDirectory()) {
|
|
9444
|
-
const absPath =
|
|
9427
|
+
const absPath = sysPath.resolve(path);
|
|
9445
9428
|
const targetPath = follow ? await fsrealpath(path) : path;
|
|
9446
9429
|
if (this.fsw.closed)
|
|
9447
9430
|
return;
|
|
@@ -9455,14 +9438,14 @@ class NodeFsHandler {
|
|
|
9455
9438
|
const targetPath = follow ? await fsrealpath(path) : path;
|
|
9456
9439
|
if (this.fsw.closed)
|
|
9457
9440
|
return;
|
|
9458
|
-
const parent =
|
|
9441
|
+
const parent = sysPath.dirname(wh.watchPath);
|
|
9459
9442
|
this.fsw._getWatchedDir(parent).add(wh.watchPath);
|
|
9460
9443
|
this.fsw._emit(EV.ADD, wh.watchPath, stats);
|
|
9461
9444
|
closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath);
|
|
9462
9445
|
if (this.fsw.closed)
|
|
9463
9446
|
return;
|
|
9464
9447
|
if (targetPath !== undefined) {
|
|
9465
|
-
this.fsw._symlinkPaths.set(
|
|
9448
|
+
this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath);
|
|
9466
9449
|
}
|
|
9467
9450
|
} else {
|
|
9468
9451
|
closer = this._handleFile(wh.watchPath, stats, initialAdd);
|
|
@@ -9480,7 +9463,7 @@ class NodeFsHandler {
|
|
|
9480
9463
|
}
|
|
9481
9464
|
}
|
|
9482
9465
|
|
|
9483
|
-
// node_modules/chokidar/index.js
|
|
9466
|
+
// node_modules/chokidar/esm/index.js
|
|
9484
9467
|
/*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
|
|
9485
9468
|
var SLASH = "/";
|
|
9486
9469
|
var SLASH_SLASH = "//";
|
|
@@ -9488,7 +9471,7 @@ var ONE_DOT = ".";
|
|
|
9488
9471
|
var TWO_DOTS = "..";
|
|
9489
9472
|
var STRING_TYPE = "string";
|
|
9490
9473
|
var BACK_SLASH_RE = /\\/g;
|
|
9491
|
-
var DOUBLE_SLASH_RE =
|
|
9474
|
+
var DOUBLE_SLASH_RE = /\/\//;
|
|
9492
9475
|
var DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
|
|
9493
9476
|
var REPLACER_RE = /^\.[/\\]/;
|
|
9494
9477
|
function arrify(item) {
|
|
@@ -9507,11 +9490,11 @@ function createPattern(matcher) {
|
|
|
9507
9490
|
if (matcher.path === string)
|
|
9508
9491
|
return true;
|
|
9509
9492
|
if (matcher.recursive) {
|
|
9510
|
-
const relative4 =
|
|
9493
|
+
const relative4 = sysPath2.relative(matcher.path, string);
|
|
9511
9494
|
if (!relative4) {
|
|
9512
9495
|
return false;
|
|
9513
9496
|
}
|
|
9514
|
-
return !relative4.startsWith("..") && !
|
|
9497
|
+
return !relative4.startsWith("..") && !sysPath2.isAbsolute(relative4);
|
|
9515
9498
|
}
|
|
9516
9499
|
return false;
|
|
9517
9500
|
};
|
|
@@ -9521,12 +9504,14 @@ function createPattern(matcher) {
|
|
|
9521
9504
|
function normalizePath(path) {
|
|
9522
9505
|
if (typeof path !== "string")
|
|
9523
9506
|
throw new Error("string expected");
|
|
9524
|
-
path =
|
|
9507
|
+
path = sysPath2.normalize(path);
|
|
9525
9508
|
path = path.replace(/\\/g, "/");
|
|
9526
9509
|
let prepend = false;
|
|
9527
9510
|
if (path.startsWith("//"))
|
|
9528
9511
|
prepend = true;
|
|
9529
|
-
|
|
9512
|
+
const DOUBLE_SLASH_RE2 = /\/\//;
|
|
9513
|
+
while (path.match(DOUBLE_SLASH_RE2))
|
|
9514
|
+
path = path.replace(DOUBLE_SLASH_RE2, "/");
|
|
9530
9515
|
if (prepend)
|
|
9531
9516
|
path = "/" + path;
|
|
9532
9517
|
return path;
|
|
@@ -9567,32 +9552,31 @@ var toUnix = (string) => {
|
|
|
9567
9552
|
if (str.startsWith(SLASH_SLASH)) {
|
|
9568
9553
|
prepend = true;
|
|
9569
9554
|
}
|
|
9570
|
-
|
|
9555
|
+
while (str.match(DOUBLE_SLASH_RE)) {
|
|
9556
|
+
str = str.replace(DOUBLE_SLASH_RE, SLASH);
|
|
9557
|
+
}
|
|
9571
9558
|
if (prepend) {
|
|
9572
9559
|
str = SLASH + str;
|
|
9573
9560
|
}
|
|
9574
9561
|
return str;
|
|
9575
9562
|
};
|
|
9576
|
-
var normalizePathToUnix = (path) => toUnix(
|
|
9563
|
+
var normalizePathToUnix = (path) => toUnix(sysPath2.normalize(toUnix(path)));
|
|
9577
9564
|
var normalizeIgnored = (cwd = "") => (path) => {
|
|
9578
9565
|
if (typeof path === "string") {
|
|
9579
|
-
return normalizePathToUnix(
|
|
9566
|
+
return normalizePathToUnix(sysPath2.isAbsolute(path) ? path : sysPath2.join(cwd, path));
|
|
9580
9567
|
} else {
|
|
9581
9568
|
return path;
|
|
9582
9569
|
}
|
|
9583
9570
|
};
|
|
9584
9571
|
var getAbsolutePath = (path, cwd) => {
|
|
9585
|
-
if (
|
|
9572
|
+
if (sysPath2.isAbsolute(path)) {
|
|
9586
9573
|
return path;
|
|
9587
9574
|
}
|
|
9588
|
-
return
|
|
9575
|
+
return sysPath2.join(cwd, path);
|
|
9589
9576
|
};
|
|
9590
9577
|
var EMPTY_SET = Object.freeze(new Set);
|
|
9591
9578
|
|
|
9592
9579
|
class DirEntry {
|
|
9593
|
-
path;
|
|
9594
|
-
_removeWatcher;
|
|
9595
|
-
items;
|
|
9596
9580
|
constructor(dir, removeWatcher) {
|
|
9597
9581
|
this.path = dir;
|
|
9598
9582
|
this._removeWatcher = removeWatcher;
|
|
@@ -9617,7 +9601,7 @@ class DirEntry {
|
|
|
9617
9601
|
await readdir2(dir);
|
|
9618
9602
|
} catch (err) {
|
|
9619
9603
|
if (this._removeWatcher) {
|
|
9620
|
-
this._removeWatcher(
|
|
9604
|
+
this._removeWatcher(sysPath2.dirname(dir), sysPath2.basename(dir));
|
|
9621
9605
|
}
|
|
9622
9606
|
}
|
|
9623
9607
|
}
|
|
@@ -9645,19 +9629,12 @@ var STAT_METHOD_F = "stat";
|
|
|
9645
9629
|
var STAT_METHOD_L = "lstat";
|
|
9646
9630
|
|
|
9647
9631
|
class WatchHelper {
|
|
9648
|
-
fsw;
|
|
9649
|
-
path;
|
|
9650
|
-
watchPath;
|
|
9651
|
-
fullWatchPath;
|
|
9652
|
-
dirParts;
|
|
9653
|
-
followSymlinks;
|
|
9654
|
-
statMethod;
|
|
9655
9632
|
constructor(path, follow, fsw) {
|
|
9656
9633
|
this.fsw = fsw;
|
|
9657
9634
|
const watchPath = path;
|
|
9658
9635
|
this.path = path = path.replace(REPLACER_RE, "");
|
|
9659
9636
|
this.watchPath = watchPath;
|
|
9660
|
-
this.fullWatchPath =
|
|
9637
|
+
this.fullWatchPath = sysPath2.resolve(watchPath);
|
|
9661
9638
|
this.dirParts = [];
|
|
9662
9639
|
this.dirParts.forEach((parts) => {
|
|
9663
9640
|
if (parts.length > 1)
|
|
@@ -9667,7 +9644,7 @@ class WatchHelper {
|
|
|
9667
9644
|
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
|
|
9668
9645
|
}
|
|
9669
9646
|
entryPath(entry) {
|
|
9670
|
-
return
|
|
9647
|
+
return sysPath2.join(this.watchPath, sysPath2.relative(this.watchPath, entry.fullPath));
|
|
9671
9648
|
}
|
|
9672
9649
|
filterPath(entry) {
|
|
9673
9650
|
const { stats } = entry;
|
|
@@ -9682,24 +9659,6 @@ class WatchHelper {
|
|
|
9682
9659
|
}
|
|
9683
9660
|
|
|
9684
9661
|
class FSWatcher extends EventEmitter {
|
|
9685
|
-
closed;
|
|
9686
|
-
options;
|
|
9687
|
-
_closers;
|
|
9688
|
-
_ignoredPaths;
|
|
9689
|
-
_throttled;
|
|
9690
|
-
_streams;
|
|
9691
|
-
_symlinkPaths;
|
|
9692
|
-
_watched;
|
|
9693
|
-
_pendingWrites;
|
|
9694
|
-
_pendingUnlinks;
|
|
9695
|
-
_readyCount;
|
|
9696
|
-
_emitReady;
|
|
9697
|
-
_closePromise;
|
|
9698
|
-
_userIgnored;
|
|
9699
|
-
_readyEmitted;
|
|
9700
|
-
_emitRaw;
|
|
9701
|
-
_boundRemove;
|
|
9702
|
-
_nodeFsHandler;
|
|
9703
9662
|
constructor(_opts = {}) {
|
|
9704
9663
|
super();
|
|
9705
9664
|
this.closed = false;
|
|
@@ -9808,7 +9767,7 @@ class FSWatcher extends EventEmitter {
|
|
|
9808
9767
|
return;
|
|
9809
9768
|
results.forEach((item) => {
|
|
9810
9769
|
if (item)
|
|
9811
|
-
this.add(
|
|
9770
|
+
this.add(sysPath2.dirname(item), sysPath2.basename(_origAdd || item));
|
|
9812
9771
|
});
|
|
9813
9772
|
});
|
|
9814
9773
|
return this;
|
|
@@ -9819,10 +9778,10 @@ class FSWatcher extends EventEmitter {
|
|
|
9819
9778
|
const paths = unifyPaths(paths_);
|
|
9820
9779
|
const { cwd } = this.options;
|
|
9821
9780
|
paths.forEach((path) => {
|
|
9822
|
-
if (!
|
|
9781
|
+
if (!sysPath2.isAbsolute(path) && !this._closers.has(path)) {
|
|
9823
9782
|
if (cwd)
|
|
9824
|
-
path =
|
|
9825
|
-
path =
|
|
9783
|
+
path = sysPath2.join(cwd, path);
|
|
9784
|
+
path = sysPath2.resolve(path);
|
|
9826
9785
|
}
|
|
9827
9786
|
this._closePath(path);
|
|
9828
9787
|
this._addIgnoredPath(path);
|
|
@@ -9866,7 +9825,7 @@ class FSWatcher extends EventEmitter {
|
|
|
9866
9825
|
getWatched() {
|
|
9867
9826
|
const watchList = {};
|
|
9868
9827
|
this._watched.forEach((entry, dir) => {
|
|
9869
|
-
const key = this.options.cwd ?
|
|
9828
|
+
const key = this.options.cwd ? sysPath2.relative(this.options.cwd, dir) : dir;
|
|
9870
9829
|
const index = key || ONE_DOT;
|
|
9871
9830
|
watchList[index] = entry.getChildren().sort();
|
|
9872
9831
|
});
|
|
@@ -9882,9 +9841,9 @@ class FSWatcher extends EventEmitter {
|
|
|
9882
9841
|
return;
|
|
9883
9842
|
const opts = this.options;
|
|
9884
9843
|
if (isWindows)
|
|
9885
|
-
path =
|
|
9844
|
+
path = sysPath2.normalize(path);
|
|
9886
9845
|
if (opts.cwd)
|
|
9887
|
-
path =
|
|
9846
|
+
path = sysPath2.relative(opts.cwd, path);
|
|
9888
9847
|
const args = [path];
|
|
9889
9848
|
if (stats != null)
|
|
9890
9849
|
args.push(stats);
|
|
@@ -9935,7 +9894,7 @@ class FSWatcher extends EventEmitter {
|
|
|
9935
9894
|
return this;
|
|
9936
9895
|
}
|
|
9937
9896
|
if (opts.alwaysStat && stats === undefined && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
|
|
9938
|
-
const fullPath = opts.cwd ?
|
|
9897
|
+
const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path) : path;
|
|
9939
9898
|
let stats2;
|
|
9940
9899
|
try {
|
|
9941
9900
|
stats2 = await stat3(fullPath);
|
|
@@ -9991,8 +9950,8 @@ class FSWatcher extends EventEmitter {
|
|
|
9991
9950
|
const pollInterval = awf.pollInterval;
|
|
9992
9951
|
let timeoutHandler;
|
|
9993
9952
|
let fullPath = path;
|
|
9994
|
-
if (this.options.cwd && !
|
|
9995
|
-
fullPath =
|
|
9953
|
+
if (this.options.cwd && !sysPath2.isAbsolute(path)) {
|
|
9954
|
+
fullPath = sysPath2.join(this.options.cwd, path);
|
|
9996
9955
|
}
|
|
9997
9956
|
const now = new Date;
|
|
9998
9957
|
const writes = this._pendingWrites;
|
|
@@ -10049,7 +10008,7 @@ class FSWatcher extends EventEmitter {
|
|
|
10049
10008
|
return new WatchHelper(path, this.options.followSymlinks, this);
|
|
10050
10009
|
}
|
|
10051
10010
|
_getWatchedDir(directory) {
|
|
10052
|
-
const dir =
|
|
10011
|
+
const dir = sysPath2.resolve(directory);
|
|
10053
10012
|
if (!this._watched.has(dir))
|
|
10054
10013
|
this._watched.set(dir, new DirEntry(dir, this._boundRemove));
|
|
10055
10014
|
return this._watched.get(dir);
|
|
@@ -10060,8 +10019,8 @@ class FSWatcher extends EventEmitter {
|
|
|
10060
10019
|
return Boolean(Number(stats.mode) & 256);
|
|
10061
10020
|
}
|
|
10062
10021
|
_remove(directory, item, isDirectory) {
|
|
10063
|
-
const path =
|
|
10064
|
-
const fullPath =
|
|
10022
|
+
const path = sysPath2.join(directory, item);
|
|
10023
|
+
const fullPath = sysPath2.resolve(path);
|
|
10065
10024
|
isDirectory = isDirectory != null ? isDirectory : this._watched.has(path) || this._watched.has(fullPath);
|
|
10066
10025
|
if (!this._throttle("remove", path, 100))
|
|
10067
10026
|
return;
|
|
@@ -10079,7 +10038,7 @@ class FSWatcher extends EventEmitter {
|
|
|
10079
10038
|
}
|
|
10080
10039
|
let relPath = path;
|
|
10081
10040
|
if (this.options.cwd)
|
|
10082
|
-
relPath =
|
|
10041
|
+
relPath = sysPath2.relative(this.options.cwd, path);
|
|
10083
10042
|
if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
|
|
10084
10043
|
const event = this._pendingWrites.get(relPath).cancelWait();
|
|
10085
10044
|
if (event === EVENTS.ADD)
|
|
@@ -10094,8 +10053,8 @@ class FSWatcher extends EventEmitter {
|
|
|
10094
10053
|
}
|
|
10095
10054
|
_closePath(path) {
|
|
10096
10055
|
this._closeFile(path);
|
|
10097
|
-
const dir =
|
|
10098
|
-
this._getWatchedDir(dir).remove(
|
|
10056
|
+
const dir = sysPath2.dirname(path);
|
|
10057
|
+
this._getWatchedDir(dir).remove(sysPath2.basename(path));
|
|
10099
10058
|
}
|
|
10100
10059
|
_closeFile(path) {
|
|
10101
10060
|
const closers = this._closers.get(path);
|
|
@@ -10389,7 +10348,10 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10389
10348
|
}
|
|
10390
10349
|
msg += `
|
|
10391
10350
|
Fork to a new space_id to publish your own version.`;
|
|
10392
|
-
|
|
10351
|
+
const err = new Error(msg);
|
|
10352
|
+
err.ownerKind = result.owner_kind;
|
|
10353
|
+
err.ownerOrgId = result.owner_org_id;
|
|
10354
|
+
throw err;
|
|
10393
10355
|
}
|
|
10394
10356
|
if (resp.status >= 400) {
|
|
10395
10357
|
const msg = result.error || result.errors?.join("; ") || `server returned ${resp.status}`;
|
|
@@ -10397,6 +10359,20 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10397
10359
|
}
|
|
10398
10360
|
return result;
|
|
10399
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
|
+
}
|
|
10400
10376
|
function setVersionInFiles(root, oldVer, newVer) {
|
|
10401
10377
|
const oldStr = `"version": "${oldVer}"`;
|
|
10402
10378
|
const newStr = `"version": "${newVer}"`;
|
|
@@ -10553,7 +10529,23 @@ async function publish(options) {
|
|
|
10553
10529
|
}
|
|
10554
10530
|
const uploadSpinner = ora("Uploading & building...").start();
|
|
10555
10531
|
try {
|
|
10556
|
-
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
|
+
}
|
|
10557
10549
|
unlinkSync2(tarballPath);
|
|
10558
10550
|
gitSafe(root, "tag", `v${m.version}`);
|
|
10559
10551
|
gitSafe(root, "push", "origin", `v${m.version}`);
|
|
@@ -11509,7 +11501,7 @@ function graphFork(newSpaceID) {
|
|
|
11509
11501
|
// package.json
|
|
11510
11502
|
var package_default = {
|
|
11511
11503
|
name: "@construct-space/cli",
|
|
11512
|
-
version: "1.
|
|
11504
|
+
version: "1.8.0",
|
|
11513
11505
|
description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
|
|
11514
11506
|
type: "module",
|
|
11515
11507
|
bin: {
|
|
@@ -11561,7 +11553,7 @@ program2.command("scaffold [name]").alias("new").alias("create").description("Cr
|
|
|
11561
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));
|
|
11562
11554
|
program2.command("dev").description("Start dev mode with file watching and live reload").action(async () => dev());
|
|
11563
11555
|
program2.command("install").alias("run").description("Install built space to Construct spaces directory").action(() => install());
|
|
11564
|
-
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));
|
|
11565
11557
|
program2.command("validate").description("Validate space.manifest.json").action(() => validate3());
|
|
11566
11558
|
program2.command("check").description("Run type-check (vue-tsc) and linter (eslint)").action(() => check());
|
|
11567
11559
|
program2.command("clean").description("Remove build artifacts").option("--all", "Also remove node_modules and lockfiles").action((opts) => clean(opts));
|
|
@@ -11592,7 +11584,7 @@ space.command("scaffold [name]").alias("new").alias("create").option("--with-tes
|
|
|
11592
11584
|
space.command("build").option("--entry-only").action(async (opts) => build(opts));
|
|
11593
11585
|
space.command("dev").action(async () => dev());
|
|
11594
11586
|
space.command("install").alias("run").action(() => install());
|
|
11595
|
-
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));
|
|
11596
11588
|
space.command("validate").action(() => validate3());
|
|
11597
11589
|
space.command("check").action(() => check());
|
|
11598
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".
|
|
@@ -20,8 +20,9 @@ const hostExternals = [
|
|
|
20
20
|
'dexie',
|
|
21
21
|
'zod',
|
|
22
22
|
'@construct-space/ui',
|
|
23
|
-
'@construct/sdk',
|
|
24
23
|
'@construct-space/sdk',
|
|
24
|
+
'@construct-space/graph',
|
|
25
|
+
'@construct/sdk', // legacy alias — keep during cutover
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
function makeGlobals(externals: string[]): Record<string, string> {
|
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".
|
|
@@ -20,8 +20,9 @@ const hostExternals = [
|
|
|
20
20
|
'dexie',
|
|
21
21
|
'zod',
|
|
22
22
|
'@construct-space/ui',
|
|
23
|
-
'@construct/sdk',
|
|
24
23
|
'@construct-space/sdk',
|
|
24
|
+
'@construct-space/graph',
|
|
25
|
+
'@construct/sdk', // legacy alias — keep during cutover
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
function makeGlobals(externals: string[]): Record<string, string> {
|