forge-cw 0.1.13 → 0.1.15

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
@@ -2657,6 +2657,40 @@ function splitPluginId(id) {
2657
2657
  const at = id.lastIndexOf("@");
2658
2658
  return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
2659
2659
  }
2660
+ function catalogVersion(configDir, marketplaces, marketplace, name) {
2661
+ const known = marketplaces[marketplace];
2662
+ const location = isRecord(known) && typeof known["installLocation"] === "string" ? known["installLocation"] : join8(configDir, "plugins", "marketplaces", marketplace);
2663
+ const entries = readJson(join8(location, ".claude-plugin", "marketplace.json"))["plugins"];
2664
+ if (!Array.isArray(entries))
2665
+ return void 0;
2666
+ const entry = entries.find((e) => isRecord(e) && e["name"] === name);
2667
+ if (!entry)
2668
+ return void 0;
2669
+ if (typeof entry["version"] === "string")
2670
+ return entry["version"];
2671
+ const source = entry["source"];
2672
+ if (typeof source !== "string")
2673
+ return void 0;
2674
+ const dir = resolve2(location, source);
2675
+ if (!inside(location, dir))
2676
+ return void 0;
2677
+ const version = readJson(join8(dir, ".claude-plugin", "plugin.json"))["version"];
2678
+ return typeof version === "string" ? version : void 0;
2679
+ }
2680
+ function hasUpdate({ latest, version }) {
2681
+ if (!latest || latest === version)
2682
+ return false;
2683
+ const a = numeric(latest);
2684
+ const b = numeric(version);
2685
+ if (!a || !b)
2686
+ return true;
2687
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
2688
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
2689
+ if (d !== 0)
2690
+ return d > 0;
2691
+ }
2692
+ return false;
2693
+ }
2660
2694
  function projectSkillNames(projectPath) {
2661
2695
  const dir = join8(projectPath, ".claude", "skills");
2662
2696
  if (!existsSync6(dir))
@@ -2678,16 +2712,21 @@ async function listPlugins(reader, remoteOf) {
2678
2712
  const user = entries.find((e) => isRecord(e) && e["scope"] === "user" && typeof e["installPath"] === "string");
2679
2713
  if (!user)
2680
2714
  continue;
2715
+ const { name, marketplace } = splitPluginId(id);
2716
+ const version = String(user["version"] ?? "");
2717
+ const latest = catalogVersion(configDir, marketplaces, marketplace, name);
2681
2718
  const install = {
2682
2719
  scope,
2683
2720
  scopeRef,
2684
- version: String(user["version"] ?? ""),
2721
+ version,
2685
2722
  enabled: !isRecord(enabledPlugins) || enabledPlugins[id] !== false,
2686
2723
  installPath: String(user["installPath"]),
2687
- lastUpdated: typeof user["lastUpdated"] === "string" ? user["lastUpdated"] : void 0
2724
+ lastUpdated: typeof user["lastUpdated"] === "string" ? user["lastUpdated"] : void 0,
2725
+ latest,
2726
+ update: hasUpdate({ latest, version })
2688
2727
  };
2689
2728
  const list = found.get(id) ?? [];
2690
- list.push({ install, repo: marketplaceSource(marketplaces[splitPluginId(id).marketplace]) });
2729
+ list.push({ install, repo: marketplaceSource(marketplaces[marketplace]) });
2691
2730
  found.set(id, list);
2692
2731
  }
2693
2732
  }
@@ -2757,7 +2796,7 @@ function marketplaceSourceOf(reader, marketplace) {
2757
2796
  }
2758
2797
  return void 0;
2759
2798
  }
2760
- var isRecord, inside, pluginScopes, knownMarketplaces, knowsMarketplace;
2799
+ var isRecord, inside, pluginScopes, knownMarketplaces, numeric, knowsMarketplace;
2761
2800
  var init_plugins = __esm({
2762
2801
  "../core/dist/plugins.js"() {
2763
2802
  "use strict";
@@ -2772,6 +2811,7 @@ var init_plugins = __esm({
2772
2811
  ...reader.getAccounts().map((a) => ({ scope: "account", scopeRef: a }))
2773
2812
  ];
2774
2813
  knownMarketplaces = (configDir) => readJson(join8(configDir, "plugins", "known_marketplaces.json"));
2814
+ numeric = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
2775
2815
  knowsMarketplace = (configDir, marketplace) => isRecord(knownMarketplaces(configDir)[marketplace]);
2776
2816
  }
2777
2817
  });
@@ -4896,13 +4936,13 @@ var require_parse_options = __commonJS({
4896
4936
  var require_identifiers = __commonJS({
4897
4937
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/internal/identifiers.js"(exports, module) {
4898
4938
  "use strict";
4899
- var numeric = /^[0-9]+$/;
4939
+ var numeric2 = /^[0-9]+$/;
4900
4940
  var compareIdentifiers = (a, b) => {
4901
4941
  if (typeof a === "number" && typeof b === "number") {
4902
4942
  return a === b ? 0 : a < b ? -1 : 1;
4903
4943
  }
4904
- const anum = numeric.test(a);
4905
- const bnum = numeric.test(b);
4944
+ const anum = numeric2.test(a);
4945
+ const bnum = numeric2.test(b);
4906
4946
  if (anum && bnum) {
4907
4947
  a = +a;
4908
4948
  b = +b;
@@ -7173,7 +7213,7 @@ var init_module_context = __esm({
7173
7213
 
7174
7214
  // ../core/dist/module-host.js
7175
7215
  import { existsSync as existsSync13, readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync4, writeFileSync as writeFileSync5 } from "node:fs";
7176
- import { basename as basename4, dirname as dirname7, join as join16, resolve as resolve6 } from "node:path";
7216
+ import { basename as basename4, dirname as dirname7, join as join16, resolve as resolve6, sep as sep4 } from "node:path";
7177
7217
  import { pathToFileURL } from "node:url";
7178
7218
  function disabled(id, c, reason, m) {
7179
7219
  const text = (v, fallback) => typeof v === "string" && v ? v : fallback;
@@ -7268,6 +7308,7 @@ var init_module_host = __esm({
7268
7308
  opts;
7269
7309
  statuses = [];
7270
7310
  loaded = /* @__PURE__ */ new Map();
7311
+ origins = /* @__PURE__ */ new Map();
7271
7312
  constructor(opts) {
7272
7313
  this.opts = opts;
7273
7314
  }
@@ -7277,6 +7318,16 @@ var init_module_host = __esm({
7277
7318
  get(id) {
7278
7319
  return this.loaded.get(id);
7279
7320
  }
7321
+ // where a listed module was found, loaded or not; the first one found wins
7322
+ locate(id) {
7323
+ const c = this.origins.get(id);
7324
+ return c && { dir: c.dir, removable: this.removable(c) };
7325
+ }
7326
+ // only what was installed into the modules folder; bundled, dev and Forge's own packages stay
7327
+ removable(c) {
7328
+ const { modulesDir } = this.opts;
7329
+ return c.source === "installed" && dirname7(c.dir) === modulesDir || c.dir.startsWith(join16(modulesDir, "node_modules") + sep4);
7330
+ }
7280
7331
  // scans, reloads and dev changes run one at a time: each one awaits imports midway
7281
7332
  queue = Promise.resolve();
7282
7333
  serial(fn) {
@@ -7293,12 +7344,17 @@ var init_module_host = __esm({
7293
7344
  this.unload(id);
7294
7345
  const statuses = [];
7295
7346
  const seen = /* @__PURE__ */ new Map();
7347
+ const origins = /* @__PURE__ */ new Map();
7296
7348
  for (const candidate of this.candidates()) {
7297
- const status = await this.loadCandidate(candidate, seen);
7298
- if (status)
7299
- statuses.push(status);
7349
+ const status = await this.load(candidate, seen);
7350
+ if (!status)
7351
+ continue;
7352
+ statuses.push(status);
7353
+ if (!origins.has(status.id))
7354
+ origins.set(status.id, candidate);
7300
7355
  }
7301
7356
  this.statuses = statuses;
7357
+ this.origins = origins;
7302
7358
  return statuses;
7303
7359
  }
7304
7360
  // only this module is unloaded and loaded again; the others keep running
@@ -7312,7 +7368,7 @@ var init_module_host = __esm({
7312
7368
  } else {
7313
7369
  this.unload(id);
7314
7370
  const seen = new Map([...this.loaded.values()].map((m) => [m.status.id, m.dir]));
7315
- const status = await this.loadCandidate(mod.candidate, seen);
7371
+ const status = await this.load(mod.candidate, seen);
7316
7372
  this.statuses = this.statuses.flatMap((s) => s === mod.status ? status ? [status] : [] : [s]);
7317
7373
  }
7318
7374
  }
@@ -7394,6 +7450,12 @@ var init_module_host = __esm({
7394
7450
  out.push(...packages(this.opts.forgeNodeModules, "node_modules", false));
7395
7451
  return out;
7396
7452
  }
7453
+ async load(c, seen) {
7454
+ const status = await this.loadCandidate(c, seen);
7455
+ if (status && this.removable(c))
7456
+ status.removable = true;
7457
+ return status;
7458
+ }
7397
7459
  async loadCandidate(c, seen) {
7398
7460
  const fallbackId = basename4(c.dir);
7399
7461
  if (!existsSync13(c.manifestFile)) {
@@ -7534,9 +7596,186 @@ var init_module_confirmations = __esm({
7534
7596
  }
7535
7597
  });
7536
7598
 
7599
+ // ../core/dist/module-install.js
7600
+ import { randomBytes as randomBytes2 } from "node:crypto";
7601
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3 } from "node:fs";
7602
+ import { homedir as homedir7, tmpdir } from "node:os";
7603
+ import { isAbsolute as isAbsolute2, join as join17, resolve as resolve7, sep as sep5 } from "node:path";
7604
+ async function previewManifest(spec, opts) {
7605
+ if (isPathSpec(spec, opts.cwd))
7606
+ return readFolder(expand(spec, opts.cwd));
7607
+ const tmp = mkdtempSync(join17(tmpdir(), "forge-module-pack-"));
7608
+ try {
7609
+ const packed = await opts.run("npm", ["pack", spec, "--pack-destination", tmp, "--ignore-scripts", "--json"], opts.cwd);
7610
+ if (packed.code !== 0)
7611
+ throw new Error(npmPackError(spec, packed.stderr || packed.stdout));
7612
+ const info = JSON.parse(packed.stdout)[0];
7613
+ const filename = info?.filename;
7614
+ if (!filename)
7615
+ throw new Error(`npm pack ${spec} produced no tarball`);
7616
+ const untar = await opts.run("tar", ["-xzf", join17(tmp, filename), "-C", tmp], opts.cwd);
7617
+ if (untar.code !== 0)
7618
+ throw new Error(`could not unpack ${filename}: ${untar.stderr.trim()}`);
7619
+ const preview = readFolder(join17(tmp, "package"));
7620
+ return info.name && info.version ? { ...preview, packed: { name: info.name, version: info.version } } : preview;
7621
+ } finally {
7622
+ rmSync3(tmp, { recursive: true, force: true });
7623
+ }
7624
+ }
7625
+ function npmPackError(spec, output) {
7626
+ if (/\bE404\b/.test(output))
7627
+ return `${spec} is not on npm`;
7628
+ const lines2 = output.trim().split("\n").filter((l) => !l.includes("A complete log of this run"));
7629
+ return `npm pack ${spec} failed: ${lines2.join("\n")}`;
7630
+ }
7631
+ function readFolder(dir) {
7632
+ const manifestFile = manifestFileFor(dir).file;
7633
+ if (!existsSync14(manifestFile))
7634
+ throw new Error(`${dir} has no forge-module.json`);
7635
+ const raw = JSON.parse(readFileSync12(manifestFile, "utf-8"));
7636
+ const check = checkManifest(raw, dir);
7637
+ if (check.kind === "invalid")
7638
+ throw new Error(`invalid manifest: ${check.reason}`);
7639
+ return check;
7640
+ }
7641
+ function permissionLines(manifest) {
7642
+ const p = "permissions" in manifest ? manifest.permissions ?? {} : {};
7643
+ const lines2 = [];
7644
+ for (const kind of ["network", "fs:read", "fs:write", "shell"]) {
7645
+ for (const value of p[kind] ?? [])
7646
+ lines2.push({ kind, value });
7647
+ }
7648
+ if (p.pty)
7649
+ lines2.push({ kind: "pty", value: "yes" });
7650
+ const legacy = "actions" in manifest && (manifest.actions ?? []).some((a) => "command" in a);
7651
+ if (legacy)
7652
+ lines2.push({ kind: "shell", value: "runs the shell commands in its manifest" });
7653
+ return lines2;
7654
+ }
7655
+ function describePermissions(manifest) {
7656
+ const lines2 = permissionLines(manifest).map((l) => ` ${l.kind.padEnd(9)} ${l.value}`);
7657
+ return [lines2.length ? "It asks for:" : "It asks for no permissions.", ...lines2, "", TRUST_NOTE].join("\n");
7658
+ }
7659
+ function installArgs(spec, prefix) {
7660
+ return ["install", "--ignore-scripts", "--prefix", prefix, spec];
7661
+ }
7662
+ function packageName(dir) {
7663
+ try {
7664
+ const name = JSON.parse(readFileSync12(join17(dir, "package.json"), "utf-8")).name;
7665
+ return typeof name === "string" ? name : void 0;
7666
+ } catch {
7667
+ return void 0;
7668
+ }
7669
+ }
7670
+ var TRUST_NOTE, isPathSpec, expand, InstallError, PREVIEW_TTL_MS, REGISTRY_SPEC, ModuleInstaller;
7671
+ var init_module_install = __esm({
7672
+ "../core/dist/module-install.js"() {
7673
+ "use strict";
7674
+ init_module_manifest();
7675
+ init_output();
7676
+ init_state_log();
7677
+ TRUST_NOTE = "Modules run inside Forge with your access. Forge checks every call against these permissions, but it is not a sandbox: install only modules you trust.";
7678
+ isPathSpec = (spec, cwd) => spec.startsWith(".") || spec.startsWith("~") || isAbsolute2(spec) || existsSync14(resolve7(cwd, spec)) && !spec.startsWith("@");
7679
+ expand = (spec, cwd) => resolve7(cwd, expandHome(spec));
7680
+ InstallError = class extends Error {
7681
+ code;
7682
+ constructor(code, message) {
7683
+ super(message);
7684
+ this.code = code;
7685
+ }
7686
+ };
7687
+ PREVIEW_TTL_MS = 10 * 6e4;
7688
+ REGISTRY_SPEC = /^(@[^/@\s]+\/)?[^/@\s:]+(@[^\s/:]*)?$/;
7689
+ ModuleInstaller = class {
7690
+ opts;
7691
+ previews = /* @__PURE__ */ new Map();
7692
+ busy = false;
7693
+ constructor(opts) {
7694
+ this.opts = opts;
7695
+ }
7696
+ get now() {
7697
+ return (this.opts.now ?? Date.now)();
7698
+ }
7699
+ async preview(spec) {
7700
+ const cwd = this.opts.cwd ?? homedir7();
7701
+ const trimmed = spec.trim();
7702
+ if (!trimmed)
7703
+ throw new InstallError("bad_request", "Give an npm package, a folder or a git url");
7704
+ let preview;
7705
+ try {
7706
+ preview = await previewManifest(trimmed, { cwd, run: this.opts.run });
7707
+ } catch (err) {
7708
+ throw new InstallError("bad_request", err.message);
7709
+ }
7710
+ const m = preview.manifest;
7711
+ const pinned = isPathSpec(trimmed, cwd) ? expand(trimmed, cwd) : preview.packed && REGISTRY_SPEC.test(trimmed) ? `${preview.packed.name}@${preview.packed.version}` : trimmed;
7712
+ for (const [t, p] of this.previews)
7713
+ if (p.expires < this.now)
7714
+ this.previews.delete(t);
7715
+ const token = randomBytes2(24).toString("base64url");
7716
+ this.previews.set(token, { spec: pinned, name: m.name, expires: this.now + PREVIEW_TTL_MS });
7717
+ return {
7718
+ token,
7719
+ kind: preview.kind,
7720
+ name: m.name,
7721
+ version: m.version,
7722
+ displayName: m.displayName,
7723
+ description: "description" in m && typeof m.description === "string" ? m.description : void 0,
7724
+ spec: pinned,
7725
+ permissions: permissionLines(m),
7726
+ trustNote: TRUST_NOTE
7727
+ };
7728
+ }
7729
+ async install(token) {
7730
+ const p = this.previews.get(token);
7731
+ this.previews.delete(token);
7732
+ if (!p || p.expires < this.now)
7733
+ throw new InstallError("confirmation_required", "Preview the module again before installing it");
7734
+ return this.exclusive(async () => {
7735
+ mkdirSync5(this.opts.modulesDir, { recursive: true });
7736
+ const res = await this.opts.run("npm", installArgs(p.spec, this.opts.modulesDir), this.opts.modulesDir);
7737
+ if (res.code !== 0)
7738
+ throw new InstallError("install_failed", `npm install failed: ${tailOutput(res.stderr || res.stdout, 6)}`);
7739
+ const list = await this.opts.host.scan();
7740
+ return list.find((s) => s.name === p.name);
7741
+ });
7742
+ }
7743
+ remove(id) {
7744
+ return this.exclusive(async () => {
7745
+ const found = this.opts.host.locate(id);
7746
+ if (!found)
7747
+ throw new InstallError("not_found", `No module ${id}`);
7748
+ if (!found.removable)
7749
+ throw new InstallError("bad_request", `${id} was not installed into Forge's modules folder; a dev folder unloads when forge module dev stops`);
7750
+ const packages2 = join17(this.opts.modulesDir, "node_modules") + sep5;
7751
+ if (found.dir.startsWith(packages2)) {
7752
+ const name = packageName(found.dir) ?? found.dir.slice(packages2.length);
7753
+ const res = await this.opts.run("npm", ["uninstall", "--prefix", this.opts.modulesDir, name], this.opts.modulesDir);
7754
+ if (res.code !== 0)
7755
+ throw new InstallError("install_failed", `npm uninstall failed: ${tailOutput(res.stderr || res.stdout, 6)}`);
7756
+ } else {
7757
+ rmSync3(found.dir, { recursive: true, force: true });
7758
+ }
7759
+ await this.opts.host.scan();
7760
+ });
7761
+ }
7762
+ async exclusive(fn) {
7763
+ if (this.busy)
7764
+ throw new InstallError("busy", "Another module is being installed or removed");
7765
+ this.busy = true;
7766
+ try {
7767
+ return await fn();
7768
+ } finally {
7769
+ this.busy = false;
7770
+ }
7771
+ }
7772
+ };
7773
+ }
7774
+ });
7775
+
7537
7776
  // ../core/dist/module-routes.js
7538
7777
  import { readFile as readFile2 } from "node:fs/promises";
7539
- import { resolve as resolve7 } from "node:path";
7778
+ import { resolve as resolve8 } from "node:path";
7540
7779
  import { Hono as Hono5 } from "hono";
7541
7780
  function moduleRoutes(host, opts) {
7542
7781
  const { db } = opts;
@@ -7560,6 +7799,26 @@ function moduleRoutes(host, opts) {
7560
7799
  await host.removeDev(path);
7561
7800
  return c.json(host.list());
7562
7801
  });
7802
+ const installing = async (c, fn) => {
7803
+ if (!opts.installer)
7804
+ return c.json({ code: "local_only", message: "Modules are installed only in local mode" }, 403);
7805
+ try {
7806
+ return await fn(opts.installer);
7807
+ } catch (err) {
7808
+ if (err instanceof InstallError)
7809
+ return c.json({ code: err.code, message: err.message }, STATUS_BY_CODE[err.code]);
7810
+ throw err;
7811
+ }
7812
+ };
7813
+ app.post("/install/preview", (c) => installing(c, async (installer) => {
7814
+ const { spec } = await c.req.json();
7815
+ return c.json(await installer.preview(typeof spec === "string" ? spec : ""));
7816
+ }));
7817
+ app.post("/install", (c) => installing(c, async (installer) => {
7818
+ const { token } = await c.req.json();
7819
+ const status = await installer.install(typeof token === "string" ? token : "");
7820
+ return c.json({ status, modules: host.list() });
7821
+ }));
7563
7822
  app.get("/notifications", (c) => c.json({
7564
7823
  items: db.listNotifications().map((n) => ({
7565
7824
  moduleId: n.moduleId,
@@ -7581,6 +7840,10 @@ function moduleRoutes(host, opts) {
7581
7840
  await host.reload(c.req.param("id"));
7582
7841
  return c.json(host.list().find((s) => s.id === c.req.param("id")) ?? removed(c.req.param("id")));
7583
7842
  });
7843
+ app.delete("/:id", (c) => installing(c, async (installer) => {
7844
+ await installer.remove(c.req.param("id"));
7845
+ return c.json(host.list());
7846
+ }));
7584
7847
  app.get("/:id/status", (c) => {
7585
7848
  const status = host.list().find((s) => s.id === c.req.param("id"));
7586
7849
  return status ? c.json(status) : c.json(removed(c.req.param("id")), 404);
@@ -7605,7 +7868,7 @@ function moduleRoutes(host, opts) {
7605
7868
  if (!mod?.manifest.view)
7606
7869
  return c.json(removed(c.req.param("id")), 404);
7607
7870
  try {
7608
- const code = await readFile2(resolve7(mod.dir, mod.manifest.view.entry), "utf-8");
7871
+ const code = await readFile2(resolve8(mod.dir, mod.manifest.view.entry), "utf-8");
7609
7872
  return c.body(code, 200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-cache" });
7610
7873
  } catch (err) {
7611
7874
  return c.json({ code: "not_found", message: `view bundle missing: ${err.message}` }, 404);
@@ -7806,12 +8069,16 @@ var init_module_routes = __esm({
7806
8069
  init_dist();
7807
8070
  init_module_manifest();
7808
8071
  init_module_confirmations();
8072
+ init_module_install();
7809
8073
  STATUS_BY_CODE = {
7810
8074
  bad_request: 400,
7811
8075
  unauthorized: 401,
7812
8076
  not_found: 404,
7813
8077
  unreachable: 502,
7814
- upstream: 502
8078
+ upstream: 502,
8079
+ busy: 409,
8080
+ confirmation_required: 409,
8081
+ install_failed: 502
7815
8082
  };
7816
8083
  }
7817
8084
  });
@@ -7949,12 +8216,12 @@ var init_module_events = __esm({
7949
8216
  // ../core/dist/server.js
7950
8217
  import { Hono as Hono6 } from "hono";
7951
8218
  import { cors } from "hono/cors";
7952
- import { join as join17, basename as basename5, resolve as resolve8 } from "node:path";
7953
- import { readdirSync as readdirSync8, existsSync as existsSync14 } from "node:fs";
8219
+ import { join as join18, basename as basename5, resolve as resolve9 } from "node:path";
8220
+ import { readdirSync as readdirSync8, existsSync as existsSync15 } from "node:fs";
7954
8221
  function createForgeServer(options) {
7955
8222
  const { dataDir, db: externalDb, authToken, localOnly = true } = options;
7956
- const dbPath = join17(dataDir, "forge.db");
7957
- const modulesDir = join17(dataDir, "modules");
8223
+ const dbPath = join18(dataDir, "forge.db");
8224
+ const modulesDir = join18(dataDir, "modules");
7958
8225
  const db = externalDb ?? new ForgeDB(dbPath);
7959
8226
  const loader = new ModuleLoader(modulesDir, options.bundledModulesDir);
7960
8227
  const runner = new ActionRunner();
@@ -7967,7 +8234,7 @@ function createForgeServer(options) {
7967
8234
  const cwReader = new CWReader(options.cwDir);
7968
8235
  const loginManager = new LoginManager(resolveCwBin(cwReader.cwHome));
7969
8236
  app.route("/api/cw", cwRoutes(cwReader, { loginManager, localOnly, events: { emit: (name, payload) => moduleEvents.emit(name, payload) } }));
7970
- app.route("/api/skills", skillRoutes(cwReader, { backupDir: join17(dataDir, "skill-backups") }));
8237
+ app.route("/api/skills", skillRoutes(cwReader, { backupDir: join18(dataDir, "skill-backups") }));
7971
8238
  const { remote, shadow, warning } = remoteClassifierFromEnv();
7972
8239
  if (warning)
7973
8240
  console.warn(`[state] ${warning}`);
@@ -7987,7 +8254,7 @@ function createForgeServer(options) {
7987
8254
  }
7988
8255
  });
7989
8256
  const ptyManager = new PTYManager(states);
7990
- const generalSessions = new GeneralSessions(join17(dataDir, "general-sessions.json"));
8257
+ const generalSessions = new GeneralSessions(join18(dataDir, "general-sessions.json"));
7991
8258
  const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
7992
8259
  app.route("/api/liveframe", liveframeRoutes());
7993
8260
  const kioku = new Kioku({ projects: () => cwReader.getProjects() });
@@ -8021,7 +8288,7 @@ function createForgeServer(options) {
8021
8288
  modulesDir,
8022
8289
  bundledDir: options.bundledModulesDir,
8023
8290
  forgeNodeModules: options.forgeNodeModules,
8024
- devFile: join17(dataDir, "dev-modules.json"),
8291
+ devFile: join18(dataDir, "dev-modules.json"),
8025
8292
  localOnly,
8026
8293
  deps: { db, reader: cwReader, runner },
8027
8294
  hooks: {
@@ -8052,7 +8319,8 @@ function createForgeServer(options) {
8052
8319
  });
8053
8320
  ptyManager.onLifecycle = (event, session) => moduleEvents.emit(event === "started" ? "session.started" : "session.ended", sessionEvent(session));
8054
8321
  const modulesReady = moduleHost.scan().catch((err) => console.error(`[modules] scan failed: ${err.message}`));
8055
- app.route("/api/modules", moduleRoutes(moduleHost, { db, ready: modulesReady }));
8322
+ const installer = localOnly ? new ModuleInstaller({ modulesDir, host: moduleHost, run: createRunner(void 0, 5 * 6e4) }) : void 0;
8323
+ app.route("/api/modules", moduleRoutes(moduleHost, { db, ready: modulesReady, installer }));
8056
8324
  app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
8057
8325
  app.post("/api/cw/terminal/kill", async (c) => {
8058
8326
  const { project, sessionDir } = await c.req.json();
@@ -8184,25 +8452,25 @@ data: ${JSON.stringify(data)}
8184
8452
  });
8185
8453
  app.get("/api/filesystem/browse", (c) => {
8186
8454
  const home = process.env.HOME ?? "/tmp";
8187
- const dir = resolve8(c.req.query("path") ?? process.cwd());
8455
+ const dir = resolve9(c.req.query("path") ?? process.cwd());
8188
8456
  if (!dir.startsWith(home)) {
8189
8457
  return c.json({ error: "Access denied: path must be within home directory" }, 403);
8190
8458
  }
8191
- if (!existsSync14(dir)) {
8459
+ if (!existsSync15(dir)) {
8192
8460
  return c.json({ error: "Directory not found" }, 404);
8193
8461
  }
8194
8462
  try {
8195
8463
  const entries = readdirSync8(dir, { withFileTypes: true });
8196
8464
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
8197
8465
  name: e.name,
8198
- path: join17(dir, e.name),
8199
- hasPackageJson: existsSync14(join17(dir, e.name, "package.json")),
8200
- hasGit: existsSync14(join17(dir, e.name, ".git"))
8466
+ path: join18(dir, e.name),
8467
+ hasPackageJson: existsSync15(join18(dir, e.name, "package.json")),
8468
+ hasGit: existsSync15(join18(dir, e.name, ".git"))
8201
8469
  })).sort((a, b) => a.name.localeCompare(b.name));
8202
8470
  return c.json({
8203
8471
  current: dir,
8204
8472
  name: basename5(dir),
8205
- parent: join17(dir, ".."),
8473
+ parent: join18(dir, ".."),
8206
8474
  directories: dirs
8207
8475
  });
8208
8476
  } catch {
@@ -8249,6 +8517,8 @@ var init_server = __esm({
8249
8517
  init_kioku_routes();
8250
8518
  init_module_host();
8251
8519
  init_module_routes();
8520
+ init_module_install();
8521
+ init_task_review();
8252
8522
  init_module_jobs();
8253
8523
  init_module_events();
8254
8524
  init_kioku();
@@ -8415,22 +8685,22 @@ var init_db_factory = __esm({
8415
8685
  });
8416
8686
 
8417
8687
  // ../core/dist/init.js
8418
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
8419
- import { join as join18 } from "node:path";
8420
- import { homedir as homedir7 } from "node:os";
8688
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
8689
+ import { join as join19 } from "node:path";
8690
+ import { homedir as homedir8 } from "node:os";
8421
8691
  function getForgeDir() {
8422
- return join18(homedir7(), ".forge");
8692
+ return join19(homedir8(), ".forge");
8423
8693
  }
8424
8694
  function ensureForgeDir() {
8425
8695
  const forgeDir = getForgeDir();
8426
- if (existsSync15(forgeDir)) {
8696
+ if (existsSync16(forgeDir)) {
8427
8697
  return { forgeDir, created: false };
8428
8698
  }
8429
- mkdirSync5(forgeDir, { recursive: true });
8699
+ mkdirSync6(forgeDir, { recursive: true });
8430
8700
  for (const d of FORGE_SUBDIRS) {
8431
- mkdirSync5(join18(forgeDir, d), { recursive: true });
8701
+ mkdirSync6(join19(forgeDir, d), { recursive: true });
8432
8702
  }
8433
- writeFileSync6(join18(forgeDir, "config.json"), JSON.stringify({
8703
+ writeFileSync6(join19(forgeDir, "config.json"), JSON.stringify({
8434
8704
  port: 3e3,
8435
8705
  theme: "dark",
8436
8706
  openBrowser: true,
@@ -8492,68 +8762,6 @@ var init_port_in_use = __esm({
8492
8762
  }
8493
8763
  });
8494
8764
 
8495
- // ../core/dist/module-install.js
8496
- import { existsSync as existsSync16, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3 } from "node:fs";
8497
- import { tmpdir } from "node:os";
8498
- import { isAbsolute as isAbsolute2, join as join19, resolve as resolve9 } from "node:path";
8499
- async function previewManifest(spec, opts) {
8500
- if (isPathSpec(spec, opts.cwd))
8501
- return readFolder(expand(spec, opts.cwd));
8502
- const tmp = mkdtempSync(join19(tmpdir(), "forge-module-pack-"));
8503
- try {
8504
- const packed = await opts.run("npm", ["pack", spec, "--pack-destination", tmp, "--ignore-scripts", "--json"], opts.cwd);
8505
- if (packed.code !== 0)
8506
- throw new Error(`npm pack ${spec} failed: ${(packed.stderr || packed.stdout).trim()}`);
8507
- const filename = JSON.parse(packed.stdout)[0]?.filename;
8508
- if (!filename)
8509
- throw new Error(`npm pack ${spec} produced no tarball`);
8510
- const untar = await opts.run("tar", ["-xzf", join19(tmp, filename), "-C", tmp], opts.cwd);
8511
- if (untar.code !== 0)
8512
- throw new Error(`could not unpack ${filename}: ${untar.stderr.trim()}`);
8513
- return readFolder(join19(tmp, "package"));
8514
- } finally {
8515
- rmSync3(tmp, { recursive: true, force: true });
8516
- }
8517
- }
8518
- function readFolder(dir) {
8519
- const manifestFile = manifestFileFor(dir).file;
8520
- if (!existsSync16(manifestFile))
8521
- throw new Error(`${dir} has no forge-module.json`);
8522
- const raw = JSON.parse(readFileSync12(manifestFile, "utf-8"));
8523
- const check = checkManifest(raw, dir);
8524
- if (check.kind === "invalid")
8525
- throw new Error(`invalid manifest: ${check.reason}`);
8526
- return check;
8527
- }
8528
- function describePermissions(manifest) {
8529
- const p = "permissions" in manifest ? manifest.permissions ?? {} : {};
8530
- const lines2 = [];
8531
- for (const key of ["network", "fs:read", "fs:write", "shell"]) {
8532
- for (const value of p[key] ?? [])
8533
- lines2.push(` ${key.padEnd(9)} ${value}`);
8534
- }
8535
- if (p.pty)
8536
- lines2.push(` ${"pty".padEnd(9)} yes`);
8537
- const legacy = "actions" in manifest && (manifest.actions ?? []).some((a) => "command" in a);
8538
- if (legacy)
8539
- lines2.push(" shell runs the shell commands in its manifest");
8540
- return [lines2.length ? "It asks for:" : "It asks for no permissions.", ...lines2, "", TRUST_NOTE].join("\n");
8541
- }
8542
- function installArgs(spec, prefix) {
8543
- return ["install", "--ignore-scripts", "--prefix", prefix, spec];
8544
- }
8545
- var TRUST_NOTE, isPathSpec, expand;
8546
- var init_module_install = __esm({
8547
- "../core/dist/module-install.js"() {
8548
- "use strict";
8549
- init_module_manifest();
8550
- init_state_log();
8551
- TRUST_NOTE = "Modules run inside Forge with your access. Forge checks every call against these permissions, but it is not a sandbox: install only modules you trust.";
8552
- isPathSpec = (spec, cwd) => spec.startsWith(".") || spec.startsWith("~") || isAbsolute2(spec) || existsSync16(resolve9(cwd, spec)) && !spec.startsWith("@");
8553
- expand = (spec, cwd) => resolve9(cwd, expandHome(spec));
8554
- }
8555
- });
8556
-
8557
8765
  // ../core/dist/index.js
8558
8766
  var dist_exports = {};
8559
8767
  __export(dist_exports, {
@@ -8630,7 +8838,7 @@ var init_dist2 = __esm({
8630
8838
 
8631
8839
  // src/index.ts
8632
8840
  import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
8633
- import { homedir as homedir8 } from "node:os";
8841
+ import { homedir as homedir9 } from "node:os";
8634
8842
  import { join as join20 } from "node:path";
8635
8843
  function parseArgs(argv) {
8636
8844
  const out = { open: true };
@@ -8655,7 +8863,7 @@ async function main() {
8655
8863
  if (created) {
8656
8864
  console.log("First run \u2014 initialized Forge.");
8657
8865
  }
8658
- const cwHome = join20(process.env.HOME ?? homedir8(), ".cw");
8866
+ const cwHome = join20(process.env.HOME ?? homedir9(), ".cw");
8659
8867
  const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir: join20(packageRoot, "cw") });
8660
8868
  if (cw.action === "installed") console.log(` Installed CW ${cw.version} in ${cwHome}`);
8661
8869
  else if (cw.action === "updated") console.log(` Updated ${cw.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-cw",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "A visual dashboard for your coding agents on Claude Code, Codex, Pi or OpenCode. Installs CW for you.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,8 +40,8 @@
40
40
  "@types/node": "^22.0.0",
41
41
  "esbuild": "^0.25.0",
42
42
  "typescript": "^5.8.0",
43
- "@forge-dev/core": "0.1.0",
44
- "@forge-dev/console": "0.1.0"
43
+ "@forge-dev/console": "0.1.0",
44
+ "@forge-dev/core": "0.1.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsc",