@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 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 { stat as statcb, Stats } from "fs";
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, stat } from "fs/promises";
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 != null && Number.isSafeInteger(opts.depth) ? opts.depth : defaultOptions.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, unwatchFile, watchFile } from "fs";
8772
- import { realpath as fsrealpath, lstat as lstat2, open, stat as stat2 } from "fs/promises";
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(sp.extname(filePath).slice(1).toLowerCase());
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(sp.resolve(path, evPath), KEY_LISTENERS, sp.join(path, evPath));
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 = sp.dirname(path);
9228
- const basename4 = sp.basename(path);
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 = sp.resolve(path);
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 = sp.dirname(file);
9259
- const basename4 = sp.basename(file);
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 = sp.join(directory, "");
9343
- const throttleKey = target ? `${directory}:${target}` : directory;
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 = sp.join(directory, item);
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 = sp.join(dir, sp.relative(dir, 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(sp.dirname(dir));
9399
- const tracked = parentDir.has(sp.basename(dir));
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(sp.basename(dir));
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 = sp.resolve(path);
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 = sp.dirname(wh.watchPath);
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(sp.resolve(path), targetPath);
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 = /\/\//g;
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 = sp2.relative(matcher.path, string);
9493
+ const relative4 = sysPath2.relative(matcher.path, string);
9511
9494
  if (!relative4) {
9512
9495
  return false;
9513
9496
  }
9514
- return !relative4.startsWith("..") && !sp2.isAbsolute(relative4);
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 = sp2.normalize(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
- path = path.replace(DOUBLE_SLASH_RE, "/");
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
- str = str.replace(DOUBLE_SLASH_RE, SLASH);
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(sp2.normalize(toUnix(path)));
9563
+ var normalizePathToUnix = (path) => toUnix(sysPath2.normalize(toUnix(path)));
9577
9564
  var normalizeIgnored = (cwd = "") => (path) => {
9578
9565
  if (typeof path === "string") {
9579
- return normalizePathToUnix(sp2.isAbsolute(path) ? path : sp2.join(cwd, path));
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 (sp2.isAbsolute(path)) {
9572
+ if (sysPath2.isAbsolute(path)) {
9586
9573
  return path;
9587
9574
  }
9588
- return sp2.join(cwd, path);
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(sp2.dirname(dir), sp2.basename(dir));
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 = sp2.resolve(watchPath);
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 sp2.join(this.watchPath, sp2.relative(this.watchPath, entry.fullPath));
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(sp2.dirname(item), sp2.basename(_origAdd || item));
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 (!sp2.isAbsolute(path) && !this._closers.has(path)) {
9781
+ if (!sysPath2.isAbsolute(path) && !this._closers.has(path)) {
9823
9782
  if (cwd)
9824
- path = sp2.join(cwd, path);
9825
- path = sp2.resolve(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 ? sp2.relative(this.options.cwd, dir) : dir;
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 = sp2.normalize(path);
9844
+ path = sysPath2.normalize(path);
9886
9845
  if (opts.cwd)
9887
- path = sp2.relative(opts.cwd, 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 ? sp2.join(opts.cwd, path) : path;
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 && !sp2.isAbsolute(path)) {
9995
- fullPath = sp2.join(this.options.cwd, path);
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 = sp2.resolve(directory);
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 = sp2.join(directory, item);
10064
- const fullPath = sp2.resolve(path);
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 = sp2.relative(this.options.cwd, path);
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 = sp2.dirname(path);
10098
- this._getWatchedDir(dir).remove(sp2.basename(path));
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
- throw new Error(msg);
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 result = await uploadSource(creds.portal, creds.token, creds.publisherKey, tarballPath, m, { private: wantPrivate, public: wantPublic });
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.7.7",
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` describe agent-callable inputs (type, description, required).
6
- * - `run` receives the payload and returns any JSON-serialisable value.
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
- export const actions = {
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 space_list_actions to discover available actions for this space.
16
- Use space_run_action to execute actions.
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 listing available actions to understand what you can do
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
- scope: 'org', // partitions schema per organization
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` — single shared schema (rare)
193
- - `org` — partitioned by organization (most product spaces)
194
- - `project` — partitioned by project (dev-tooling spaces)
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
- export const actions = {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@construct-space/cli",
3
- "version": "1.7.7",
3
+ "version": "1.8.0",
4
4
  "description": "Construct CLI — scaffold, build, develop, and publish spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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` describe agent-callable inputs (type, description, required).
6
- * - `run` receives the payload and returns any JSON-serialisable value.
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
- export const actions = {
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 space_list_actions to discover available actions for this space.
16
- Use space_run_action to execute actions.
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 listing available actions to understand what you can do
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
- scope: 'org', // partitions schema per organization
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` — single shared schema (rare)
193
- - `org` — partitioned by organization (most product spaces)
194
- - `project` — partitioned by project (dev-tooling spaces)
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
- export const actions = {
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> {