forge-cw 0.1.0 → 0.1.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/cw/cw CHANGED
@@ -155,6 +155,29 @@ _link_account_skills() {
155
155
  done
156
156
  }
157
157
 
158
+ # links global and account skills into the config dir the harness reads, since CLAUDE_CONFIG_DIR hides ~/.claude/skills
159
+ _link_shared_skills() {
160
+ local harness_dir="${1:?Usage: _link_shared_skills <harness_dir> <acct_root>}" acct_root="${2:?}"
161
+ harness_supports skills || return 0
162
+ local target_dir="$harness_dir/skills" link src name
163
+ mkdir -p "$target_dir" 2>/dev/null || return 0
164
+ # drops links whose skill was removed from its source
165
+ for link in "$target_dir"/*; do
166
+ [[ -L "$link" && ! -e "$link" ]] && rm -f "$link"
167
+ done
168
+ local sources=("$HOME/.claude/skills")
169
+ [[ "$acct_root/skills" != "$target_dir" ]] && sources=("$acct_root/skills" "${sources[@]}")
170
+ for src in "${sources[@]}"; do
171
+ [[ -d "$src" && "$src" != "$target_dir" ]] || continue
172
+ for link in "$src"/*/; do
173
+ [[ -f "$link/SKILL.md" ]] || continue
174
+ name=$(basename "$link")
175
+ case "$name" in acct--*) continue ;; esac
176
+ [[ -e "$target_dir/$name" || -L "$target_dir/$name" ]] || ln -s "${link%/}" "$target_dir/$name"
177
+ done
178
+ done
179
+ }
180
+
158
181
  _get_project() {
159
182
  CW_REG="$CW_REGISTRY" CW_NAME="$1" python3 -c "
160
183
  import json, os, sys
@@ -559,10 +582,17 @@ _harness_provider_applies() {
559
582
  return 1
560
583
  }
561
584
 
585
+ # every launch sees the global skills and its account's skills
586
+ _link_harness_skills() {
587
+ [[ -n "${CW_HARNESS_DIR:-}" && -n "${CW_ACCOUNT:-}" ]] || return 0
588
+ _link_shared_skills "$CW_HARNESS_DIR" "$(_account_root "$CW_ACCOUNT")"
589
+ }
590
+
562
591
  # a driver that refuses to build a launch sets _CW_LAUNCH_REFUSED so callers can fail
563
592
  _harness_launch() {
564
593
  HARNESS_ARGV=(); HARNESS_ENV=()
565
594
  _harness_provider_applies || { _CW_LAUNCH_REFUSED=1; return 1; }
595
+ _link_harness_skills
566
596
  harness_launch || { _CW_LAUNCH_REFUSED=1; return 1; }
567
597
  _harness_exec
568
598
  }
@@ -571,6 +601,7 @@ _harness_launch() {
571
601
  _harness_resume() {
572
602
  local attempt=1 rc=1
573
603
  _harness_provider_applies || { _CW_LAUNCH_REFUSED=1; return 1; }
604
+ _link_harness_skills
574
605
  while :; do
575
606
  HARNESS_ARGV=(); HARNESS_ENV=()
576
607
  harness_resume "$attempt" || break
@@ -965,6 +996,15 @@ _account_migrate_split() {
965
996
  _account_migrate_undo() {
966
997
  local account="$1" root="$2" dry="$3"
967
998
  [[ -d "$root/claude" ]] || { _warn "Account '$account' is already flat."; return 0; }
999
+ # launches link skills into claude/skills; the skills themselves live elsewhere
1000
+ if [[ -d "$root/claude/skills" ]]; then
1001
+ if $dry; then
1002
+ _dim " would drop the skill links in claude/skills"
1003
+ else
1004
+ find "$root/claude/skills" -maxdepth 1 -type l -delete
1005
+ rmdir "$root/claude/skills" 2>/dev/null || true
1006
+ fi
1007
+ fi
968
1008
  _account_scan_dir "$root/claude"
969
1009
  local entries=(${CW_ACCOUNT_ENTRIES[@]+"${CW_ACCOUNT_ENTRIES[@]}"})
970
1010
  local entry base blocked=0
package/dist/index.js CHANGED
@@ -260,8 +260,17 @@ var init_runner = __esm({
260
260
  const proc = spawn("sh", ["-c", command], {
261
261
  cwd: options.cwd,
262
262
  env: { ...process.env, ...options.env },
263
- stdio: ["ignore", "pipe", "pipe"]
263
+ stdio: ["ignore", "pipe", "pipe"],
264
+ detached: true
264
265
  });
266
+ const stop = () => {
267
+ try {
268
+ if (proc.pid)
269
+ process.kill(-proc.pid, "SIGTERM");
270
+ } catch {
271
+ proc.kill("SIGTERM");
272
+ }
273
+ };
265
274
  const handleData = (data) => {
266
275
  const str2 = data.toString();
267
276
  output += str2;
@@ -270,14 +279,12 @@ var init_runner = __esm({
270
279
  proc.stdout.on("data", handleData);
271
280
  proc.stderr.on("data", handleData);
272
281
  if (options.signal) {
273
- options.signal.addEventListener("abort", () => {
274
- proc.kill("SIGTERM");
275
- }, { once: true });
282
+ options.signal.addEventListener("abort", stop, { once: true });
276
283
  }
277
284
  if (options.timeout) {
278
285
  timeoutId = setTimeout(() => {
279
286
  timedOut = true;
280
- proc.kill("SIGTERM");
287
+ stop();
281
288
  }, options.timeout);
282
289
  }
283
290
  proc.on("close", (code) => {
@@ -630,45 +637,46 @@ var init_cw_reader = __esm({
630
637
  getSkillDir(scope, scopeRef, name) {
631
638
  return join2(this.getSkillConfigDir(scope, scopeRef), "skills", name);
632
639
  }
633
- getSkills(account, project) {
640
+ // global, every account and every registered project
641
+ getSkills() {
642
+ return [
643
+ ...this.scanSkills(join2(this.getSkillConfigDir("global", "global"), "skills"), "global", "global"),
644
+ ...this.getAccounts().flatMap((a) => this.scanSkills(join2(this.getSkillConfigDir("account", a), "skills"), "account", a)),
645
+ ...Object.entries(this.getProjects()).flatMap(([name, p]) => this.scanSkills(join2(p.path, ".claude", "skills"), "project", name))
646
+ ];
647
+ }
648
+ scanSkills(dir, scope, scopeRef) {
634
649
  const results = [];
635
- const scanDir = (dir, scope, scopeRef) => {
636
- if (!existsSync2(dir))
637
- return;
638
- const entries = readdirSync2(dir, { withFileTypes: true });
639
- for (const entry of entries) {
640
- if (!entry.isDirectory())
641
- continue;
642
- if (entry.name.startsWith("acct--"))
643
- continue;
644
- const fullPath = join2(dir, entry.name);
645
- const skillMd = join2(fullPath, "SKILL.md");
646
- if (!existsSync2(skillMd))
647
- continue;
648
- try {
649
- const content = readFileSync2(skillMd, "utf-8");
650
- const { frontmatter } = this.parseFrontmatter(content);
651
- const refsDir = join2(fullPath, "references");
652
- const hasReferences = existsSync2(refsDir) && readdirSync2(refsDir).some((f) => f.endsWith(".md"));
653
- results.push({
654
- name: String(frontmatter["name"] ?? entry.name),
655
- dirName: entry.name,
656
- scope,
657
- scopeRef,
658
- description: String(frontmatter["description"] ?? ""),
659
- domain: frontmatter["domain"] ? String(frontmatter["domain"]) : void 0,
660
- triggers: frontmatter["triggers"] ? String(frontmatter["triggers"]) : void 0,
661
- hasReferences
662
- });
663
- } catch {
664
- }
650
+ if (!existsSync2(dir))
651
+ return results;
652
+ const entries = readdirSync2(dir, { withFileTypes: true });
653
+ for (const entry of entries) {
654
+ if (!entry.isDirectory())
655
+ continue;
656
+ if (entry.name.startsWith("acct--"))
657
+ continue;
658
+ const fullPath = join2(dir, entry.name);
659
+ const skillMd = join2(fullPath, "SKILL.md");
660
+ if (!existsSync2(skillMd))
661
+ continue;
662
+ try {
663
+ const content = readFileSync2(skillMd, "utf-8");
664
+ const { frontmatter } = this.parseFrontmatter(content);
665
+ const refsDir = join2(fullPath, "references");
666
+ const hasReferences = existsSync2(refsDir) && readdirSync2(refsDir).some((f) => f.endsWith(".md"));
667
+ results.push({
668
+ name: String(frontmatter["name"] ?? entry.name),
669
+ dirName: entry.name,
670
+ scope,
671
+ scopeRef,
672
+ description: String(frontmatter["description"] ?? ""),
673
+ domain: frontmatter["domain"] ? String(frontmatter["domain"]) : void 0,
674
+ triggers: frontmatter["triggers"] ? String(frontmatter["triggers"]) : void 0,
675
+ hasReferences
676
+ });
677
+ } catch {
665
678
  }
666
- };
667
- scanDir(join2(this.getSkillConfigDir("global", "global"), "skills"), "global", "global");
668
- if (account)
669
- scanDir(join2(this.getSkillConfigDir("account", account), "skills"), "account", account);
670
- if (project)
671
- scanDir(join2(this.getSkillConfigDir("project", project), "skills"), "project", project);
679
+ }
672
680
  return results;
673
681
  }
674
682
  getSkill(scope, scopeRef, name) {
@@ -2528,7 +2536,7 @@ var init_plugins = __esm({
2528
2536
 
2529
2537
  // ../core/dist/skill-routes.js
2530
2538
  import { Hono as Hono2 } from "hono";
2531
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, rmSync as rmSync2, existsSync as existsSync7, unlinkSync } from "node:fs";
2539
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, rmSync as rmSync2, existsSync as existsSync7, unlinkSync, cpSync } from "node:fs";
2532
2540
  import { dirname as dirname2, join as join9 } from "node:path";
2533
2541
  function installFailure(result2) {
2534
2542
  try {
@@ -2552,10 +2560,32 @@ function skillRoutes(reader, options = {}) {
2552
2560
  const remoteOf = options.remoteOf ?? createRemoteLookup(runCommand);
2553
2561
  const app = new Hono2();
2554
2562
  const updating = /* @__PURE__ */ new Set();
2555
- app.get("/", (c) => {
2556
- const account = c.req.query("account");
2557
- const project = c.req.query("project");
2558
- return c.json(reader.getSkills(account, project));
2563
+ const refOf = (scope, scopeRef) => scope === "global" ? "global" : scopeRef ?? "";
2564
+ const knownScope = (scope, ref) => scope === "global" || (scope === "account" ? reader.getAccounts().includes(ref) : scope === "project" && ref in reader.getProjects());
2565
+ app.get("/", (c) => c.json(reader.getSkills()));
2566
+ app.post("/copy", async (c) => {
2567
+ const { from, to } = await c.req.json();
2568
+ const fromRef = refOf(from.scope, from.scopeRef);
2569
+ const toRef = refOf(to.scope, to.scopeRef);
2570
+ if (!SKILL_NAME_RE.test(from.name))
2571
+ return c.json({ error: "Invalid skill name" }, 400);
2572
+ if (!knownScope(from.scope, fromRef))
2573
+ return c.json({ error: `Unknown ${from.scope}: ${fromRef}` }, 404);
2574
+ if (!knownScope(to.scope, toRef))
2575
+ return c.json({ error: `Unknown ${to.scope}: ${toRef}` }, 404);
2576
+ const source = reader.getSkillDir(from.scope, fromRef, from.name);
2577
+ const target = reader.getSkillDir(to.scope, toRef, from.name);
2578
+ if (!existsSync7(join9(source, "SKILL.md")))
2579
+ return c.json({ error: "Skill not found" }, 404);
2580
+ if (existsSync7(target))
2581
+ return c.json({ error: `${from.name} already exists in ${toRef}` }, 409);
2582
+ try {
2583
+ cpSync(source, target, { recursive: true });
2584
+ return c.json({ ok: true });
2585
+ } catch (err) {
2586
+ const message = err instanceof Error ? err.message : "Unknown error";
2587
+ return c.json({ error: `Failed to copy skill: ${message}` }, 500);
2588
+ }
2559
2589
  });
2560
2590
  app.get("/global/:name", (c) => {
2561
2591
  const name = c.req.param("name");
@@ -2701,9 +2731,8 @@ function skillRoutes(reader, options = {}) {
2701
2731
  if (!repo || !skill) {
2702
2732
  return c.json({ error: "repo and skill are required" }, 400);
2703
2733
  }
2704
- const ref = scope === "global" ? "global" : scopeRef ?? "";
2705
- const known = scope === "global" || (scope === "account" ? reader.getAccounts().includes(ref) : ref in reader.getProjects());
2706
- if (!known) {
2734
+ const ref = refOf(scope, scopeRef);
2735
+ if (!knownScope(scope, ref)) {
2707
2736
  return c.json({ error: `Unknown ${scope}: ${ref}` }, 404);
2708
2737
  }
2709
2738
  const configDir = reader.getSkillConfigDir(scope, ref);
@@ -2754,7 +2783,7 @@ function skillRoutes(reader, options = {}) {
2754
2783
  });
2755
2784
  return app;
2756
2785
  }
2757
- var INSTALL_TIMEOUT_MS;
2786
+ var INSTALL_TIMEOUT_MS, SKILL_NAME_RE;
2758
2787
  var init_skill_routes = __esm({
2759
2788
  "../core/dist/skill-routes.js"() {
2760
2789
  "use strict";
@@ -2762,6 +2791,7 @@ var init_skill_routes = __esm({
2762
2791
  init_task_review();
2763
2792
  init_plugins();
2764
2793
  INSTALL_TIMEOUT_MS = 12e4;
2794
+ SKILL_NAME_RE = /^\w[\w.-]*$/;
2765
2795
  }
2766
2796
  });
2767
2797
 
@@ -3143,9 +3173,8 @@ var init_terminal_screen = __esm({
3143
3173
  write(data) {
3144
3174
  this.term.write(data);
3145
3175
  }
3146
- // a resize applies at once while writes wait to be parsed, so it waits its turn behind them
3147
3176
  resize(cols, rows) {
3148
- this.term.write("", () => this.term.resize(cols, rows));
3177
+ this.term.resize(cols, rows);
3149
3178
  }
3150
3179
  // rows oldest first, scrollback included, without the blank rows under the last one written
3151
3180
  read() {
@@ -3401,17 +3430,22 @@ var init_state_log = __esm({
3401
3430
 
3402
3431
  // ../core/dist/pty-routes.js
3403
3432
  import { WebSocketServer, WebSocket } from "ws";
3404
- function takeSession(reader, project, sessionDir) {
3433
+ function takeSession(reader, project, sessionDir, general) {
3405
3434
  const sessionId = `${project}::${sessionDir}`;
3406
3435
  const pending = pendingSessions.get(sessionId);
3407
3436
  if (pending) {
3408
3437
  pendingSessions.delete(sessionId);
3438
+ if (pending.type === "general")
3439
+ general?.remember(sessionId, pending);
3409
3440
  return { session: pending, isNew: true };
3410
3441
  }
3411
3442
  const session = reader.getSession(project, sessionDir);
3412
- return session ? { session, isNew: false } : null;
3443
+ if (session)
3444
+ return { session, isNew: false };
3445
+ const known = general?.get(sessionId);
3446
+ return known ? { session: known, isNew: true } : null;
3413
3447
  }
3414
- function createTerminalWss(manager, reader, { localOnly = true } = {}) {
3448
+ function createTerminalWss(manager, reader, { localOnly = true, general } = {}) {
3415
3449
  const wss = new WebSocketServer({ noServer: true });
3416
3450
  function wireWs(ws, sessionId, project, sessionDir) {
3417
3451
  const client = {
@@ -3448,18 +3482,18 @@ function createTerminalWss(manager, reader, { localOnly = true } = {}) {
3448
3482
  wireWs(ws, sessionId, project, sessionDir);
3449
3483
  return;
3450
3484
  }
3451
- const taken = takeSession(reader, project, sessionDir);
3485
+ const taken = takeSession(reader, project, sessionDir, general);
3452
3486
  if (!taken) {
3453
3487
  console.log(`[pty-ws] Session not found: ${project}/${sessionDir}`);
3454
3488
  ws.send(JSON.stringify({ type: "error", message: `Session not found: ${project}/${sessionDir}` }));
3455
- ws.close();
3489
+ ws.close(REFUSED_CLOSE_CODE);
3456
3490
  return;
3457
3491
  }
3458
3492
  const ptySession = manager.getOrCreate(project, sessionDir, taken.session, { isNew: taken.isNew });
3459
3493
  if (!ptySession) {
3460
3494
  console.error(`[pty-ws] Failed to spawn terminal for: ${project}/${sessionDir}`);
3461
3495
  ws.send(JSON.stringify({ type: "error", message: `Failed to start terminal for ${project}/${sessionDir}` }));
3462
- ws.close();
3496
+ ws.close(REFUSED_CLOSE_CODE);
3463
3497
  return;
3464
3498
  }
3465
3499
  wireWs(ws, sessionId, project, sessionDir);
@@ -3495,16 +3529,67 @@ function parseTerminalUpgradeUrl(pathname) {
3495
3529
  return null;
3496
3530
  }
3497
3531
  }
3532
+ var REFUSED_CLOSE_CODE;
3498
3533
  var init_pty_routes = __esm({
3499
3534
  "../core/dist/pty-routes.js"() {
3500
3535
  "use strict";
3501
3536
  init_cw_routes();
3502
3537
  init_origin_guard();
3538
+ REFUSED_CLOSE_CODE = 4404;
3539
+ }
3540
+ });
3541
+
3542
+ // ../core/dist/general-sessions.js
3543
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
3544
+ import { dirname as dirname5 } from "node:path";
3545
+ var MAX_AGE_MS, GeneralSessions;
3546
+ var init_general_sessions = __esm({
3547
+ "../core/dist/general-sessions.js"() {
3548
+ "use strict";
3549
+ MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3550
+ GeneralSessions = class {
3551
+ file;
3552
+ sessions = /* @__PURE__ */ new Map();
3553
+ constructor(file = null, now = Date.now()) {
3554
+ this.file = file;
3555
+ if (!file || !existsSync9(file))
3556
+ return;
3557
+ try {
3558
+ const saved = JSON.parse(readFileSync7(file, "utf-8"));
3559
+ for (const [key, session] of Object.entries(saved)) {
3560
+ if (now - Date.parse(session.created) < MAX_AGE_MS)
3561
+ this.sessions.set(key, session);
3562
+ }
3563
+ } catch {
3564
+ }
3565
+ }
3566
+ get(key) {
3567
+ return this.sessions.get(key);
3568
+ }
3569
+ remember(key, session) {
3570
+ this.sessions.set(key, session);
3571
+ this.save();
3572
+ }
3573
+ forget(key) {
3574
+ if (this.sessions.delete(key))
3575
+ this.save();
3576
+ }
3577
+ save() {
3578
+ if (!this.file)
3579
+ return;
3580
+ try {
3581
+ mkdirSync4(dirname5(this.file), { recursive: true });
3582
+ writeFileSync3(this.file, JSON.stringify(Object.fromEntries(this.sessions), null, 2));
3583
+ } catch (err) {
3584
+ console.warn(`[pty] could not save general sessions: ${err.message}`);
3585
+ }
3586
+ }
3587
+ };
3503
3588
  }
3504
3589
  });
3505
3590
 
3506
3591
  // ../core/dist/liveframe.js
3507
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
3592
+ import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "node:fs";
3508
3593
  import { mkdir, rm } from "node:fs/promises";
3509
3594
  import { homedir as homedir4 } from "node:os";
3510
3595
  import { join as join11 } from "node:path";
@@ -3523,7 +3608,7 @@ function parseFrameUrl(stdout) {
3523
3608
  }
3524
3609
  function readJson2(path) {
3525
3610
  try {
3526
- return JSON.parse(readFileSync7(path, "utf8"));
3611
+ return JSON.parse(readFileSync8(path, "utf8"));
3527
3612
  } catch {
3528
3613
  return null;
3529
3614
  }
@@ -3596,7 +3681,7 @@ var init_liveframe = __esm({
3596
3681
  return this.readFrame(project, frame, this.config().api);
3597
3682
  }
3598
3683
  listFrames() {
3599
- if (!existsSync9(this.root))
3684
+ if (!existsSync10(this.root))
3600
3685
  return [];
3601
3686
  const { api } = this.config();
3602
3687
  const frames = [];
@@ -3636,7 +3721,7 @@ var init_liveframe = __esm({
3636
3721
  return this.exclusive(project, frame, async () => {
3637
3722
  const projectDir = join11(this.root, project);
3638
3723
  const dir = this.frameDir(project, frame);
3639
- if (existsSync9(dir))
3724
+ if (existsSync10(dir))
3640
3725
  throw new LiveframeError(`${project}/${frame} already exists in ${this.root}`, 409);
3641
3726
  await mkdir(projectDir, { recursive: true });
3642
3727
  try {
@@ -3715,7 +3800,7 @@ var init_liveframe_routes = __esm({
3715
3800
  import { Hono as Hono4 } from "hono";
3716
3801
  import { cors } from "hono/cors";
3717
3802
  import { join as join12, basename as basename3, resolve as resolve4 } from "node:path";
3718
- import { readdirSync as readdirSync6, existsSync as existsSync10 } from "node:fs";
3803
+ import { readdirSync as readdirSync6, existsSync as existsSync11 } from "node:fs";
3719
3804
  function createForgeServer(options) {
3720
3805
  const { dataDir, db: externalDb, authToken, localOnly = true } = options;
3721
3806
  const dbPath = join12(dataDir, "forge.db");
@@ -3752,13 +3837,15 @@ function createForgeServer(options) {
3752
3837
  }
3753
3838
  });
3754
3839
  const ptyManager = new PTYManager(states);
3755
- const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly });
3840
+ const generalSessions = new GeneralSessions(join12(dataDir, "general-sessions.json"));
3841
+ const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
3756
3842
  app.route("/api/liveframe", liveframeRoutes());
3757
3843
  app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
3758
3844
  app.post("/api/cw/terminal/kill", async (c) => {
3759
3845
  const { project, sessionDir } = await c.req.json();
3760
3846
  const sessionId = `${project}::${sessionDir}`;
3761
3847
  ptyManager.kill(sessionId);
3848
+ generalSessions.forget(sessionId);
3762
3849
  return c.json({ ok: true });
3763
3850
  });
3764
3851
  async function resolveAction(c) {
@@ -3895,7 +3982,7 @@ data: ${JSON.stringify(data)}
3895
3982
  if (!dir.startsWith(home)) {
3896
3983
  return c.json({ error: "Access denied: path must be within home directory" }, 403);
3897
3984
  }
3898
- if (!existsSync10(dir)) {
3985
+ if (!existsSync11(dir)) {
3899
3986
  return c.json({ error: "Directory not found" }, 404);
3900
3987
  }
3901
3988
  try {
@@ -3903,8 +3990,8 @@ data: ${JSON.stringify(data)}
3903
3990
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
3904
3991
  name: e.name,
3905
3992
  path: join12(dir, e.name),
3906
- hasPackageJson: existsSync10(join12(dir, e.name, "package.json")),
3907
- hasGit: existsSync10(join12(dir, e.name, ".git"))
3993
+ hasPackageJson: existsSync11(join12(dir, e.name, "package.json")),
3994
+ hasGit: existsSync11(join12(dir, e.name, ".git"))
3908
3995
  })).sort((a, b) => a.name.localeCompare(b.name));
3909
3996
  return c.json({
3910
3997
  current: dir,
@@ -3947,6 +4034,7 @@ var init_server = __esm({
3947
4034
  init_state_log();
3948
4035
  init_pty_routes();
3949
4036
  init_login_manager();
4037
+ init_general_sessions();
3950
4038
  init_liveframe_routes();
3951
4039
  }
3952
4040
  });
@@ -4081,7 +4169,7 @@ var init_db_factory = __esm({
4081
4169
  });
4082
4170
 
4083
4171
  // ../core/dist/init.js
4084
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
4172
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
4085
4173
  import { join as join13 } from "node:path";
4086
4174
  import { homedir as homedir5 } from "node:os";
4087
4175
  function getForgeDir() {
@@ -4089,14 +4177,14 @@ function getForgeDir() {
4089
4177
  }
4090
4178
  function ensureForgeDir() {
4091
4179
  const forgeDir = getForgeDir();
4092
- if (existsSync11(forgeDir)) {
4180
+ if (existsSync12(forgeDir)) {
4093
4181
  return { forgeDir, created: false };
4094
4182
  }
4095
- mkdirSync4(forgeDir, { recursive: true });
4183
+ mkdirSync5(forgeDir, { recursive: true });
4096
4184
  for (const d of FORGE_SUBDIRS) {
4097
- mkdirSync4(join13(forgeDir, d), { recursive: true });
4185
+ mkdirSync5(join13(forgeDir, d), { recursive: true });
4098
4186
  }
4099
- writeFileSync3(join13(forgeDir, "config.json"), JSON.stringify({
4187
+ writeFileSync4(join13(forgeDir, "config.json"), JSON.stringify({
4100
4188
  port: 3e3,
4101
4189
  theme: "dark",
4102
4190
  openBrowser: true,
@@ -4115,11 +4203,11 @@ var init_init = __esm({
4115
4203
  // ../core/dist/cw-install.js
4116
4204
  import { execFile as execFile5 } from "node:child_process";
4117
4205
  import { createHash as createHash2 } from "node:crypto";
4118
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
4206
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
4119
4207
  import { delimiter as delimiter2, join as join14 } from "node:path";
4120
4208
  function readCwVersion(file) {
4121
4209
  try {
4122
- const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync8(file, "utf-8"));
4210
+ const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync9(file, "utf-8"));
4123
4211
  return match ? match[1] : null;
4124
4212
  } catch {
4125
4213
  return null;
@@ -4137,9 +4225,9 @@ function compareVersions(a, b) {
4137
4225
  }
4138
4226
  function readCwBundle(dir) {
4139
4227
  try {
4140
- const { ref } = JSON.parse(readFileSync8(join14(dir, ".bundle.json"), "utf-8"));
4228
+ const { ref } = JSON.parse(readFileSync9(join14(dir, ".bundle.json"), "utf-8"));
4141
4229
  const version = readCwVersion(join14(dir, "cw"));
4142
- if (!ref || !version || !existsSync12(join14(dir, "install.sh")))
4230
+ if (!ref || !version || !existsSync13(join14(dir, "install.sh")))
4143
4231
  return null;
4144
4232
  return { dir, version, ref };
4145
4233
  } catch {
@@ -4187,7 +4275,7 @@ function pathWithCw(cwHome, pathVar) {
4187
4275
  }
4188
4276
  function readMarker(cwHome) {
4189
4277
  try {
4190
- return JSON.parse(readFileSync8(join14(cwHome, MARKER_FILE), "utf-8"));
4278
+ return JSON.parse(readFileSync9(join14(cwHome, MARKER_FILE), "utf-8"));
4191
4279
  } catch {
4192
4280
  return null;
4193
4281
  }
@@ -4203,7 +4291,7 @@ async function ensureCw(options) {
4203
4291
  const run = options.run ?? runInstall;
4204
4292
  const bin = join14(cwHome, "bin", "cw");
4205
4293
  const bundle = readCwBundle(bundleDir);
4206
- const installed = existsSync12(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
4294
+ const installed = existsSync13(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
4207
4295
  const decision = decideCwInstall({
4208
4296
  bundle,
4209
4297
  installed,
@@ -4214,10 +4302,10 @@ async function ensureCw(options) {
4214
4302
  if (decision.action === "skip" || !bundle)
4215
4303
  return { action: "skipped", message: decision.reason };
4216
4304
  const { code, output } = await run(join14(bundle.dir, "install.sh"), ["--no-shell"], { ...env, CW_HOME: cwHome });
4217
- if (code !== 0 || !existsSync12(bin))
4305
+ if (code !== 0 || !existsSync13(bin))
4218
4306
  return { action: "failed", message: failureMessage2(output) };
4219
4307
  const marker = { version: bundle.version, ref: bundle.ref, sha256: sha256Of(bin) };
4220
- writeFileSync4(join14(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
4308
+ writeFileSync5(join14(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
4221
4309
  return {
4222
4310
  action: decision.action === "install" ? "installed" : "updated",
4223
4311
  version: bundle.version,
@@ -4231,7 +4319,7 @@ var init_cw_install = __esm({
4231
4319
  MARKER_FILE = ".forge-cw.json";
4232
4320
  INSTALL_TIMEOUT_MS2 = 12e4;
4233
4321
  ANSI_RE3 = /\u001b\[[0-9;?]*[A-Za-z]/g;
4234
- sha256Of = (file) => createHash2("sha256").update(readFileSync8(file)).digest("hex");
4322
+ sha256Of = (file) => createHash2("sha256").update(readFileSync9(file)).digest("hex");
4235
4323
  runInstall = (script, args, env) => new Promise((resolve5) => {
4236
4324
  execFile5("bash", [script, ...args], { env, timeout: INSTALL_TIMEOUT_MS2, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
4237
4325
  const output = `${String(stdout)}${String(stderr)}`;
@@ -4299,7 +4387,7 @@ var init_dist = __esm({
4299
4387
  });
4300
4388
 
4301
4389
  // src/index.ts
4302
- import { existsSync as existsSync13 } from "node:fs";
4390
+ import { existsSync as existsSync14 } from "node:fs";
4303
4391
  import { homedir as homedir6 } from "node:os";
4304
4392
  import { join as join15 } from "node:path";
4305
4393
  function parseArgs(argv) {
@@ -4313,7 +4401,7 @@ function parseArgs(argv) {
4313
4401
  return out;
4314
4402
  }
4315
4403
  var packageRoot = join15(import.meta.dirname, "..");
4316
- var isCheckout = existsSync13(join15(packageRoot, "src"));
4404
+ var isCheckout = existsSync14(join15(packageRoot, "src"));
4317
4405
  var consoleDist = isCheckout ? join15(packageRoot, "../console/dist") : join15(packageRoot, "console");
4318
4406
  async function main() {
4319
4407
  const { ensureForgeDir: ensureForgeDir2, createForgeServer: createForgeServer2, createDatabase: createDatabase2, resolveListenOptions: resolveListenOptions2, ensureCw: ensureCw2, pathWithCw: pathWithCw2, quietSqliteWarning: quietSqliteWarning2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
@@ -4347,10 +4435,10 @@ async function main() {
4347
4435
  localOnly
4348
4436
  });
4349
4437
  const { serveStatic } = await import("@hono/node-server/serve-static");
4350
- const { readFileSync: readFileSync9 } = await import("node:fs");
4351
- if (existsSync13(join15(consoleDist, "index.html"))) {
4438
+ const { readFileSync: readFileSync10 } = await import("node:fs");
4439
+ if (existsSync14(join15(consoleDist, "index.html"))) {
4352
4440
  server.app.use("/*", serveStatic({ root: consoleDist }));
4353
- const indexHtml = readFileSync9(join15(consoleDist, "index.html"), "utf-8");
4441
+ const indexHtml = readFileSync10(join15(consoleDist, "index.html"), "utf-8");
4354
4442
  server.app.get("*", (c) => c.html(indexHtml));
4355
4443
  }
4356
4444
  const { serve } = await import("@hono/node-server");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-cw",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -39,8 +39,8 @@
39
39
  "@types/node": "^22.0.0",
40
40
  "esbuild": "^0.25.0",
41
41
  "typescript": "^5.8.0",
42
- "@forge-dev/core": "0.1.0",
43
- "@forge-dev/console": "0.1.0"
42
+ "@forge-dev/console": "0.1.0",
43
+ "@forge-dev/core": "0.1.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsc",