frizz 0.13.10 → 0.13.12

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.
Files changed (3) hide show
  1. package/README.md +9 -10
  2. package/dist/frizz.js +39 -45
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -27,22 +27,20 @@ Frizz is for you if you have any of these opinions:
27
27
 
28
28
  **Requirements.** Node 22.13+, and a [Claude Code](https://claude.com/claude-code) or [Codex](https://developers.openai.com/codex) subscription you are signed in to — Frizz drives the subscription you already pay for. Frizz brings its own pinned copy of each CLI (downloaded once, on first start), so the version on your PATH is yours to manage and never changes what a thread runs.
29
29
 
30
- Then run it in any directory — a repo, a jj checkout, or a folder of scripts. Frizz has no opinion about version control and does not require Git.
30
+ Then run it, from anywhere.
31
31
 
32
32
  ```sh
33
- $ cd path/to/acme
34
33
  $ npx frizz
35
34
 
36
35
  FRIZZ v0.4.0 ready in 4.0s
37
36
 
38
- ➜ Local: http://127.0.0.1:9393/project/acme/
39
- ➜ Project: acme — path/to/acme
37
+ ➜ Local: http://127.0.0.1:9393/
40
38
  ➜ Logs: ~/Library/Application Support/Frizz/projects/979dae3c-fe15-4038-817e-11d0e7491959/logs/frizz-2026-08-01T13-44-43-16931.log
41
39
 
42
40
  press ctrl-c to stop · run with --debug for the full event feed
43
41
  ```
44
42
 
45
- A browser tab opens at `http://127.0.0.1:9393/project/acme/`. Frizz always listens on port 9393 (19393 if something else holds it), and one server serves every project on the machine. Each directory you run it in becomes a **project** with its own board at `/project/<name>`, so running `npx frizz` in a second repo registers that project and opens its board in the server already running rather than starting another. Runs on macOS, Linux, and Windows.
43
+ A browser tab opens on the dashboard at `http://127.0.0.1:9393/`. Add a **project** there — any folder: a repo, a jj checkout, or a folder of scripts. Frizz has no opinion about version control and does not require Git. Each project gets its own board at `/project/<name>`, and from then on running `npx frizz` inside that folder opens its board. Frizz always listens on port 9393 (19393 if something else holds it), and one server serves every project on the machine, so a second run joins the server already running rather than starting another. Runs on macOS, Linux, and Windows.
46
44
 
47
45
  <p align="center">
48
46
  <img src="assets/board.png" alt="The Frizz board: the project rail down the left, the composer and the queue of threads beside it, and on the right a card where an agent is asking an answerable question with numbered options drawn as keycaps, with its snooze clock and done check in the card's header." width="100%">
@@ -67,7 +65,7 @@ Frizz is a browser tab, a queue, and the agent CLIs you already pay for. It brin
67
65
 
68
66
  ### Projects
69
67
 
70
- Every directory you run `npx frizz` in becomes a project with its own board, all served by the one Frizz on your machine. The home page at `http://127.0.0.1:9393/` lists them.
68
+ Every folder you add becomes a project with its own board, all served by the one Frizz on your machine. The dashboard at `http://127.0.0.1:9393/` lists them, and it is where `npx frizz` lands when it is run outside one.
71
69
 
72
70
  <p align="center">
73
71
  <img src="assets/projects.png" alt="The Frizz home page: a rail of project icons down the left, and four project cards — design-system, acme-web, acme-api, frizz — each with its home-relative path and when it was last opened, plus an Add a project card." width="100%">
@@ -134,10 +132,11 @@ Frizz production launcher
134
132
 
135
133
  Usage: npx frizz [options]
136
134
 
137
- Run it in the directory you want to work in. One server serves EVERY project on this machine,
138
- each at its own /project/<name> URL, so a second run joins the one already going. Runs the
139
- npm-resolved immutable Frizz package, then opens it in your default browser. Use frizz-dev only
140
- for a source checkout.
135
+ Run it anywhere. One server serves EVERY project on this machine, each at its own
136
+ /project/<name> URL, so a second run joins the one already going. In a folder that is already a
137
+ project it opens that project's board; anywhere else it opens the dashboard, where projects are
138
+ added. Runs the npm-resolved immutable Frizz package, then opens it in your default browser. Use
139
+ frizz-dev only for a source checkout.
141
140
 
142
141
  Options:
143
142
  --no-app print the URL without opening a browser
package/dist/frizz.js CHANGED
@@ -862,11 +862,6 @@ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative
862
862
  function legacyFrizzRoot(home = homedir2()) {
863
863
  return join3(home, LEGACY_DIR_NAME);
864
864
  }
865
- function isLegacyStateRoot(root, exists) {
866
- if (!exists(root)) return false;
867
- if (!exists(join3(root, ".id")) && !exists(join3(root, ".gitignore"))) return true;
868
- return exists(join3(root, "projects")) || exists(join3(root, "registry.json"));
869
- }
870
865
  function xdg(env, name) {
871
866
  const value = env[name];
872
867
  return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
@@ -909,7 +904,7 @@ function frizzPaths(options2 = {}) {
909
904
  const home = options2.home ?? homedir2();
910
905
  const exists = options2.exists ?? existsSync2;
911
906
  const legacyRoot = legacyFrizzRoot(home);
912
- if (isLegacyStateRoot(legacyRoot, exists)) {
907
+ if (exists(legacyRoot)) {
913
908
  return { data: legacyRoot, state: legacyRoot, cache: legacyRoot, legacy: true };
914
909
  }
915
910
  const explicit = {
@@ -931,6 +926,9 @@ function frizzRoots() {
931
926
  function projectStateDir(projectId, home) {
932
927
  return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "projects", projectId);
933
928
  }
929
+ function launchHostDir(home) {
930
+ return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "host");
931
+ }
934
932
  var LEGACY_DIR_NAME, cached;
935
933
  var init_frizz_paths = __esm({
936
934
  "packages/server/src/frizz-paths.ts"() {
@@ -13641,7 +13639,7 @@ var init_src = __esm({
13641
13639
  "running",
13642
13640
  // process alive, turn in flight
13643
13641
  "perm-prompt",
13644
- // process alive, paused on an interactive permission prompt (answer in the terminal)
13642
+ // process alive, parked on a permission request a person has not answered yet
13645
13643
  "turn-idle",
13646
13644
  // process alive, waiting at the prompt
13647
13645
  "exited"
@@ -21105,13 +21103,6 @@ function isHomeDirectory(dir, home = homedir8()) {
21105
21103
  return false;
21106
21104
  }
21107
21105
  }
21108
- function isStateRootHome(dir, home = homedir8()) {
21109
- return isHomeDirectory(dir, home) && frizzPaths({ home }).legacy;
21110
- }
21111
- function refuseStateRootHome(dir, home = homedir8()) {
21112
- if (isStateRootHome(dir, home))
21113
- throw new Error("Frizz keeps its own files in ~/.frizz on this machine, so the home folder cannot be a project here \u2014 choose a folder inside it.");
21114
- }
21115
21106
  function isWorldWritable(dir, platform = process.platform) {
21116
21107
  if (platform === "win32") return false;
21117
21108
  try {
@@ -21148,7 +21139,7 @@ function discoverProjectRoot(cwd = process.cwd(), home = homedir8()) {
21148
21139
  const stop = resolve3(home);
21149
21140
  const filesystemRoot = parse(dir).root;
21150
21141
  for (let at = dir; ; at = dirname8(at)) {
21151
- if (at === stop || at === filesystemRoot || isHomeDirectory(at, home)) break;
21142
+ if (at === stop || at === filesystemRoot) break;
21152
21143
  if (existsSync4(projectIdPath(at))) return at;
21153
21144
  if (hasAny(at, REPO_MARKERS)) return at;
21154
21145
  if (hasAny(at, PROJECT_MARKERS)) return at;
@@ -21160,7 +21151,6 @@ var FRIZZ_DIR, ID_FILE, SELF_IGNORE, REPO_MARKERS, PROJECT_MARKERS;
21160
21151
  var init_project_root = __esm({
21161
21152
  "packages/server/src/project-root.ts"() {
21162
21153
  "use strict";
21163
- init_frizz_paths();
21164
21154
  init_project_identity();
21165
21155
  FRIZZ_DIR = ".frizz";
21166
21156
  ID_FILE = ".id";
@@ -21269,6 +21259,7 @@ function canonicalPath(dir) {
21269
21259
  }
21270
21260
  function registerProject(input, home = homedir9()) {
21271
21261
  input = { ...input, dir: canonicalPath(input.dir) };
21262
+ if (input.dir === canonicalPath(launchHostDir(home))) return { action: "unlisted" };
21272
21263
  const registry = readRegistry(home);
21273
21264
  if (registry.forgotten) {
21274
21265
  registry.forgotten = registry.forgotten.filter((id) => id !== input.id);
@@ -25467,6 +25458,7 @@ init_boot_progress();
25467
25458
  init_frizz_paths();
25468
25459
  import { execFileSync as execFileSync3 } from "node:child_process";
25469
25460
  import { createServer as createServer2 } from "node:net";
25461
+ import { randomUUID as randomUUID5 } from "node:crypto";
25470
25462
  import { existsSync as existsSync6, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync10, realpathSync as realpathSync5, rmSync as rmSync8, symlinkSync as symlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
25471
25463
  import { homedir as homedir10, tmpdir as tmpdir2 } from "node:os";
25472
25464
  import { basename as basename5, dirname as dirname10, join as join13, resolve as resolve4 } from "node:path";
@@ -25606,6 +25598,7 @@ function prepareSandbox(env = process.env, realHome = homedir10()) {
25606
25598
  } catch {
25607
25599
  throw new Error("--sandbox mints a throwaway git repository and git is not available \u2014 install git, or play in a repo of your own with a temp HOME");
25608
25600
  }
25601
+ writeProjectIdFile(project, randomUUID5());
25609
25602
  process.chdir(project);
25610
25603
  return { home, project };
25611
25604
  }
@@ -25646,26 +25639,26 @@ function cleanupSandbox(home) {
25646
25639
  } catch {
25647
25640
  }
25648
25641
  }
25649
- function emptyLaunchMessage(intent) {
25650
- return intent.reason === "home" ? "Frizz keeps its own files in ~/.frizz on this machine, so it cannot open your home directory as a project, and there is no other project to show yet. cd into another folder and run frizz there." : `A folder every account can write to cannot be a project: ${intent.directory}. Run frizz in a folder only you can write to.`;
25651
- }
25652
25642
  function resolveLaunchIntent(cwd = process.cwd(), home = homedir10(), env = process.env) {
25653
25643
  const candidate = realpathSync5(discoverProjectRoot(cwd, home));
25654
25644
  const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
25655
- const isHome = isHomeDirectory(candidate, home);
25656
- const refused = isWorldWritable(candidate) ? "world-writable" : isStateRootHome(candidate, home) ? "home" : void 0;
25657
- if (!refused && (known || hasProjectMarker(candidate) && !isHome))
25658
- return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
25659
- const host = mostRecentProject(home, env);
25660
- if (!host) {
25661
- if (refused) return { kind: "empty", directory: candidate, reason: refused };
25662
- return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
25663
- }
25664
- return refused || isHome ? { kind: "grid", workspace: host } : { kind: "offer", workspace: host, directory: candidate };
25645
+ const refused = isHomeDirectory(candidate, home) || isWorldWritable(candidate);
25646
+ if (known && !refused) return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
25647
+ const host = mostRecentProject(home, env) ?? hostWorkspace(home);
25648
+ return !refused && hasProjectMarker(candidate) ? { kind: "offer", workspace: host, directory: candidate } : { kind: "grid", workspace: host };
25649
+ }
25650
+ function hostWorkspace(home = homedir10()) {
25651
+ const dir = launchHostDir(home);
25652
+ mkdirSync10(dir, { recursive: true });
25653
+ const root = realpathSync5(dir);
25654
+ const id = ensureProjectIdFile(root, home);
25655
+ const stateDir = projectStateDir(id, home);
25656
+ mkdirSync10(stateDir, { recursive: true });
25657
+ return { root, id, stateDir, name: basename5(root), identityScope: "repository" };
25665
25658
  }
25666
25659
  function mostRecentProject(home, env) {
25667
25660
  for (const entry of listProjects(home)) {
25668
- if (entry.stale || isStateRootHome(entry.path, home) || isWorldWritable(entry.path)) continue;
25661
+ if (entry.stale || isHomeDirectory(entry.path, home) || isWorldWritable(entry.path)) continue;
25669
25662
  try {
25670
25663
  return resolveWorkspace(entry.path, home, env);
25671
25664
  } catch {
@@ -25689,7 +25682,6 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process
25689
25682
  }
25690
25683
  const root0 = realpathSync5(gitRoot ?? discoverProjectRoot(cwd, home));
25691
25684
  refuseWorldWritableProject(root0);
25692
- refuseStateRootHome(root0, home);
25693
25685
  const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
25694
25686
  const root = identity?.root ?? root0;
25695
25687
  const id = ensureProjectIdFile(root, home, identity?.id);
@@ -26019,7 +26011,7 @@ init_restart_supervisor();
26019
26011
  init_logging();
26020
26012
  init_boot_progress();
26021
26013
  import { fork } from "node:child_process";
26022
- import { randomUUID as randomUUID5 } from "node:crypto";
26014
+ import { randomUUID as randomUUID6 } from "node:crypto";
26023
26015
  import { existsSync as existsSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync11 } from "node:fs";
26024
26016
  import { createServer as createNetServer } from "node:net";
26025
26017
  import { basename as basename6, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve5, sep as sep3 } from "node:path";
@@ -26264,7 +26256,7 @@ var Supervisor = class {
26264
26256
  watchSubscribe;
26265
26257
  reexec;
26266
26258
  supervisorLock;
26267
- statusPublisherToken = randomUUID5();
26259
+ statusPublisherToken = randomUUID6();
26268
26260
  processGeneration = currentProcessGeneration();
26269
26261
  ownerToken;
26270
26262
  launchTarget;
@@ -27220,7 +27212,7 @@ var npmRegistryReleaseAdapter = createNpmRegistryReleaseAdapter();
27220
27212
  // src/server-release.ts
27221
27213
  init_frizz_paths();
27222
27214
  import { execFile as execFile4 } from "node:child_process";
27223
- import { createHash as createHash4, randomUUID as randomUUID6 } from "node:crypto";
27215
+ import { createHash as createHash4, randomUUID as randomUUID7 } from "node:crypto";
27224
27216
  import {
27225
27217
  closeSync as closeSync5,
27226
27218
  existsSync as existsSync9,
@@ -27376,7 +27368,7 @@ function npmServerPackageInstaller(env = process.env) {
27376
27368
  }
27377
27369
  function atomicJson2(path, value) {
27378
27370
  mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
27379
- const temp = `${path}.${randomUUID6()}.tmp`;
27371
+ const temp = `${path}.${randomUUID7()}.tmp`;
27380
27372
  try {
27381
27373
  const fd = openSync6(temp, "wx", 384);
27382
27374
  try {
@@ -27477,7 +27469,7 @@ var ServerReleaseStore = class {
27477
27469
  const spec = { ...this.spec, version };
27478
27470
  assertSpec(spec, this.compatibility);
27479
27471
  mkdirSync12(this.generations, { recursive: true, mode: 448 });
27480
- const id = randomUUID6();
27472
+ const id = randomUUID7();
27481
27473
  const staging = join15(this.generations, `${id}.staging`);
27482
27474
  const destination = join15(this.generations, id);
27483
27475
  mkdirSync12(staging, { mode: 448 });
@@ -27503,7 +27495,7 @@ var ServerReleaseStore = class {
27503
27495
  // src/server-owner.ts
27504
27496
  init_frizz_paths();
27505
27497
  init_project_launch();
27506
- import { randomUUID as randomUUID7 } from "node:crypto";
27498
+ import { randomUUID as randomUUID8 } from "node:crypto";
27507
27499
  import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, realpathSync as realpathSync7 } from "node:fs";
27508
27500
  import { join as join16 } from "node:path";
27509
27501
  var STABLE_SERVER_OWNER_NAME = "frizz-server";
@@ -27548,7 +27540,7 @@ function wrapLease(lease) {
27548
27540
  token: lease.token,
27549
27541
  pid: lease.pid,
27550
27542
  processStart: lease.processStart,
27551
- publisherToken: randomUUID7()
27543
+ publisherToken: randomUUID8()
27552
27544
  };
27553
27545
  }
27554
27546
  function acquireStableServerOwner(roots) {
@@ -27679,10 +27671,11 @@ if (options.help) {
27679
27671
 
27680
27672
  Usage: npx ${PACKAGE_NAME} [options]
27681
27673
 
27682
- Run it in the directory you want to work in. One server serves EVERY project on this machine,
27683
- each at its own /project/<name> URL, so a second run joins the one already going. Runs the
27684
- npm-resolved immutable Frizz package, then opens it in your default browser. Use frizz-dev only
27685
- for a source checkout.
27674
+ Run it anywhere. One server serves EVERY project on this machine, each at its own
27675
+ /project/<name> URL, so a second run joins the one already going. In a folder that is already a
27676
+ project it opens that project's board; anywhere else it opens the dashboard, where projects are
27677
+ added. Runs the npm-resolved immutable Frizz package, then opens it in your default browser. Use
27678
+ frizz-dev only for a source checkout.
27686
27679
 
27687
27680
  Options:
27688
27681
  --no-app print the URL without opening a browser
@@ -27733,7 +27726,6 @@ var workspace = (() => {
27733
27726
  return workspaceFromLaunchTarget(pinned);
27734
27727
  }
27735
27728
  const intent = resolveLaunchIntent();
27736
- if (intent.kind === "empty") throw new Error(emptyLaunchMessage(intent));
27737
27729
  launchIntent = intent;
27738
27730
  return intent.workspace;
27739
27731
  } catch (error) {
@@ -27879,7 +27871,7 @@ async function openOrPrint(port, reused, path = "") {
27879
27871
  if (sandbox) warnings.push(`Sandbox: everything here is throwaway (${sandbox.home}) and is deleted when this terminal closes.`);
27880
27872
  if (sessionNotice && !reused) warnings.push(sessionNotice);
27881
27873
  if (!readout) {
27882
- console.log(`${reused ? "reusing" : "started"} Frizz for ${workspace.root}`);
27874
+ console.log(`${reused ? "reusing" : "started"} Frizz${launchIntent && launchIntent.kind !== "open" ? "" : ` for ${workspace.root}`}`);
27883
27875
  console.log(url);
27884
27876
  if (publicOrigin) console.log(activeAccessLink?.url ?? `${publicOrigin}/`);
27885
27877
  for (const warning of warnings) console.log(warning);
@@ -27890,7 +27882,9 @@ async function openOrPrint(port, reused, path = "") {
27890
27882
  [
27891
27883
  { label: "Local", value: boardAddress(url), accent: true },
27892
27884
  ...publicOrigin ? [{ label: "Public", value: activeAccessLink?.url ?? `${publicOrigin}/`, accent: true }] : [],
27893
- { label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
27885
+ // Only an `open` launch is ABOUT its workspace; any other is hosted on a project the operator
27886
+ // did not pick (launcher.ts resolveLaunchIntent), and naming it here reads as "you opened this".
27887
+ ...launchIntent && launchIntent.kind !== "open" ? [] : [{ label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` }],
27894
27888
  ...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
27895
27889
  ],
27896
27890
  reused ? (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frizz",
3
- "version": "0.13.10",
3
+ "version": "0.13.12",
4
4
  "private": false,
5
5
  "description": "A local web UI for running many coding agents at once",
6
6
  "keywords": [
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "frizzServer": {
68
68
  "package": "frizz-server",
69
- "version": "0.15.20",
69
+ "version": "0.15.22",
70
70
  "protocol": 1,
71
71
  "dataEpoch": 1
72
72
  }