@construct-space/cli 1.7.7 → 1.8.1
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 +213 -129
- 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
|
@@ -2876,26 +2876,92 @@ function listDesktopProfiles() {
|
|
|
2876
2876
|
}
|
|
2877
2877
|
return results;
|
|
2878
2878
|
}
|
|
2879
|
-
function
|
|
2880
|
-
return join12(dataDir(),
|
|
2879
|
+
function legacyCredentialsPath() {
|
|
2880
|
+
return join12(dataDir(), LEGACY_CREDENTIALS_FILE);
|
|
2881
2881
|
}
|
|
2882
|
-
function
|
|
2883
|
-
|
|
2882
|
+
function registryPath() {
|
|
2883
|
+
return join12(dataDir(), PROFILES_REGISTRY);
|
|
2884
|
+
}
|
|
2885
|
+
function readRegistry() {
|
|
2886
|
+
const path = registryPath();
|
|
2887
|
+
if (!existsSync10(path))
|
|
2888
|
+
return {};
|
|
2889
|
+
try {
|
|
2890
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
2891
|
+
} catch {
|
|
2892
|
+
return {};
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
function writeRegistry(reg) {
|
|
2896
|
+
const path = registryPath();
|
|
2884
2897
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
2885
|
-
writeFileSync5(path, JSON.stringify(
|
|
2886
|
-
`, { mode: 384 });
|
|
2898
|
+
writeFileSync5(path, JSON.stringify({ version: 1, ...reg }, null, 2));
|
|
2887
2899
|
}
|
|
2888
|
-
function
|
|
2889
|
-
const
|
|
2890
|
-
if (
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2900
|
+
function store(creds) {
|
|
2901
|
+
const profileId = creds.profileId || creds.user?.id;
|
|
2902
|
+
if (!profileId) {
|
|
2903
|
+
throw new Error("cannot store credentials without a profile id (user.id or profileId)");
|
|
2904
|
+
}
|
|
2905
|
+
const profilePath = join12(profilesDir(), profileId, "auth.json");
|
|
2906
|
+
mkdirSync4(dirname4(profilePath), { recursive: true });
|
|
2907
|
+
let existing = {};
|
|
2908
|
+
if (existsSync10(profilePath)) {
|
|
2909
|
+
try {
|
|
2910
|
+
existing = JSON.parse(readFileSync7(profilePath, "utf-8"));
|
|
2911
|
+
} catch {}
|
|
2912
|
+
}
|
|
2913
|
+
existing.token = creds.token;
|
|
2914
|
+
existing.authenticated = true;
|
|
2915
|
+
existing.updated_at = new Date().toISOString();
|
|
2916
|
+
if (creds.user) {
|
|
2917
|
+
existing.user = { ...existing.user, ...creds.user };
|
|
2898
2918
|
}
|
|
2919
|
+
if (creds.publisherKey) {
|
|
2920
|
+
existing.publisher = {
|
|
2921
|
+
...existing.publisher,
|
|
2922
|
+
name: creds.publisherName || existing.publisher?.name || "",
|
|
2923
|
+
kind: creds.publisherKind || existing.publisher?.kind || "user",
|
|
2924
|
+
api_key: creds.publisherKey
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
writeFileSync5(profilePath, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
2928
|
+
const reg = readRegistry();
|
|
2929
|
+
reg.active_profile = profileId;
|
|
2930
|
+
reg.profiles = reg.profiles || [];
|
|
2931
|
+
const existingEntry = reg.profiles.find((p) => p.id === profileId);
|
|
2932
|
+
if (existingEntry) {
|
|
2933
|
+
if (creds.user?.name)
|
|
2934
|
+
existingEntry.name = creds.user.name;
|
|
2935
|
+
if (creds.user?.email)
|
|
2936
|
+
existingEntry.email = creds.user.email;
|
|
2937
|
+
} else if (creds.user) {
|
|
2938
|
+
reg.profiles.push({
|
|
2939
|
+
id: profileId,
|
|
2940
|
+
name: creds.user.name,
|
|
2941
|
+
email: creds.user.email
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
writeRegistry(reg);
|
|
2945
|
+
}
|
|
2946
|
+
function migrateLegacyCredentials() {
|
|
2947
|
+
const legacy = legacyCredentialsPath();
|
|
2948
|
+
if (!existsSync10(legacy))
|
|
2949
|
+
return;
|
|
2950
|
+
try {
|
|
2951
|
+
const data = JSON.parse(readFileSync7(legacy, "utf-8"));
|
|
2952
|
+
if (!data.token)
|
|
2953
|
+
return;
|
|
2954
|
+
const pid = data.profileId || data.user?.id;
|
|
2955
|
+
if (!pid)
|
|
2956
|
+
return;
|
|
2957
|
+
const profilePath = join12(profilesDir(), pid, "auth.json");
|
|
2958
|
+
if (existsSync10(profilePath))
|
|
2959
|
+
return;
|
|
2960
|
+
store({ ...data, profileId: pid });
|
|
2961
|
+
} catch {}
|
|
2962
|
+
}
|
|
2963
|
+
function load2() {
|
|
2964
|
+
migrateLegacyCredentials();
|
|
2899
2965
|
const fromProfile = loadFromActiveProfile();
|
|
2900
2966
|
if (fromProfile)
|
|
2901
2967
|
return fromProfile;
|
|
@@ -2945,11 +3011,24 @@ function isAuthenticated() {
|
|
|
2945
3011
|
}
|
|
2946
3012
|
}
|
|
2947
3013
|
function clear() {
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
|
|
3014
|
+
const reg = readRegistry();
|
|
3015
|
+
const activeId = reg.active_profile;
|
|
3016
|
+
if (activeId) {
|
|
3017
|
+
const profilePath = join12(profilesDir(), activeId, "auth.json");
|
|
3018
|
+
if (existsSync10(profilePath)) {
|
|
3019
|
+
try {
|
|
3020
|
+
const data = JSON.parse(readFileSync7(profilePath, "utf-8"));
|
|
3021
|
+
data.authenticated = false;
|
|
3022
|
+
data.updated_at = new Date().toISOString();
|
|
3023
|
+
writeFileSync5(profilePath, JSON.stringify(data, null, 2), { mode: 384 });
|
|
3024
|
+
} catch {}
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
const legacy = legacyCredentialsPath();
|
|
3028
|
+
if (existsSync10(legacy))
|
|
3029
|
+
unlinkSync(legacy);
|
|
2951
3030
|
}
|
|
2952
|
-
var
|
|
3031
|
+
var LEGACY_CREDENTIALS_FILE = "credentials.json", PROFILES_REGISTRY = "profiles.json", DEFAULT_PORTAL = "https://my.construct.space/api/developer";
|
|
2953
3032
|
var init_auth = __esm(() => {
|
|
2954
3033
|
init_appdir();
|
|
2955
3034
|
});
|
|
@@ -8527,16 +8606,16 @@ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
|
8527
8606
|
import { join as join9 } from "path";
|
|
8528
8607
|
import { createHash as createHash2 } from "crypto";
|
|
8529
8608
|
|
|
8530
|
-
// node_modules/chokidar/index.js
|
|
8609
|
+
// node_modules/chokidar/esm/index.js
|
|
8610
|
+
import { stat as statcb } from "fs";
|
|
8611
|
+
import { stat as stat3, readdir as readdir2 } from "fs/promises";
|
|
8531
8612
|
import { EventEmitter } from "events";
|
|
8532
|
-
import
|
|
8533
|
-
import { readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
8534
|
-
import * as sp2 from "path";
|
|
8613
|
+
import * as sysPath2 from "path";
|
|
8535
8614
|
|
|
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";
|
|
8615
|
+
// node_modules/readdirp/esm/index.js
|
|
8616
|
+
import { stat, lstat, readdir, realpath } from "fs/promises";
|
|
8539
8617
|
import { Readable } from "stream";
|
|
8618
|
+
import { resolve as presolve, relative as prelative, join as pjoin, sep as psep } from "path";
|
|
8540
8619
|
var EntryTypes = {
|
|
8541
8620
|
FILE_TYPE: "files",
|
|
8542
8621
|
DIR_TYPE: "directories",
|
|
@@ -8592,20 +8671,6 @@ var normalizeFilter = (filter) => {
|
|
|
8592
8671
|
};
|
|
8593
8672
|
|
|
8594
8673
|
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
8674
|
constructor(options = {}) {
|
|
8610
8675
|
super({
|
|
8611
8676
|
objectMode: true,
|
|
@@ -8622,7 +8687,7 @@ class ReaddirpStream extends Readable {
|
|
|
8622
8687
|
} else {
|
|
8623
8688
|
this._stat = statMethod;
|
|
8624
8689
|
}
|
|
8625
|
-
this._maxDepth = opts.depth
|
|
8690
|
+
this._maxDepth = opts.depth ?? defaultOptions.depth;
|
|
8626
8691
|
this._wantsDir = type ? DIR_TYPES.has(type) : false;
|
|
8627
8692
|
this._wantsFile = type ? FILE_TYPES.has(type) : false;
|
|
8628
8693
|
this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE;
|
|
@@ -8767,11 +8832,11 @@ function readdirp(root, options = {}) {
|
|
|
8767
8832
|
return new ReaddirpStream(options);
|
|
8768
8833
|
}
|
|
8769
8834
|
|
|
8770
|
-
// node_modules/chokidar/handler.js
|
|
8771
|
-
import { watch as fs_watch
|
|
8772
|
-
import {
|
|
8835
|
+
// node_modules/chokidar/esm/handler.js
|
|
8836
|
+
import { watchFile, unwatchFile, watch as fs_watch } from "fs";
|
|
8837
|
+
import { open, stat as stat2, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
|
|
8838
|
+
import * as sysPath from "path";
|
|
8773
8839
|
import { type as osType } from "os";
|
|
8774
|
-
import * as sp from "path";
|
|
8775
8840
|
var STR_DATA = "data";
|
|
8776
8841
|
var STR_END = "end";
|
|
8777
8842
|
var STR_CLOSE = "close";
|
|
@@ -9063,7 +9128,7 @@ var binaryExtensions = new Set([
|
|
|
9063
9128
|
"zip",
|
|
9064
9129
|
"zipx"
|
|
9065
9130
|
]);
|
|
9066
|
-
var isBinaryPath = (filePath) => binaryExtensions.has(
|
|
9131
|
+
var isBinaryPath = (filePath) => binaryExtensions.has(sysPath.extname(filePath).slice(1).toLowerCase());
|
|
9067
9132
|
var foreach = (val, fn) => {
|
|
9068
9133
|
if (val instanceof Set) {
|
|
9069
9134
|
val.forEach(fn);
|
|
@@ -9101,7 +9166,7 @@ function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
|
|
|
9101
9166
|
listener(path);
|
|
9102
9167
|
emitRaw(rawEvent, evPath, { watchedPath: path });
|
|
9103
9168
|
if (evPath && path !== evPath) {
|
|
9104
|
-
fsWatchBroadcast(
|
|
9169
|
+
fsWatchBroadcast(sysPath.resolve(path, evPath), KEY_LISTENERS, sysPath.join(path, evPath));
|
|
9105
9170
|
}
|
|
9106
9171
|
};
|
|
9107
9172
|
try {
|
|
@@ -9216,19 +9281,17 @@ var setFsWatchFileListener = (path, fullPath, options, handlers) => {
|
|
|
9216
9281
|
};
|
|
9217
9282
|
|
|
9218
9283
|
class NodeFsHandler {
|
|
9219
|
-
fsw;
|
|
9220
|
-
_boundHandleError;
|
|
9221
9284
|
constructor(fsW) {
|
|
9222
9285
|
this.fsw = fsW;
|
|
9223
9286
|
this._boundHandleError = (error2) => fsW._handleError(error2);
|
|
9224
9287
|
}
|
|
9225
9288
|
_watchWithNodeFs(path, listener) {
|
|
9226
9289
|
const opts = this.fsw.options;
|
|
9227
|
-
const directory =
|
|
9228
|
-
const basename4 =
|
|
9290
|
+
const directory = sysPath.dirname(path);
|
|
9291
|
+
const basename4 = sysPath.basename(path);
|
|
9229
9292
|
const parent = this.fsw._getWatchedDir(directory);
|
|
9230
9293
|
parent.add(basename4);
|
|
9231
|
-
const absolutePath =
|
|
9294
|
+
const absolutePath = sysPath.resolve(path);
|
|
9232
9295
|
const options = {
|
|
9233
9296
|
persistent: opts.persistent
|
|
9234
9297
|
};
|
|
@@ -9255,8 +9318,8 @@ class NodeFsHandler {
|
|
|
9255
9318
|
if (this.fsw.closed) {
|
|
9256
9319
|
return;
|
|
9257
9320
|
}
|
|
9258
|
-
const dirname3 =
|
|
9259
|
-
const basename4 =
|
|
9321
|
+
const dirname3 = sysPath.dirname(file);
|
|
9322
|
+
const basename4 = sysPath.basename(file);
|
|
9260
9323
|
const parent = this.fsw._getWatchedDir(dirname3);
|
|
9261
9324
|
let prevStats = stats;
|
|
9262
9325
|
if (parent.has(basename4))
|
|
@@ -9339,9 +9402,8 @@ class NodeFsHandler {
|
|
|
9339
9402
|
this.fsw._symlinkPaths.set(full, true);
|
|
9340
9403
|
}
|
|
9341
9404
|
_handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
|
|
9342
|
-
directory =
|
|
9343
|
-
|
|
9344
|
-
throttler = this.fsw._throttle("readdir", throttleKey, 1000);
|
|
9405
|
+
directory = sysPath.join(directory, "");
|
|
9406
|
+
throttler = this.fsw._throttle("readdir", directory, 1000);
|
|
9345
9407
|
if (!throttler)
|
|
9346
9408
|
return;
|
|
9347
9409
|
const previous = this.fsw._getWatchedDir(wh.path);
|
|
@@ -9358,7 +9420,7 @@ class NodeFsHandler {
|
|
|
9358
9420
|
return;
|
|
9359
9421
|
}
|
|
9360
9422
|
const item = entry.path;
|
|
9361
|
-
let path =
|
|
9423
|
+
let path = sysPath.join(directory, item);
|
|
9362
9424
|
current.add(item);
|
|
9363
9425
|
if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path, item)) {
|
|
9364
9426
|
return;
|
|
@@ -9369,7 +9431,7 @@ class NodeFsHandler {
|
|
|
9369
9431
|
}
|
|
9370
9432
|
if (item === target || !target && !previous.has(item)) {
|
|
9371
9433
|
this.fsw._incrReadyCount();
|
|
9372
|
-
path =
|
|
9434
|
+
path = sysPath.join(dir, sysPath.relative(dir, path));
|
|
9373
9435
|
this._addToNodeFs(path, initialAdd, wh, depth + 1);
|
|
9374
9436
|
}
|
|
9375
9437
|
}).on(EV.ERROR, this._boundHandleError);
|
|
@@ -9395,12 +9457,12 @@ class NodeFsHandler {
|
|
|
9395
9457
|
});
|
|
9396
9458
|
}
|
|
9397
9459
|
async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath2) {
|
|
9398
|
-
const parentDir = this.fsw._getWatchedDir(
|
|
9399
|
-
const tracked = parentDir.has(
|
|
9460
|
+
const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir));
|
|
9461
|
+
const tracked = parentDir.has(sysPath.basename(dir));
|
|
9400
9462
|
if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
|
|
9401
9463
|
this.fsw._emit(EV.ADD_DIR, dir, stats);
|
|
9402
9464
|
}
|
|
9403
|
-
parentDir.add(
|
|
9465
|
+
parentDir.add(sysPath.basename(dir));
|
|
9404
9466
|
this.fsw._getWatchedDir(dir);
|
|
9405
9467
|
let throttler;
|
|
9406
9468
|
let closer;
|
|
@@ -9441,7 +9503,7 @@ class NodeFsHandler {
|
|
|
9441
9503
|
const follow = this.fsw.options.followSymlinks;
|
|
9442
9504
|
let closer;
|
|
9443
9505
|
if (stats.isDirectory()) {
|
|
9444
|
-
const absPath =
|
|
9506
|
+
const absPath = sysPath.resolve(path);
|
|
9445
9507
|
const targetPath = follow ? await fsrealpath(path) : path;
|
|
9446
9508
|
if (this.fsw.closed)
|
|
9447
9509
|
return;
|
|
@@ -9455,14 +9517,14 @@ class NodeFsHandler {
|
|
|
9455
9517
|
const targetPath = follow ? await fsrealpath(path) : path;
|
|
9456
9518
|
if (this.fsw.closed)
|
|
9457
9519
|
return;
|
|
9458
|
-
const parent =
|
|
9520
|
+
const parent = sysPath.dirname(wh.watchPath);
|
|
9459
9521
|
this.fsw._getWatchedDir(parent).add(wh.watchPath);
|
|
9460
9522
|
this.fsw._emit(EV.ADD, wh.watchPath, stats);
|
|
9461
9523
|
closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath);
|
|
9462
9524
|
if (this.fsw.closed)
|
|
9463
9525
|
return;
|
|
9464
9526
|
if (targetPath !== undefined) {
|
|
9465
|
-
this.fsw._symlinkPaths.set(
|
|
9527
|
+
this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath);
|
|
9466
9528
|
}
|
|
9467
9529
|
} else {
|
|
9468
9530
|
closer = this._handleFile(wh.watchPath, stats, initialAdd);
|
|
@@ -9480,7 +9542,7 @@ class NodeFsHandler {
|
|
|
9480
9542
|
}
|
|
9481
9543
|
}
|
|
9482
9544
|
|
|
9483
|
-
// node_modules/chokidar/index.js
|
|
9545
|
+
// node_modules/chokidar/esm/index.js
|
|
9484
9546
|
/*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
|
|
9485
9547
|
var SLASH = "/";
|
|
9486
9548
|
var SLASH_SLASH = "//";
|
|
@@ -9488,7 +9550,7 @@ var ONE_DOT = ".";
|
|
|
9488
9550
|
var TWO_DOTS = "..";
|
|
9489
9551
|
var STRING_TYPE = "string";
|
|
9490
9552
|
var BACK_SLASH_RE = /\\/g;
|
|
9491
|
-
var DOUBLE_SLASH_RE =
|
|
9553
|
+
var DOUBLE_SLASH_RE = /\/\//;
|
|
9492
9554
|
var DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
|
|
9493
9555
|
var REPLACER_RE = /^\.[/\\]/;
|
|
9494
9556
|
function arrify(item) {
|
|
@@ -9507,11 +9569,11 @@ function createPattern(matcher) {
|
|
|
9507
9569
|
if (matcher.path === string)
|
|
9508
9570
|
return true;
|
|
9509
9571
|
if (matcher.recursive) {
|
|
9510
|
-
const relative4 =
|
|
9572
|
+
const relative4 = sysPath2.relative(matcher.path, string);
|
|
9511
9573
|
if (!relative4) {
|
|
9512
9574
|
return false;
|
|
9513
9575
|
}
|
|
9514
|
-
return !relative4.startsWith("..") && !
|
|
9576
|
+
return !relative4.startsWith("..") && !sysPath2.isAbsolute(relative4);
|
|
9515
9577
|
}
|
|
9516
9578
|
return false;
|
|
9517
9579
|
};
|
|
@@ -9521,12 +9583,14 @@ function createPattern(matcher) {
|
|
|
9521
9583
|
function normalizePath(path) {
|
|
9522
9584
|
if (typeof path !== "string")
|
|
9523
9585
|
throw new Error("string expected");
|
|
9524
|
-
path =
|
|
9586
|
+
path = sysPath2.normalize(path);
|
|
9525
9587
|
path = path.replace(/\\/g, "/");
|
|
9526
9588
|
let prepend = false;
|
|
9527
9589
|
if (path.startsWith("//"))
|
|
9528
9590
|
prepend = true;
|
|
9529
|
-
|
|
9591
|
+
const DOUBLE_SLASH_RE2 = /\/\//;
|
|
9592
|
+
while (path.match(DOUBLE_SLASH_RE2))
|
|
9593
|
+
path = path.replace(DOUBLE_SLASH_RE2, "/");
|
|
9530
9594
|
if (prepend)
|
|
9531
9595
|
path = "/" + path;
|
|
9532
9596
|
return path;
|
|
@@ -9567,32 +9631,31 @@ var toUnix = (string) => {
|
|
|
9567
9631
|
if (str.startsWith(SLASH_SLASH)) {
|
|
9568
9632
|
prepend = true;
|
|
9569
9633
|
}
|
|
9570
|
-
|
|
9634
|
+
while (str.match(DOUBLE_SLASH_RE)) {
|
|
9635
|
+
str = str.replace(DOUBLE_SLASH_RE, SLASH);
|
|
9636
|
+
}
|
|
9571
9637
|
if (prepend) {
|
|
9572
9638
|
str = SLASH + str;
|
|
9573
9639
|
}
|
|
9574
9640
|
return str;
|
|
9575
9641
|
};
|
|
9576
|
-
var normalizePathToUnix = (path) => toUnix(
|
|
9642
|
+
var normalizePathToUnix = (path) => toUnix(sysPath2.normalize(toUnix(path)));
|
|
9577
9643
|
var normalizeIgnored = (cwd = "") => (path) => {
|
|
9578
9644
|
if (typeof path === "string") {
|
|
9579
|
-
return normalizePathToUnix(
|
|
9645
|
+
return normalizePathToUnix(sysPath2.isAbsolute(path) ? path : sysPath2.join(cwd, path));
|
|
9580
9646
|
} else {
|
|
9581
9647
|
return path;
|
|
9582
9648
|
}
|
|
9583
9649
|
};
|
|
9584
9650
|
var getAbsolutePath = (path, cwd) => {
|
|
9585
|
-
if (
|
|
9651
|
+
if (sysPath2.isAbsolute(path)) {
|
|
9586
9652
|
return path;
|
|
9587
9653
|
}
|
|
9588
|
-
return
|
|
9654
|
+
return sysPath2.join(cwd, path);
|
|
9589
9655
|
};
|
|
9590
9656
|
var EMPTY_SET = Object.freeze(new Set);
|
|
9591
9657
|
|
|
9592
9658
|
class DirEntry {
|
|
9593
|
-
path;
|
|
9594
|
-
_removeWatcher;
|
|
9595
|
-
items;
|
|
9596
9659
|
constructor(dir, removeWatcher) {
|
|
9597
9660
|
this.path = dir;
|
|
9598
9661
|
this._removeWatcher = removeWatcher;
|
|
@@ -9617,7 +9680,7 @@ class DirEntry {
|
|
|
9617
9680
|
await readdir2(dir);
|
|
9618
9681
|
} catch (err) {
|
|
9619
9682
|
if (this._removeWatcher) {
|
|
9620
|
-
this._removeWatcher(
|
|
9683
|
+
this._removeWatcher(sysPath2.dirname(dir), sysPath2.basename(dir));
|
|
9621
9684
|
}
|
|
9622
9685
|
}
|
|
9623
9686
|
}
|
|
@@ -9645,19 +9708,12 @@ var STAT_METHOD_F = "stat";
|
|
|
9645
9708
|
var STAT_METHOD_L = "lstat";
|
|
9646
9709
|
|
|
9647
9710
|
class WatchHelper {
|
|
9648
|
-
fsw;
|
|
9649
|
-
path;
|
|
9650
|
-
watchPath;
|
|
9651
|
-
fullWatchPath;
|
|
9652
|
-
dirParts;
|
|
9653
|
-
followSymlinks;
|
|
9654
|
-
statMethod;
|
|
9655
9711
|
constructor(path, follow, fsw) {
|
|
9656
9712
|
this.fsw = fsw;
|
|
9657
9713
|
const watchPath = path;
|
|
9658
9714
|
this.path = path = path.replace(REPLACER_RE, "");
|
|
9659
9715
|
this.watchPath = watchPath;
|
|
9660
|
-
this.fullWatchPath =
|
|
9716
|
+
this.fullWatchPath = sysPath2.resolve(watchPath);
|
|
9661
9717
|
this.dirParts = [];
|
|
9662
9718
|
this.dirParts.forEach((parts) => {
|
|
9663
9719
|
if (parts.length > 1)
|
|
@@ -9667,7 +9723,7 @@ class WatchHelper {
|
|
|
9667
9723
|
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
|
|
9668
9724
|
}
|
|
9669
9725
|
entryPath(entry) {
|
|
9670
|
-
return
|
|
9726
|
+
return sysPath2.join(this.watchPath, sysPath2.relative(this.watchPath, entry.fullPath));
|
|
9671
9727
|
}
|
|
9672
9728
|
filterPath(entry) {
|
|
9673
9729
|
const { stats } = entry;
|
|
@@ -9682,24 +9738,6 @@ class WatchHelper {
|
|
|
9682
9738
|
}
|
|
9683
9739
|
|
|
9684
9740
|
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
9741
|
constructor(_opts = {}) {
|
|
9704
9742
|
super();
|
|
9705
9743
|
this.closed = false;
|
|
@@ -9808,7 +9846,7 @@ class FSWatcher extends EventEmitter {
|
|
|
9808
9846
|
return;
|
|
9809
9847
|
results.forEach((item) => {
|
|
9810
9848
|
if (item)
|
|
9811
|
-
this.add(
|
|
9849
|
+
this.add(sysPath2.dirname(item), sysPath2.basename(_origAdd || item));
|
|
9812
9850
|
});
|
|
9813
9851
|
});
|
|
9814
9852
|
return this;
|
|
@@ -9819,10 +9857,10 @@ class FSWatcher extends EventEmitter {
|
|
|
9819
9857
|
const paths = unifyPaths(paths_);
|
|
9820
9858
|
const { cwd } = this.options;
|
|
9821
9859
|
paths.forEach((path) => {
|
|
9822
|
-
if (!
|
|
9860
|
+
if (!sysPath2.isAbsolute(path) && !this._closers.has(path)) {
|
|
9823
9861
|
if (cwd)
|
|
9824
|
-
path =
|
|
9825
|
-
path =
|
|
9862
|
+
path = sysPath2.join(cwd, path);
|
|
9863
|
+
path = sysPath2.resolve(path);
|
|
9826
9864
|
}
|
|
9827
9865
|
this._closePath(path);
|
|
9828
9866
|
this._addIgnoredPath(path);
|
|
@@ -9866,7 +9904,7 @@ class FSWatcher extends EventEmitter {
|
|
|
9866
9904
|
getWatched() {
|
|
9867
9905
|
const watchList = {};
|
|
9868
9906
|
this._watched.forEach((entry, dir) => {
|
|
9869
|
-
const key = this.options.cwd ?
|
|
9907
|
+
const key = this.options.cwd ? sysPath2.relative(this.options.cwd, dir) : dir;
|
|
9870
9908
|
const index = key || ONE_DOT;
|
|
9871
9909
|
watchList[index] = entry.getChildren().sort();
|
|
9872
9910
|
});
|
|
@@ -9882,9 +9920,9 @@ class FSWatcher extends EventEmitter {
|
|
|
9882
9920
|
return;
|
|
9883
9921
|
const opts = this.options;
|
|
9884
9922
|
if (isWindows)
|
|
9885
|
-
path =
|
|
9923
|
+
path = sysPath2.normalize(path);
|
|
9886
9924
|
if (opts.cwd)
|
|
9887
|
-
path =
|
|
9925
|
+
path = sysPath2.relative(opts.cwd, path);
|
|
9888
9926
|
const args = [path];
|
|
9889
9927
|
if (stats != null)
|
|
9890
9928
|
args.push(stats);
|
|
@@ -9935,7 +9973,7 @@ class FSWatcher extends EventEmitter {
|
|
|
9935
9973
|
return this;
|
|
9936
9974
|
}
|
|
9937
9975
|
if (opts.alwaysStat && stats === undefined && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
|
|
9938
|
-
const fullPath = opts.cwd ?
|
|
9976
|
+
const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path) : path;
|
|
9939
9977
|
let stats2;
|
|
9940
9978
|
try {
|
|
9941
9979
|
stats2 = await stat3(fullPath);
|
|
@@ -9991,8 +10029,8 @@ class FSWatcher extends EventEmitter {
|
|
|
9991
10029
|
const pollInterval = awf.pollInterval;
|
|
9992
10030
|
let timeoutHandler;
|
|
9993
10031
|
let fullPath = path;
|
|
9994
|
-
if (this.options.cwd && !
|
|
9995
|
-
fullPath =
|
|
10032
|
+
if (this.options.cwd && !sysPath2.isAbsolute(path)) {
|
|
10033
|
+
fullPath = sysPath2.join(this.options.cwd, path);
|
|
9996
10034
|
}
|
|
9997
10035
|
const now = new Date;
|
|
9998
10036
|
const writes = this._pendingWrites;
|
|
@@ -10049,7 +10087,7 @@ class FSWatcher extends EventEmitter {
|
|
|
10049
10087
|
return new WatchHelper(path, this.options.followSymlinks, this);
|
|
10050
10088
|
}
|
|
10051
10089
|
_getWatchedDir(directory) {
|
|
10052
|
-
const dir =
|
|
10090
|
+
const dir = sysPath2.resolve(directory);
|
|
10053
10091
|
if (!this._watched.has(dir))
|
|
10054
10092
|
this._watched.set(dir, new DirEntry(dir, this._boundRemove));
|
|
10055
10093
|
return this._watched.get(dir);
|
|
@@ -10060,8 +10098,8 @@ class FSWatcher extends EventEmitter {
|
|
|
10060
10098
|
return Boolean(Number(stats.mode) & 256);
|
|
10061
10099
|
}
|
|
10062
10100
|
_remove(directory, item, isDirectory) {
|
|
10063
|
-
const path =
|
|
10064
|
-
const fullPath =
|
|
10101
|
+
const path = sysPath2.join(directory, item);
|
|
10102
|
+
const fullPath = sysPath2.resolve(path);
|
|
10065
10103
|
isDirectory = isDirectory != null ? isDirectory : this._watched.has(path) || this._watched.has(fullPath);
|
|
10066
10104
|
if (!this._throttle("remove", path, 100))
|
|
10067
10105
|
return;
|
|
@@ -10079,7 +10117,7 @@ class FSWatcher extends EventEmitter {
|
|
|
10079
10117
|
}
|
|
10080
10118
|
let relPath = path;
|
|
10081
10119
|
if (this.options.cwd)
|
|
10082
|
-
relPath =
|
|
10120
|
+
relPath = sysPath2.relative(this.options.cwd, path);
|
|
10083
10121
|
if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
|
|
10084
10122
|
const event = this._pendingWrites.get(relPath).cancelWait();
|
|
10085
10123
|
if (event === EVENTS.ADD)
|
|
@@ -10094,8 +10132,8 @@ class FSWatcher extends EventEmitter {
|
|
|
10094
10132
|
}
|
|
10095
10133
|
_closePath(path) {
|
|
10096
10134
|
this._closeFile(path);
|
|
10097
|
-
const dir =
|
|
10098
|
-
this._getWatchedDir(dir).remove(
|
|
10135
|
+
const dir = sysPath2.dirname(path);
|
|
10136
|
+
this._getWatchedDir(dir).remove(sysPath2.basename(path));
|
|
10099
10137
|
}
|
|
10100
10138
|
_closeFile(path) {
|
|
10101
10139
|
const closers = this._closers.get(path);
|
|
@@ -10389,7 +10427,10 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10389
10427
|
}
|
|
10390
10428
|
msg += `
|
|
10391
10429
|
Fork to a new space_id to publish your own version.`;
|
|
10392
|
-
|
|
10430
|
+
const err = new Error(msg);
|
|
10431
|
+
err.ownerKind = result.owner_kind;
|
|
10432
|
+
err.ownerOrgId = result.owner_org_id;
|
|
10433
|
+
throw err;
|
|
10393
10434
|
}
|
|
10394
10435
|
if (resp.status >= 400) {
|
|
10395
10436
|
const msg = result.error || result.errors?.join("; ") || `server returned ${resp.status}`;
|
|
@@ -10397,6 +10438,20 @@ async function uploadSource(portalURL, identityToken, publisherKey, tarballPath,
|
|
|
10397
10438
|
}
|
|
10398
10439
|
return result;
|
|
10399
10440
|
}
|
|
10441
|
+
async function fetchOrgPublisherKey(portalURL, identityToken) {
|
|
10442
|
+
const root = portalURL.replace(/\/api\/developer\/?$/, "").replace(/\/+$/, "");
|
|
10443
|
+
try {
|
|
10444
|
+
const resp = await fetch(`${root}/api/publisher/org`, {
|
|
10445
|
+
headers: { Authorization: `Bearer ${identityToken}` }
|
|
10446
|
+
});
|
|
10447
|
+
if (!resp.ok)
|
|
10448
|
+
return null;
|
|
10449
|
+
const body = await resp.json();
|
|
10450
|
+
return body.publisher?.api_key || null;
|
|
10451
|
+
} catch {
|
|
10452
|
+
return null;
|
|
10453
|
+
}
|
|
10454
|
+
}
|
|
10400
10455
|
function setVersionInFiles(root, oldVer, newVer) {
|
|
10401
10456
|
const oldStr = `"version": "${oldVer}"`;
|
|
10402
10457
|
const newStr = `"version": "${newVer}"`;
|
|
@@ -10553,7 +10608,23 @@ async function publish(options) {
|
|
|
10553
10608
|
}
|
|
10554
10609
|
const uploadSpinner = ora("Uploading & building...").start();
|
|
10555
10610
|
try {
|
|
10556
|
-
const
|
|
10611
|
+
const explicitKey = options?.apiKey || process.env.CONSTRUCT_PUBLISHER_KEY;
|
|
10612
|
+
const initialKey = explicitKey || creds.publisherKey;
|
|
10613
|
+
let result;
|
|
10614
|
+
try {
|
|
10615
|
+
result = await uploadSource(creds.portal, creds.token, initialKey, tarballPath, m, { private: wantPrivate, public: wantPublic });
|
|
10616
|
+
} catch (e) {
|
|
10617
|
+
if (e?.ownerKind === "org" && !creds.publisherKey) {
|
|
10618
|
+
uploadSpinner.text = "Fetching org publisher key\u2026";
|
|
10619
|
+
const orgKey = await fetchOrgPublisherKey(creds.portal, creds.token);
|
|
10620
|
+
if (!orgKey)
|
|
10621
|
+
throw e;
|
|
10622
|
+
uploadSpinner.text = "Uploading & building (as org)\u2026";
|
|
10623
|
+
result = await uploadSource(creds.portal, creds.token, orgKey, tarballPath, m, { private: wantPrivate, public: wantPublic });
|
|
10624
|
+
} else {
|
|
10625
|
+
throw e;
|
|
10626
|
+
}
|
|
10627
|
+
}
|
|
10557
10628
|
unlinkSync2(tarballPath);
|
|
10558
10629
|
gitSafe(root, "tag", `v${m.version}`);
|
|
10559
10630
|
gitSafe(root, "push", "origin", `v${m.version}`);
|
|
@@ -10794,6 +10865,19 @@ async function loginFromProfile(profiles) {
|
|
|
10794
10865
|
console.log(source_default.dim(` profile: ${picked.id}`));
|
|
10795
10866
|
}
|
|
10796
10867
|
async function loginWithToken(token) {
|
|
10868
|
+
if (token.startsWith("cst_live_")) {
|
|
10869
|
+
console.error(source_default.red("cst_live_* tokens are being retired."));
|
|
10870
|
+
console.error(source_default.dim(" Sign in via the Construct desktop app, or get a new"));
|
|
10871
|
+
console.error(source_default.dim(` identity token at ${source_default.cyan("https://my.construct.space/settings/tokens")}.`));
|
|
10872
|
+
process.exit(1);
|
|
10873
|
+
}
|
|
10874
|
+
if (token.startsWith("csk_live_")) {
|
|
10875
|
+
console.error(source_default.red("That's a publisher key, not an identity token."));
|
|
10876
|
+
console.error(source_default.dim(" Publisher keys (csk_live_*) authorize publish-as-publisher"));
|
|
10877
|
+
console.error(source_default.dim(" on top of identity. Pass an identity token (cat_*) here;"));
|
|
10878
|
+
console.error(source_default.dim(" the CLI loads your publisher key automatically once logged in."));
|
|
10879
|
+
process.exit(1);
|
|
10880
|
+
}
|
|
10797
10881
|
let resp;
|
|
10798
10882
|
try {
|
|
10799
10883
|
resp = await fetch(ACCOUNTS_SCOPE_URL, {
|
|
@@ -11509,7 +11593,7 @@ function graphFork(newSpaceID) {
|
|
|
11509
11593
|
// package.json
|
|
11510
11594
|
var package_default = {
|
|
11511
11595
|
name: "@construct-space/cli",
|
|
11512
|
-
version: "1.
|
|
11596
|
+
version: "1.8.1",
|
|
11513
11597
|
description: "Construct CLI \u2014 scaffold, build, develop, and publish spaces",
|
|
11514
11598
|
type: "module",
|
|
11515
11599
|
bin: {
|
|
@@ -11561,7 +11645,7 @@ program2.command("scaffold [name]").alias("new").alias("create").description("Cr
|
|
|
11561
11645
|
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
11646
|
program2.command("dev").description("Start dev mode with file watching and live reload").action(async () => dev());
|
|
11563
11647
|
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));
|
|
11648
|
+
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
11649
|
program2.command("validate").description("Validate space.manifest.json").action(() => validate3());
|
|
11566
11650
|
program2.command("check").description("Run type-check (vue-tsc) and linter (eslint)").action(() => check());
|
|
11567
11651
|
program2.command("clean").description("Remove build artifacts").option("--all", "Also remove node_modules and lockfiles").action((opts) => clean(opts));
|
|
@@ -11592,7 +11676,7 @@ space.command("scaffold [name]").alias("new").alias("create").option("--with-tes
|
|
|
11592
11676
|
space.command("build").option("--entry-only").action(async (opts) => build(opts));
|
|
11593
11677
|
space.command("dev").action(async () => dev());
|
|
11594
11678
|
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));
|
|
11679
|
+
space.command("publish").option("-y, --yes").option("--bump <type>").option("--private").option("--public").option("--api-key <key>").action(async (opts) => publish(opts));
|
|
11596
11680
|
space.command("validate").action(() => validate3());
|
|
11597
11681
|
space.command("check").action(() => check());
|
|
11598
11682
|
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> {
|