frizz 0.13.8 → 0.13.11

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 +14 -15
  2. package/dist/frizz.js +547 -419
  3. package/package.json +2 -2
package/dist/frizz.js CHANGED
@@ -450,6 +450,15 @@ var init_access_codes = __esm({
450
450
  const id = _AccessStore.idOf(payload);
451
451
  return !(id && this.sessions.isRevoked(id));
452
452
  }
453
+ /**
454
+ * Which device this session belongs to, for the audit trail: its record id (what `frizz --sessions`
455
+ * lists and `--sign-out` takes), `"legacy"` for a session minted before ids existed, or null when it
456
+ * does not verify. Never the session itself — the id is the one part of it that is not a credential.
457
+ */
458
+ sessionIdOf(session) {
459
+ if (!session || !this.verifySession(session)) return null;
460
+ return _AccessStore.idOf(session.slice(0, session.lastIndexOf("."))) ?? "legacy";
461
+ }
453
462
  /**
454
463
  * Sign out the device HOLDING this session — the "Sign out this device" row, not `--sign-out`.
455
464
  *
@@ -846,6 +855,293 @@ code{font-family:var(--mono);font-size:.92em;padding:.1em .35em;border-radius:4p
846
855
  }
847
856
  });
848
857
 
858
+ // packages/server/src/frizz-paths.ts
859
+ import { existsSync as existsSync2 } from "node:fs";
860
+ import { homedir as homedir2, tmpdir } from "node:os";
861
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative, resolve, sep as sep2 } from "node:path";
862
+ function legacyFrizzRoot(home = homedir2()) {
863
+ return join3(home, LEGACY_DIR_NAME);
864
+ }
865
+ function xdg(env, name) {
866
+ const value = env[name];
867
+ return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
868
+ }
869
+ function insideHome(child, home) {
870
+ const norm = (value) => value.replace(/[\\/]+$/u, "").replaceAll("\\", "/").toLowerCase();
871
+ const parent = norm(home);
872
+ const inner = norm(child);
873
+ return inner === parent || inner.startsWith(`${parent}/`);
874
+ }
875
+ function windowsRoots(env, home) {
876
+ const local = env.LOCALAPPDATA && insideHome(env.LOCALAPPDATA, home) ? env.LOCALAPPDATA : join3(home, "AppData", "Local");
877
+ const base = join3(local, "Frizz");
878
+ return {
879
+ data: join3(base, "Data"),
880
+ state: join3(base, "State"),
881
+ cache: join3(base, "Cache")
882
+ };
883
+ }
884
+ function darwinRoots(home) {
885
+ const support = join3(home, "Library", "Application Support", "Frizz");
886
+ return {
887
+ data: support,
888
+ // macOS has no state directory concept; Application Support is where this belongs, and the log
889
+ // files themselves already live under each project's own directory.
890
+ state: support,
891
+ cache: join3(home, "Library", "Caches", "Frizz")
892
+ };
893
+ }
894
+ function xdgRoots(env, home) {
895
+ return {
896
+ data: join3(xdg(env, "XDG_DATA_HOME") ?? join3(home, ".local", "share"), "frizz"),
897
+ state: join3(xdg(env, "XDG_STATE_HOME") ?? join3(home, ".local", "state"), "frizz"),
898
+ cache: join3(xdg(env, "XDG_CACHE_HOME") ?? join3(home, ".cache"), "frizz")
899
+ };
900
+ }
901
+ function frizzPaths(options2 = {}) {
902
+ const env = options2.env ?? process.env;
903
+ const platform = options2.platform ?? process.platform;
904
+ const home = options2.home ?? homedir2();
905
+ const exists = options2.exists ?? existsSync2;
906
+ const legacyRoot = legacyFrizzRoot(home);
907
+ if (exists(legacyRoot)) {
908
+ return { data: legacyRoot, state: legacyRoot, cache: legacyRoot, legacy: true };
909
+ }
910
+ const explicit = {
911
+ data: xdg(env, "XDG_DATA_HOME"),
912
+ state: xdg(env, "XDG_STATE_HOME"),
913
+ cache: xdg(env, "XDG_CACHE_HOME")
914
+ };
915
+ const platformRoots = platform === "win32" ? windowsRoots(env, home) : platform === "darwin" ? darwinRoots(home) : xdgRoots(env, home);
916
+ return {
917
+ data: explicit.data ? join3(explicit.data, "frizz") : platformRoots.data,
918
+ state: explicit.state ? join3(explicit.state, "frizz") : platformRoots.state,
919
+ cache: explicit.cache ? join3(explicit.cache, "frizz") : platformRoots.cache,
920
+ legacy: false
921
+ };
922
+ }
923
+ function frizzRoots() {
924
+ return cached ??= frizzPaths();
925
+ }
926
+ function projectStateDir(projectId, home) {
927
+ return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "projects", projectId);
928
+ }
929
+ function launchHostDir(home) {
930
+ return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "host");
931
+ }
932
+ var LEGACY_DIR_NAME, cached;
933
+ var init_frizz_paths = __esm({
934
+ "packages/server/src/frizz-paths.ts"() {
935
+ "use strict";
936
+ LEGACY_DIR_NAME = ".frizz";
937
+ }
938
+ });
939
+
940
+ // packages/server/src/logging.ts
941
+ import {
942
+ appendFileSync,
943
+ mkdirSync as mkdirSync2,
944
+ openSync,
945
+ readdirSync,
946
+ rmSync,
947
+ statSync as statSync2,
948
+ symlinkSync,
949
+ writeFileSync as writeFileSync2,
950
+ writeSync
951
+ } from "node:fs";
952
+ import { homedir as homedir3 } from "node:os";
953
+ import { join as join4 } from "node:path";
954
+ function defaultLogRoot(stateDir, home = homedir3()) {
955
+ return stateDir ? join4(stateDir, "logs") : join4(frizzPaths({ home }).state, "logs");
956
+ }
957
+ function runStamp(at) {
958
+ const pad = (value) => String(value).padStart(2, "0");
959
+ return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
960
+ }
961
+ function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir3(), env = process.env) {
962
+ const name = `frizz-${runStamp(at)}-${pid}.log`;
963
+ const override = env[LOG_PATH_ENV]?.trim();
964
+ if (override) return override.endsWith(".log") ? override : join4(override, name);
965
+ return join4(defaultLogRoot(stateDir, home), name);
966
+ }
967
+ function latestLogPath(dir) {
968
+ return join4(dir, "latest.log");
969
+ }
970
+ function linkLatest(dir, target2) {
971
+ const link = latestLogPath(dir);
972
+ try {
973
+ rmSync(link, { force: true });
974
+ symlinkSync(target2, link);
975
+ } catch {
976
+ try {
977
+ writeFileSync2(link, `${target2}
978
+ `, { mode: 384 });
979
+ } catch {
980
+ }
981
+ }
982
+ }
983
+ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
984
+ let entries;
985
+ try {
986
+ entries = readdirSync(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
987
+ } catch {
988
+ return;
989
+ }
990
+ const dated = entries.map((name) => {
991
+ try {
992
+ return { name, at: statSync2(join4(dir, name)).mtimeMs };
993
+ } catch {
994
+ return { name, at: 0 };
995
+ }
996
+ }).sort((a, b) => b.at - a.at);
997
+ const cutoff = now - days * 24 * 60 * 60 * 1e3;
998
+ const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
999
+ for (const entry of stale) {
1000
+ try {
1001
+ rmSync(join4(dir, entry.name), { force: true });
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ }
1006
+ function formatDiskLine(record2) {
1007
+ const at = new Date(record2.at);
1008
+ const pad = (value, width = 2) => String(value).padStart(width, "0");
1009
+ const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}.${pad(at.getMilliseconds(), 3)}`;
1010
+ return `${clock} ${record2.level.toUpperCase().padEnd(5)} ${record2.scope.padEnd(12)} ${record2.message}
1011
+ `;
1012
+ }
1013
+ function openLogFile(path) {
1014
+ try {
1015
+ mkdirSync2(join4(path, ".."), { recursive: true, mode: 448 });
1016
+ return openSync(path, "a", 384);
1017
+ } catch {
1018
+ return null;
1019
+ }
1020
+ }
1021
+ function createLogger(options2 = {}) {
1022
+ const path = options2.file === null ? null : options2.file ?? runLogPath(options2.stateDir);
1023
+ const level = options2.level ?? "debug";
1024
+ const now = options2.now ?? Date.now;
1025
+ const threshold = LEVEL_ORDER[level];
1026
+ const maxBytes = options2.maxBytes ?? MAX_LOG_BYTES;
1027
+ let fd = path === null ? null : openLogFile(path);
1028
+ if (fd !== null && path !== null && options2.owner !== false) {
1029
+ const dir = join4(path, "..");
1030
+ pruneRunLogs(dir);
1031
+ linkLatest(dir, path);
1032
+ }
1033
+ let written = 0;
1034
+ const listeners = /* @__PURE__ */ new Set();
1035
+ const emit = (recordLevel, scope, message) => {
1036
+ if (LEVEL_ORDER[recordLevel] < threshold) return;
1037
+ const record2 = { at: now(), level: recordLevel, scope, message };
1038
+ if (fd !== null) {
1039
+ try {
1040
+ const line = formatDiskLine(record2);
1041
+ if (written + line.length > maxBytes) {
1042
+ writeSync(fd, `--- log truncated at ${maxBytes} bytes ---
1043
+ `);
1044
+ fd = null;
1045
+ } else {
1046
+ writeSync(fd, line);
1047
+ written += line.length;
1048
+ }
1049
+ } catch {
1050
+ fd = null;
1051
+ }
1052
+ }
1053
+ for (const listener of listeners) {
1054
+ try {
1055
+ listener(record2);
1056
+ } catch {
1057
+ }
1058
+ }
1059
+ };
1060
+ return {
1061
+ debug: (scope, message) => emit("debug", scope, message),
1062
+ info: (scope, message) => emit("info", scope, message),
1063
+ warn: (scope, message) => emit("warn", scope, message),
1064
+ error: (scope, message) => emit("error", scope, message),
1065
+ get file() {
1066
+ return fd === null ? null : path;
1067
+ },
1068
+ onRecord(listener) {
1069
+ listeners.add(listener);
1070
+ return () => listeners.delete(listener);
1071
+ },
1072
+ close() {
1073
+ listeners.clear();
1074
+ fd = null;
1075
+ }
1076
+ };
1077
+ }
1078
+ function setAmbientLogger(logger2) {
1079
+ ambient = logger2;
1080
+ return logger2;
1081
+ }
1082
+ function formatFeedLine(record2) {
1083
+ return formatDiskLine(record2).trimEnd();
1084
+ }
1085
+ function attachTerminalMirror(logger2, enabled = process.env.FRIZZ_DEBUG === "1") {
1086
+ if (!enabled) return () => {
1087
+ };
1088
+ return logger2.onRecord((record2) => process.stderr.write(`${formatFeedLine(record2)}
1089
+ `));
1090
+ }
1091
+ function ambientLogger() {
1092
+ if (ambient) return ambient;
1093
+ const inherited = process.env[LOG_FILE_ENV] ?? process.env[LOG_PATH_ENV];
1094
+ const level = process.env[LOG_LEVEL_ENV];
1095
+ const logger2 = createLogger({
1096
+ ...inherited ? { file: inherited, owner: false } : { file: null },
1097
+ ...level && level in LEVEL_ORDER ? { level } : {}
1098
+ });
1099
+ attachTerminalMirror(logger2);
1100
+ ambient = logger2;
1101
+ return ambient;
1102
+ }
1103
+ function logEnvironment(logger2, level = "debug") {
1104
+ return logger2.file ? { [LOG_FILE_ENV]: logger2.file, [LOG_LEVEL_ENV]: level } : {};
1105
+ }
1106
+ var LEVEL_ORDER, LOG_FILE_ENV, LOG_LEVEL_ENV, LOG_PATH_ENV, RETAINED_RUNS, RETAINED_DAYS, MAX_LOG_BYTES, ambient, log;
1107
+ var init_logging = __esm({
1108
+ "packages/server/src/logging.ts"() {
1109
+ "use strict";
1110
+ init_frizz_paths();
1111
+ LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
1112
+ LOG_FILE_ENV = "FRIZZ_LOG_FILE";
1113
+ LOG_LEVEL_ENV = "FRIZZ_LOG_LEVEL";
1114
+ LOG_PATH_ENV = "FRIZZ_LOG_PATH";
1115
+ RETAINED_RUNS = 20;
1116
+ RETAINED_DAYS = 14;
1117
+ MAX_LOG_BYTES = 32 * 1024 * 1024;
1118
+ ambient = null;
1119
+ log = {
1120
+ debug: (scope, message) => ambientLogger().debug(scope, message),
1121
+ info: (scope, message) => ambientLogger().info(scope, message),
1122
+ warn: (scope, message) => ambientLogger().warn(scope, message),
1123
+ error: (scope, message) => ambientLogger().error(scope, message)
1124
+ };
1125
+ }
1126
+ });
1127
+
1128
+ // packages/server/src/audit.ts
1129
+ import { AsyncLocalStorage } from "node:async_hooks";
1130
+ function encodeCallerStamp(stamp) {
1131
+ return Buffer.from(JSON.stringify(stamp)).toString("base64url");
1132
+ }
1133
+ var CALLER_HEADER, requestOrigin;
1134
+ var init_audit = __esm({
1135
+ "packages/server/src/audit.ts"() {
1136
+ "use strict";
1137
+ init_access_codes();
1138
+ init_frizz_paths();
1139
+ init_logging();
1140
+ CALLER_HEADER = "x-frizz-caller";
1141
+ requestOrigin = new AsyncLocalStorage();
1142
+ }
1143
+ });
1144
+
849
1145
  // node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
850
1146
  var require_identity = __commonJS({
851
1147
  "node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js"(exports) {
@@ -12982,31 +13278,6 @@ var init_drainable_worker = __esm({
12982
13278
  }
12983
13279
  });
12984
13280
 
12985
- // packages/shared/src/frizz-md.ts
12986
- var FrizzMdLanding, FrizzMdAutonomy, FRIZZ_MD_NOTES_MAX, FrizzMdAnswers, FrizzMdStatus;
12987
- var init_frizz_md = __esm({
12988
- "packages/shared/src/frizz-md.ts"() {
12989
- "use strict";
12990
- init_zod();
12991
- FrizzMdLanding = external_exports.enum(["pull-request", "main", "main-push", "branch"]);
12992
- FrizzMdAutonomy = external_exports.enum(["decide", "balanced", "check-in"]);
12993
- FRIZZ_MD_NOTES_MAX = 8e3;
12994
- FrizzMdAnswers = external_exports.object({
12995
- landing: FrizzMdLanding,
12996
- autonomy: FrizzMdAutonomy,
12997
- notes: external_exports.string().max(FRIZZ_MD_NOTES_MAX)
12998
- }).strict();
12999
- FrizzMdStatus = external_exports.object({
13000
- /** A FRIZZ.md is already at the project root: the project has its rules, so nothing is asked. */
13001
- exists: external_exports.boolean(),
13002
- /** The operator skipped the questionnaire for this project: it is not asked again. */
13003
- skipped: external_exports.boolean(),
13004
- /** The branch the landing rules name — the remote's default, else the checked-out one, else `main`. */
13005
- defaultBranch: external_exports.string()
13006
- });
13007
- }
13008
- });
13009
-
13010
13281
  // packages/shared/src/receipt-bus.ts
13011
13282
  var init_receipt_bus = __esm({
13012
13283
  "packages/shared/src/receipt-bus.ts"() {
@@ -13254,7 +13525,7 @@ function requireEffortOutsideAcp(profile, ctx) {
13254
13525
  function fallbackPort(base) {
13255
13526
  return base + 1e4;
13256
13527
  }
13257
- var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
13528
+ var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, SteerThreadInput, SteerThreadResult, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
13258
13529
  var init_src = __esm({
13259
13530
  "packages/shared/src/index.ts"() {
13260
13531
  "use strict";
@@ -13267,7 +13538,6 @@ var init_src = __esm({
13267
13538
  init_code_fences();
13268
13539
  init_delta();
13269
13540
  init_drainable_worker();
13270
- init_frizz_md();
13271
13541
  init_interactions();
13272
13542
  init_receipt_bus();
13273
13543
  init_relay_protocol();
@@ -13369,7 +13639,7 @@ var init_src = __esm({
13369
13639
  "running",
13370
13640
  // process alive, turn in flight
13371
13641
  "perm-prompt",
13372
- // 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
13373
13643
  "turn-idle",
13374
13644
  // process alive, waiting at the prompt
13375
13645
  "exited"
@@ -14566,6 +14836,19 @@ var init_src = __esm({
14566
14836
  // ambiguous, and only the operator decides to risk a second copy.
14567
14837
  supersedes: external_exports.string().min(1).max(200).optional()
14568
14838
  });
14839
+ SteerThreadInput = external_exports.object({
14840
+ from: ThreadSlug,
14841
+ to: ThreadSlug,
14842
+ message: external_exports.string().min(1).max(1e5)
14843
+ }).strict();
14844
+ SteerThreadResult = external_exports.object({
14845
+ slug: ThreadSlug,
14846
+ title: external_exports.string(),
14847
+ // What the target was doing when the message arrived: `running` ⇒ it reads it at its next tool
14848
+ // boundary; `resting` ⇒ the message started its next turn; `reopened` ⇒ it was marked done and this
14849
+ // message put it back on the board.
14850
+ delivery: external_exports.enum(["running", "resting", "reopened"])
14851
+ }).strict();
14569
14852
  DismissFailedFollowUpInput = external_exports.object({
14570
14853
  slug: ThreadSlug,
14571
14854
  deliveryId: external_exports.string().min(1).max(200)
@@ -15275,6 +15558,20 @@ ${DISPATCH_TASK_BANNER}
15275
15558
  // pairing, and `projectTranscriptPeerNames` uses this id to ask it. Never a drawer key on its own —
15276
15559
  // that is `peerDispatchId`, which the same pass can also supply once this resolves.
15277
15560
  peerSenderTaskId: external_exports.string().optional(),
15561
+ // ANOTHER TOP-LEVEL SESSION wrote this user turn — a thread messaging this one through Claude Code's
15562
+ // cross-session channel (see parseCrossSessionMessage), most often the coordinator thread that spawned
15563
+ // it. Not the human, and not a sub-agent either: the chat draws it as an expandable hairline naming
15564
+ // the sending thread, with the body one click away. `displayText` carries the unwrapped body.
15565
+ //
15566
+ // `name` is the sender's Claude Code session name, the fallback label. `thread` is the Frizz thread
15567
+ // behind it, filled in by the server from the sender's socket (session-peer.ts) — absent when the
15568
+ // sender is not a Frizz thread or is gone without having been resolved, and then the line names the
15569
+ // session without a link. `project` is the URL slug of the sender's project.
15570
+ sessionPeer: external_exports.object({
15571
+ name: external_exports.string().optional(),
15572
+ socket: external_exports.string().optional(),
15573
+ thread: external_exports.object({ project: external_exports.string().optional(), slug: external_exports.string(), title: external_exports.string() }).optional()
15574
+ }).optional(),
15278
15575
  // FRIZZ REFUSED the ```awaiting fence this message ends in — it named something that is not running,
15279
15576
  // or named nothing at all, or used a retired line kind (see `isParkCorrection`). The fence is not a
15280
15577
  // park, so the chat draws nothing for it: an hourglass card with a park button asserts a wait that
@@ -15384,8 +15681,9 @@ function responseJson(res, status, value) {
15384
15681
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
15385
15682
  res.end(JSON.stringify(value));
15386
15683
  }
15387
- function proxyHeaders(req, childPort, vouchSameOrigin = false) {
15684
+ function proxyHeaders(req, childPort, callerStamp, vouchSameOrigin = false) {
15388
15685
  const headers = { ...req.headers };
15686
+ headers[CALLER_HEADER] = callerStamp;
15389
15687
  headers.host = `127.0.0.1:${childPort}`;
15390
15688
  if (typeof headers.origin === "string") headers.origin = `http://127.0.0.1:${childPort}`;
15391
15689
  for (const name of FORWARDED_HEADERS) delete headers[name];
@@ -15402,6 +15700,15 @@ function readCookie(header, name) {
15402
15700
  }
15403
15701
  return void 0;
15404
15702
  }
15703
+ function firstHeader(value) {
15704
+ const first = Array.isArray(value) ? value[0] : value;
15705
+ return first?.trim() || void 0;
15706
+ }
15707
+ function isLoopbackAddress(address) {
15708
+ if (!address) return false;
15709
+ const v4 = address.startsWith("::ffff:") ? address.slice("::ffff:".length) : address;
15710
+ return v4.startsWith("127.") || address === "::1";
15711
+ }
15405
15712
  function isControlRequest(req) {
15406
15713
  const url = new URL(req.url ?? "/", "http://frizz.invalid");
15407
15714
  return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH || url.pathname === SUPERVISOR_SIGN_OUT_PATH;
@@ -15435,6 +15742,7 @@ var init_restart_supervisor = __esm({
15435
15742
  init_local_origin();
15436
15743
  init_local_image();
15437
15744
  init_supervisor_pages();
15745
+ init_audit();
15438
15746
  init_src();
15439
15747
  SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
15440
15748
  SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
@@ -15799,6 +16107,28 @@ var init_restart_supervisor = __esm({
15799
16107
  if (!this.publicOrigin) return true;
15800
16108
  return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
15801
16109
  }
16110
+ /**
16111
+ * What this proxy knows about who sent a request, for the application server's audit trail
16112
+ * (audit.ts). The session is named by its record id, never its value, and only once it verifies.
16113
+ *
16114
+ * `clientIp` is only ever taken from a request that arrived as the public origin, and it is that
16115
+ * origin's CLAIM: a relay or tunnel reaches this port from loopback, so the real visitor address
16116
+ * exists only in what the relay chose to forward (`cf-connecting-ip` from Cloudflare).
16117
+ */
16118
+ callerStamp(req) {
16119
+ const remote2 = req.socket.remoteAddress ?? null;
16120
+ const via = this.arrivedPublicly(req) ? "public" : isLoopbackAddress(remote2) ? "loopback" : "network";
16121
+ const host = via === "loopback" ? void 0 : firstHeader(req.headers.host);
16122
+ const forwarded = firstHeader(req.headers["x-forwarded-for"])?.split(",")[0]?.trim();
16123
+ const clientIp = via === "public" ? firstHeader(req.headers["cf-connecting-ip"]) ?? firstHeader(req.headers["x-real-ip"]) ?? forwarded : void 0;
16124
+ return encodeCallerStamp({
16125
+ via,
16126
+ remote: remote2,
16127
+ ...host ? { host: host.slice(0, 200) } : {},
16128
+ ...clientIp ? { clientIp: clientIp.slice(0, 200) } : {},
16129
+ session: this.access.sessionIdOf(readCookie(req.headers.cookie, SESSION_COOKIE))
16130
+ });
16131
+ }
15802
16132
  /** Did this request arrive through the public origin carrying a live session? Loopback never does. */
15803
16133
  carriesRemoteSession(req) {
15804
16134
  return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
@@ -15898,134 +16228,55 @@ var init_restart_supervisor = __esm({
15898
16228
  port: childPort,
15899
16229
  method: req.method,
15900
16230
  path: req.url,
15901
- headers: proxyHeaders(req, childPort, this.vouchesSameOrigin(req))
15902
- }, (upstreamResponse) => {
15903
- res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
15904
- upstreamResponse.pipe(res);
15905
- });
15906
- upstream.once("error", () => {
15907
- if (res.headersSent) return res.destroy();
15908
- res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
15909
- res.end(recoveryPage(req.url ?? "/", "unreachable"));
15910
- });
15911
- req.pipe(upstream);
15912
- }
15913
- handleUpgrade(req, socket, head) {
15914
- if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
15915
- rejectWebSocketUpgrade(socket, 401, "Unauthorized");
15916
- return;
15917
- }
15918
- if (!this.authorityAccepted(req, false)) {
15919
- socket.destroy();
15920
- return;
15921
- }
15922
- const childPort = this.options.childPort();
15923
- if (!childPort) {
15924
- socket.destroy();
15925
- return;
15926
- }
15927
- const upstream = connect(childPort, "127.0.0.1");
15928
- for (const half of [socket, upstream]) {
15929
- this.upgradedSockets.add(half);
15930
- half.once("close", () => this.upgradedSockets.delete(half));
15931
- }
15932
- upstream.once("connect", () => {
15933
- const headers = proxyHeaders(req, childPort);
15934
- headers.connection = req.headers.connection ?? "Upgrade";
15935
- const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`];
15936
- for (const [name, value] of Object.entries(headers)) {
15937
- if (value === void 0) continue;
15938
- for (const entry of Array.isArray(value) ? value : [value]) lines.push(`${name}: ${entry}`);
15939
- }
15940
- upstream.write(`${lines.join("\r\n")}\r
15941
- \r
15942
- `);
15943
- if (head.length > 0) upstream.write(head);
15944
- socket.pipe(upstream).pipe(socket);
15945
- });
15946
- upstream.once("error", () => socket.destroy());
15947
- socket.once("error", () => upstream.destroy());
15948
- }
15949
- };
15950
- }
15951
- });
15952
-
15953
- // packages/server/src/frizz-paths.ts
15954
- import { existsSync as existsSync2 } from "node:fs";
15955
- import { homedir as homedir2, tmpdir } from "node:os";
15956
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative, resolve, sep as sep2 } from "node:path";
15957
- function legacyFrizzRoot(home = homedir2()) {
15958
- return join3(home, LEGACY_DIR_NAME);
15959
- }
15960
- function xdg(env, name) {
15961
- const value = env[name];
15962
- return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
15963
- }
15964
- function insideHome(child, home) {
15965
- const norm = (value) => value.replace(/[\\/]+$/u, "").replaceAll("\\", "/").toLowerCase();
15966
- const parent = norm(home);
15967
- const inner = norm(child);
15968
- return inner === parent || inner.startsWith(`${parent}/`);
15969
- }
15970
- function windowsRoots(env, home) {
15971
- const local = env.LOCALAPPDATA && insideHome(env.LOCALAPPDATA, home) ? env.LOCALAPPDATA : join3(home, "AppData", "Local");
15972
- const base = join3(local, "Frizz");
15973
- return {
15974
- data: join3(base, "Data"),
15975
- state: join3(base, "State"),
15976
- cache: join3(base, "Cache")
15977
- };
15978
- }
15979
- function darwinRoots(home) {
15980
- const support = join3(home, "Library", "Application Support", "Frizz");
15981
- return {
15982
- data: support,
15983
- // macOS has no state directory concept; Application Support is where this belongs, and the log
15984
- // files themselves already live under each project's own directory.
15985
- state: support,
15986
- cache: join3(home, "Library", "Caches", "Frizz")
15987
- };
15988
- }
15989
- function xdgRoots(env, home) {
15990
- return {
15991
- data: join3(xdg(env, "XDG_DATA_HOME") ?? join3(home, ".local", "share"), "frizz"),
15992
- state: join3(xdg(env, "XDG_STATE_HOME") ?? join3(home, ".local", "state"), "frizz"),
15993
- cache: join3(xdg(env, "XDG_CACHE_HOME") ?? join3(home, ".cache"), "frizz")
15994
- };
15995
- }
15996
- function frizzPaths(options2 = {}) {
15997
- const env = options2.env ?? process.env;
15998
- const platform = options2.platform ?? process.platform;
15999
- const home = options2.home ?? homedir2();
16000
- const exists = options2.exists ?? existsSync2;
16001
- const legacyRoot = legacyFrizzRoot(home);
16002
- if (exists(legacyRoot)) {
16003
- return { data: legacyRoot, state: legacyRoot, cache: legacyRoot, legacy: true };
16004
- }
16005
- const explicit = {
16006
- data: xdg(env, "XDG_DATA_HOME"),
16007
- state: xdg(env, "XDG_STATE_HOME"),
16008
- cache: xdg(env, "XDG_CACHE_HOME")
16009
- };
16010
- const platformRoots = platform === "win32" ? windowsRoots(env, home) : platform === "darwin" ? darwinRoots(home) : xdgRoots(env, home);
16011
- return {
16012
- data: explicit.data ? join3(explicit.data, "frizz") : platformRoots.data,
16013
- state: explicit.state ? join3(explicit.state, "frizz") : platformRoots.state,
16014
- cache: explicit.cache ? join3(explicit.cache, "frizz") : platformRoots.cache,
16015
- legacy: false
16016
- };
16017
- }
16018
- function frizzRoots() {
16019
- return cached ??= frizzPaths();
16020
- }
16021
- function projectStateDir(projectId, home) {
16022
- return join3(home ? frizzPaths({ home }).data : frizzRoots().data, "projects", projectId);
16023
- }
16024
- var LEGACY_DIR_NAME, cached;
16025
- var init_frizz_paths = __esm({
16026
- "packages/server/src/frizz-paths.ts"() {
16027
- "use strict";
16028
- LEGACY_DIR_NAME = ".frizz";
16231
+ headers: proxyHeaders(req, childPort, this.callerStamp(req), this.vouchesSameOrigin(req))
16232
+ }, (upstreamResponse) => {
16233
+ res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
16234
+ upstreamResponse.pipe(res);
16235
+ });
16236
+ upstream.once("error", () => {
16237
+ if (res.headersSent) return res.destroy();
16238
+ res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
16239
+ res.end(recoveryPage(req.url ?? "/", "unreachable"));
16240
+ });
16241
+ req.pipe(upstream);
16242
+ }
16243
+ handleUpgrade(req, socket, head) {
16244
+ if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
16245
+ rejectWebSocketUpgrade(socket, 401, "Unauthorized");
16246
+ return;
16247
+ }
16248
+ if (!this.authorityAccepted(req, false)) {
16249
+ socket.destroy();
16250
+ return;
16251
+ }
16252
+ const childPort = this.options.childPort();
16253
+ if (!childPort) {
16254
+ socket.destroy();
16255
+ return;
16256
+ }
16257
+ const upstream = connect(childPort, "127.0.0.1");
16258
+ for (const half of [socket, upstream]) {
16259
+ this.upgradedSockets.add(half);
16260
+ half.once("close", () => this.upgradedSockets.delete(half));
16261
+ }
16262
+ upstream.once("connect", () => {
16263
+ const headers = proxyHeaders(req, childPort, this.callerStamp(req));
16264
+ headers.connection = req.headers.connection ?? "Upgrade";
16265
+ const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`];
16266
+ for (const [name, value] of Object.entries(headers)) {
16267
+ if (value === void 0) continue;
16268
+ for (const entry of Array.isArray(value) ? value : [value]) lines.push(`${name}: ${entry}`);
16269
+ }
16270
+ upstream.write(`${lines.join("\r\n")}\r
16271
+ \r
16272
+ `);
16273
+ if (head.length > 0) upstream.write(head);
16274
+ socket.pipe(upstream).pipe(socket);
16275
+ });
16276
+ upstream.once("error", () => socket.destroy());
16277
+ socket.once("error", () => upstream.destroy());
16278
+ }
16279
+ };
16029
16280
  }
16030
16281
  });
16031
16282
 
@@ -19723,194 +19974,6 @@ var init_wrapper = __esm({
19723
19974
  }
19724
19975
  });
19725
19976
 
19726
- // packages/server/src/logging.ts
19727
- import {
19728
- appendFileSync,
19729
- mkdirSync as mkdirSync4,
19730
- openSync,
19731
- readdirSync,
19732
- rmSync as rmSync2,
19733
- statSync as statSync2,
19734
- symlinkSync,
19735
- writeFileSync as writeFileSync4,
19736
- writeSync
19737
- } from "node:fs";
19738
- import { homedir as homedir6 } from "node:os";
19739
- import { join as join6 } from "node:path";
19740
- function defaultLogRoot(stateDir, home = homedir6()) {
19741
- return stateDir ? join6(stateDir, "logs") : join6(frizzPaths({ home }).state, "logs");
19742
- }
19743
- function runStamp(at) {
19744
- const pad = (value) => String(value).padStart(2, "0");
19745
- return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
19746
- }
19747
- function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir6(), env = process.env) {
19748
- const name = `frizz-${runStamp(at)}-${pid}.log`;
19749
- const override = env[LOG_PATH_ENV]?.trim();
19750
- if (override) return override.endsWith(".log") ? override : join6(override, name);
19751
- return join6(defaultLogRoot(stateDir, home), name);
19752
- }
19753
- function latestLogPath(dir) {
19754
- return join6(dir, "latest.log");
19755
- }
19756
- function linkLatest(dir, target2) {
19757
- const link = latestLogPath(dir);
19758
- try {
19759
- rmSync2(link, { force: true });
19760
- symlinkSync(target2, link);
19761
- } catch {
19762
- try {
19763
- writeFileSync4(link, `${target2}
19764
- `, { mode: 384 });
19765
- } catch {
19766
- }
19767
- }
19768
- }
19769
- function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Date.now()) {
19770
- let entries;
19771
- try {
19772
- entries = readdirSync(dir).filter((name) => name.endsWith(".log") && name !== "latest.log");
19773
- } catch {
19774
- return;
19775
- }
19776
- const dated = entries.map((name) => {
19777
- try {
19778
- return { name, at: statSync2(join6(dir, name)).mtimeMs };
19779
- } catch {
19780
- return { name, at: 0 };
19781
- }
19782
- }).sort((a, b) => b.at - a.at);
19783
- const cutoff = now - days * 24 * 60 * 60 * 1e3;
19784
- const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
19785
- for (const entry of stale) {
19786
- try {
19787
- rmSync2(join6(dir, entry.name), { force: true });
19788
- } catch {
19789
- }
19790
- }
19791
- }
19792
- function formatDiskLine(record2) {
19793
- const at = new Date(record2.at);
19794
- const pad = (value, width = 2) => String(value).padStart(width, "0");
19795
- const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}.${pad(at.getMilliseconds(), 3)}`;
19796
- return `${clock} ${record2.level.toUpperCase().padEnd(5)} ${record2.scope.padEnd(12)} ${record2.message}
19797
- `;
19798
- }
19799
- function openLogFile(path) {
19800
- try {
19801
- mkdirSync4(join6(path, ".."), { recursive: true, mode: 448 });
19802
- return openSync(path, "a", 384);
19803
- } catch {
19804
- return null;
19805
- }
19806
- }
19807
- function createLogger(options2 = {}) {
19808
- const path = options2.file === null ? null : options2.file ?? runLogPath(options2.stateDir);
19809
- const level = options2.level ?? "debug";
19810
- const now = options2.now ?? Date.now;
19811
- const threshold = LEVEL_ORDER[level];
19812
- const maxBytes = options2.maxBytes ?? MAX_LOG_BYTES;
19813
- let fd = path === null ? null : openLogFile(path);
19814
- if (fd !== null && path !== null && options2.owner !== false) {
19815
- const dir = join6(path, "..");
19816
- pruneRunLogs(dir);
19817
- linkLatest(dir, path);
19818
- }
19819
- let written = 0;
19820
- const listeners = /* @__PURE__ */ new Set();
19821
- const emit = (recordLevel, scope, message) => {
19822
- if (LEVEL_ORDER[recordLevel] < threshold) return;
19823
- const record2 = { at: now(), level: recordLevel, scope, message };
19824
- if (fd !== null) {
19825
- try {
19826
- const line = formatDiskLine(record2);
19827
- if (written + line.length > maxBytes) {
19828
- writeSync(fd, `--- log truncated at ${maxBytes} bytes ---
19829
- `);
19830
- fd = null;
19831
- } else {
19832
- writeSync(fd, line);
19833
- written += line.length;
19834
- }
19835
- } catch {
19836
- fd = null;
19837
- }
19838
- }
19839
- for (const listener of listeners) {
19840
- try {
19841
- listener(record2);
19842
- } catch {
19843
- }
19844
- }
19845
- };
19846
- return {
19847
- debug: (scope, message) => emit("debug", scope, message),
19848
- info: (scope, message) => emit("info", scope, message),
19849
- warn: (scope, message) => emit("warn", scope, message),
19850
- error: (scope, message) => emit("error", scope, message),
19851
- get file() {
19852
- return fd === null ? null : path;
19853
- },
19854
- onRecord(listener) {
19855
- listeners.add(listener);
19856
- return () => listeners.delete(listener);
19857
- },
19858
- close() {
19859
- listeners.clear();
19860
- fd = null;
19861
- }
19862
- };
19863
- }
19864
- function setAmbientLogger(logger2) {
19865
- ambient = logger2;
19866
- return logger2;
19867
- }
19868
- function formatFeedLine(record2) {
19869
- return formatDiskLine(record2).trimEnd();
19870
- }
19871
- function attachTerminalMirror(logger2, enabled = process.env.FRIZZ_DEBUG === "1") {
19872
- if (!enabled) return () => {
19873
- };
19874
- return logger2.onRecord((record2) => process.stderr.write(`${formatFeedLine(record2)}
19875
- `));
19876
- }
19877
- function ambientLogger() {
19878
- if (ambient) return ambient;
19879
- const inherited = process.env[LOG_FILE_ENV] ?? process.env[LOG_PATH_ENV];
19880
- const level = process.env[LOG_LEVEL_ENV];
19881
- const logger2 = createLogger({
19882
- ...inherited ? { file: inherited, owner: false } : { file: null },
19883
- ...level && level in LEVEL_ORDER ? { level } : {}
19884
- });
19885
- attachTerminalMirror(logger2);
19886
- ambient = logger2;
19887
- return ambient;
19888
- }
19889
- function logEnvironment(logger2, level = "debug") {
19890
- return logger2.file ? { [LOG_FILE_ENV]: logger2.file, [LOG_LEVEL_ENV]: level } : {};
19891
- }
19892
- var LEVEL_ORDER, LOG_FILE_ENV, LOG_LEVEL_ENV, LOG_PATH_ENV, RETAINED_RUNS, RETAINED_DAYS, MAX_LOG_BYTES, ambient, log;
19893
- var init_logging = __esm({
19894
- "packages/server/src/logging.ts"() {
19895
- "use strict";
19896
- init_frizz_paths();
19897
- LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
19898
- LOG_FILE_ENV = "FRIZZ_LOG_FILE";
19899
- LOG_LEVEL_ENV = "FRIZZ_LOG_LEVEL";
19900
- LOG_PATH_ENV = "FRIZZ_LOG_PATH";
19901
- RETAINED_RUNS = 20;
19902
- RETAINED_DAYS = 14;
19903
- MAX_LOG_BYTES = 32 * 1024 * 1024;
19904
- ambient = null;
19905
- log = {
19906
- debug: (scope, message) => ambientLogger().debug(scope, message),
19907
- info: (scope, message) => ambientLogger().info(scope, message),
19908
- warn: (scope, message) => ambientLogger().warn(scope, message),
19909
- error: (scope, message) => ambientLogger().error(scope, message)
19910
- };
19911
- }
19912
- });
19913
-
19914
19977
  // packages/server/src/process-generation.ts
19915
19978
  import { execFileSync } from "node:child_process";
19916
19979
  import { randomUUID } from "node:crypto";
@@ -20946,7 +21009,7 @@ var init_boot_progress = __esm({
20946
21009
 
20947
21010
  // packages/server/src/project-root.ts
20948
21011
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
20949
- import { closeSync as closeSync3, existsSync as existsSync4, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
21012
+ import { closeSync as closeSync3, existsSync as existsSync4, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync8 } from "node:fs";
20950
21013
  import { homedir as homedir8 } from "node:os";
20951
21014
  import { dirname as dirname8, join as join11, parse, resolve as resolve3 } from "node:path";
20952
21015
  function projectIdPath(root) {
@@ -21040,6 +21103,17 @@ function isHomeDirectory(dir, home = homedir8()) {
21040
21103
  return false;
21041
21104
  }
21042
21105
  }
21106
+ function isWorldWritable(dir, platform = process.platform) {
21107
+ if (platform === "win32") return false;
21108
+ try {
21109
+ return (statSync5(dir).mode & 2) !== 0;
21110
+ } catch {
21111
+ return false;
21112
+ }
21113
+ }
21114
+ function refuseWorldWritableProject(dir) {
21115
+ if (isWorldWritable(dir)) throw new Error(`A folder every account can write to cannot be a project: ${dir}`);
21116
+ }
21043
21117
  function existingProjectId(dir) {
21044
21118
  const fromFile = readProjectIdFile(dir);
21045
21119
  if (fromFile) return fromFile;
@@ -21115,11 +21189,13 @@ function readRegistry(home = homedir9()) {
21115
21189
  try {
21116
21190
  const parsed = JSON.parse(raw);
21117
21191
  if (parsed?.version !== 1 || !Array.isArray(parsed.projects)) return { ...EMPTY, projects: [] };
21192
+ const forgotten = Array.isArray(parsed.forgotten) ? parsed.forgotten.filter((id) => typeof id === "string") : [];
21118
21193
  return {
21119
21194
  version: 1,
21120
21195
  projects: parsed.projects.filter(
21121
21196
  (p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string"
21122
- )
21197
+ ),
21198
+ ...forgotten.length > 0 ? { forgotten } : {}
21123
21199
  };
21124
21200
  } catch {
21125
21201
  return { ...EMPTY, projects: [] };
@@ -21183,7 +21259,12 @@ function canonicalPath(dir) {
21183
21259
  }
21184
21260
  function registerProject(input, home = homedir9()) {
21185
21261
  input = { ...input, dir: canonicalPath(input.dir) };
21262
+ if (input.dir === canonicalPath(launchHostDir(home))) return { action: "unlisted" };
21186
21263
  const registry = readRegistry(home);
21264
+ if (registry.forgotten) {
21265
+ registry.forgotten = registry.forgotten.filter((id) => id !== input.id);
21266
+ if (registry.forgotten.length === 0) delete registry.forgotten;
21267
+ }
21187
21268
  const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
21188
21269
  const byId = registry.projects.find((p) => p.id === input.id);
21189
21270
  if (byId) {
@@ -23699,27 +23780,27 @@ function createAccessPane(options2) {
23699
23780
  }
23700
23781
 
23701
23782
  // src/remote-controller.ts
23702
- import { homedir as homedir5 } from "node:os";
23783
+ import { homedir as homedir6 } from "node:os";
23703
23784
 
23704
23785
  // src/cloud.ts
23705
23786
  init_frizz_paths();
23706
23787
  init_src();
23707
23788
  import { spawn as spawn2 } from "node:child_process";
23708
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, rmSync } from "node:fs";
23789
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "node:fs";
23709
23790
  import { createInterface } from "node:readline/promises";
23710
- import { homedir as homedir4 } from "node:os";
23711
- import { dirname as dirname4, join as join5 } from "node:path";
23791
+ import { homedir as homedir5 } from "node:os";
23792
+ import { dirname as dirname4, join as join6 } from "node:path";
23712
23793
 
23713
23794
  // src/identity.ts
23714
23795
  init_frizz_paths();
23715
23796
  init_src();
23716
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
23717
- import { homedir as homedir3 } from "node:os";
23718
- import { dirname as dirname3, join as join4 } from "node:path";
23719
- function claimIdentityPath(home = homedir3(), env) {
23720
- return join4(frizzPaths({ home, env }).state, "identity.key");
23797
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
23798
+ import { homedir as homedir4 } from "node:os";
23799
+ import { dirname as dirname3, join as join5 } from "node:path";
23800
+ function claimIdentityPath(home = homedir4(), env) {
23801
+ return join5(frizzPaths({ home, env }).state, "identity.key");
23721
23802
  }
23722
- async function loadOrCreateClaimIdentity(home = homedir3()) {
23803
+ async function loadOrCreateClaimIdentity(home = homedir4()) {
23723
23804
  const path = claimIdentityPath(home);
23724
23805
  let stored;
23725
23806
  try {
@@ -23735,8 +23816,8 @@ async function loadOrCreateClaimIdentity(home = homedir3()) {
23735
23816
  );
23736
23817
  }
23737
23818
  const identity = await generateClaimIdentity();
23738
- mkdirSync2(dirname3(path), { recursive: true });
23739
- writeFileSync2(path, await exportClaimPrivateKey(identity.privateKey), { mode: 384 });
23819
+ mkdirSync3(dirname3(path), { recursive: true });
23820
+ writeFileSync3(path, await exportClaimPrivateKey(identity.privateKey), { mode: 384 });
23740
23821
  return identity;
23741
23822
  }
23742
23823
 
@@ -24332,6 +24413,11 @@ function describeCloudConfig(config) {
24332
24413
  return "Cloudflare Tunnel";
24333
24414
  }
24334
24415
  var REGISTRAR_IS_LIVE = true;
24416
+ var CUSTOM_NAMES_OFFERED = false;
24417
+ function customNamesOffered(env = process.env) {
24418
+ return CUSTOM_NAMES_OFFERED || env.FRIZZ_CUSTOM_NAMES === "1";
24419
+ }
24420
+ var CUSTOM_NAMES_PAUSED = "custom frizz.sh names are paused for now \u2014 press enter for a private name (no account needed), or answer with the hostname of a tunnel you run";
24335
24421
  function isClaimedConfig(config) {
24336
24422
  return typeof config.claim === "string" && config.claim.length > 0;
24337
24423
  }
@@ -24345,19 +24431,24 @@ function zoneClaimLabel(hostname) {
24345
24431
  const label = hostname.slice(0, -suffix.length);
24346
24432
  return label.length > 0 && !label.includes(".") ? label : null;
24347
24433
  }
24348
- async function reconcileCloudConfig(config, port, home = homedir4(), onNotice, origin, github = githubDeviceFlow(onNotice ? { onPrompt: (prompt) => onNotice(deviceCodeMessage(prompt)) } : {})) {
24434
+ async function reconcileCloudConfig(config, port, home = homedir5(), onNotice, origin, github = githubDeviceFlow(onNotice ? { onPrompt: (prompt) => onNotice(deviceCodeMessage(prompt)) } : {})) {
24349
24435
  if (isRelayConfig(config)) return config;
24350
24436
  const label = config.claim ?? zoneClaimLabel(config.hostname);
24351
24437
  if (!label) return config;
24438
+ if (!isAnonymousClaimName(label) && !customNamesOffered()) {
24439
+ throw new Error(
24440
+ `${config.hostname} needs moving onto the Frizz relay, which means claiming it again, and custom frizz.sh names are paused for now \u2014 press R to pick a private name or another setup`
24441
+ );
24442
+ }
24352
24443
  onNotice?.(
24353
24444
  isClaimedConfig(config) ? `${config.hostname} is served by the Frizz relay now; moving this board off its tunnel` : `${config.hostname} is inside ${CLAIM_ZONE}, where names are claimed, not tunnelled; claiming ${label}`
24354
24445
  );
24355
24446
  return establishCloudConfig(label, port, home, origin, github);
24356
24447
  }
24357
- function cloudConfigPath(home = homedir4()) {
24358
- return join5(frizzPaths({ home }).data, "cloud.json");
24448
+ function cloudConfigPath(home = homedir5()) {
24449
+ return join6(frizzPaths({ home }).data, "cloud.json");
24359
24450
  }
24360
- function readCloudConfig(home = homedir4()) {
24451
+ function readCloudConfig(home = homedir5()) {
24361
24452
  try {
24362
24453
  const parsed = JSON.parse(readFileSync3(cloudConfigPath(home), "utf8"));
24363
24454
  if (typeof parsed.hostname !== "string" || !parsed.hostname) return null;
@@ -24377,10 +24468,10 @@ function readCloudConfig(home = homedir4()) {
24377
24468
  return null;
24378
24469
  }
24379
24470
  }
24380
- function tunnelTokenPath(home = homedir4()) {
24381
- return join5(frizzPaths({ home }).state, "tunnel-token");
24471
+ function tunnelTokenPath(home = homedir5()) {
24472
+ return join6(frizzPaths({ home }).state, "tunnel-token");
24382
24473
  }
24383
- function readTunnelToken(home = homedir4()) {
24474
+ function readTunnelToken(home = homedir5()) {
24384
24475
  try {
24385
24476
  const token = readFileSync3(tunnelTokenPath(home), "utf8").trim();
24386
24477
  return token.length > 0 ? token : null;
@@ -24388,19 +24479,19 @@ function readTunnelToken(home = homedir4()) {
24388
24479
  return null;
24389
24480
  }
24390
24481
  }
24391
- function writeTunnelToken(token, home = homedir4()) {
24482
+ function writeTunnelToken(token, home = homedir5()) {
24392
24483
  const path = tunnelTokenPath(home);
24393
- mkdirSync3(dirname4(path), { recursive: true });
24394
- writeFileSync3(path, `${token}
24484
+ mkdirSync4(dirname4(path), { recursive: true });
24485
+ writeFileSync4(path, `${token}
24395
24486
  `, { mode: 384 });
24396
24487
  }
24397
- function deleteCloudConfig(home = homedir4()) {
24398
- rmSync(cloudConfigPath(home), { force: true });
24488
+ function deleteCloudConfig(home = homedir5()) {
24489
+ rmSync2(cloudConfigPath(home), { force: true });
24399
24490
  }
24400
- function writeCloudConfig(config, home = homedir4()) {
24491
+ function writeCloudConfig(config, home = homedir5()) {
24401
24492
  const path = cloudConfigPath(home);
24402
- mkdirSync3(dirname4(path), { recursive: true });
24403
- writeFileSync3(path, `${JSON.stringify(config, null, 2)}
24493
+ mkdirSync4(dirname4(path), { recursive: true });
24494
+ writeFileSync4(path, `${JSON.stringify(config, null, 2)}
24404
24495
  `);
24405
24496
  }
24406
24497
  function normalizeHostname(raw) {
@@ -24420,7 +24511,7 @@ async function promptForTunnelName(hostname, fallback) {
24420
24511
  rl.close();
24421
24512
  }
24422
24513
  }
24423
- async function establishCloudConfig(answer, port, home = homedir4(), origin, github = githubDeviceFlow()) {
24514
+ async function establishCloudConfig(answer, port, home = homedir5(), origin, github = githubDeviceFlow()) {
24424
24515
  if (answer.includes(".")) {
24425
24516
  const hostname = normalizeHostname(answer);
24426
24517
  const tunnel = await promptForTunnelName(hostname, hostname.split(".")[0]);
@@ -24432,6 +24523,7 @@ async function establishCloudConfig(answer, port, home = homedir4(), origin, git
24432
24523
  );
24433
24524
  }
24434
24525
  const anonymous = answer === "" || isAnonymousClaimName(answer);
24526
+ if (!anonymous && !customNamesOffered()) throw new Error(CUSTOM_NAMES_PAUSED);
24435
24527
  if (!anonymous && !claimNameIsValid(answer)) {
24436
24528
  normalizeClaimName(answer);
24437
24529
  }
@@ -24464,7 +24556,7 @@ async function establishCloudConfig(answer, port, home = homedir4(), origin, git
24464
24556
  }
24465
24557
  return { hostname: result.hostname, claim: name, serve: "relay" };
24466
24558
  }
24467
- async function resolveRunToken(config, port, home = homedir4(), onWarning, origin) {
24559
+ async function resolveRunToken(config, port, home = homedir5(), onWarning, origin) {
24468
24560
  if (!isClaimedConfig(config)) return null;
24469
24561
  const cached2 = readTunnelToken(home);
24470
24562
  try {
@@ -24481,12 +24573,12 @@ async function resolveRunToken(config, port, home = homedir4(), onWarning, origi
24481
24573
  throw error;
24482
24574
  }
24483
24575
  }
24484
- function resolveTunnelConfigPath(config, home = homedir4()) {
24576
+ function resolveTunnelConfigPath(config, home = homedir5()) {
24485
24577
  if (config.config) return config.config;
24486
- const candidate = join5(home, ".cloudflared", "frizz.yml");
24578
+ const candidate = join6(home, ".cloudflared", "frizz.yml");
24487
24579
  return existsSync3(candidate) ? candidate : null;
24488
24580
  }
24489
- async function startRelay(config, port, home = homedir4(), onStatus) {
24581
+ async function startRelay(config, port, home = homedir5(), onStatus) {
24490
24582
  const identity = await loadOrCreateClaimIdentity(home);
24491
24583
  const origin = `https://${config.hostname}`;
24492
24584
  return connectRelay({
@@ -24504,7 +24596,7 @@ async function startRelay(config, port, home = homedir4(), onStatus) {
24504
24596
  } : {}
24505
24597
  });
24506
24598
  }
24507
- function startTunnel(config, onExit, onError, home = homedir4(), runToken) {
24599
+ function startTunnel(config, onExit, onError, home = homedir5(), runToken) {
24508
24600
  const configPath = runToken ? null : resolveTunnelConfigPath(config, home);
24509
24601
  const args2 = runToken ? ["tunnel", "--no-autoupdate", "run", "--token", runToken] : [
24510
24602
  "tunnel",
@@ -24535,7 +24627,7 @@ function startTunnel(config, onExit, onError, home = homedir4(), runToken) {
24535
24627
 
24536
24628
  // src/remote-controller.ts
24537
24629
  function createRemoteController(options2) {
24538
- const home = options2.home ?? homedir5();
24630
+ const home = options2.home ?? homedir6();
24539
24631
  let config = null;
24540
24632
  let transport = null;
24541
24633
  const stopTransport = () => {
@@ -24650,6 +24742,15 @@ var CHOICES = [
24650
24742
  { kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
24651
24743
  { kind: "off", title: "Off", blurb: "loopback only" }
24652
24744
  ];
24745
+ function choicesFor(current) {
24746
+ if (customNamesOffered()) return CHOICES;
24747
+ if (kindOf(current) === "frizz") {
24748
+ return CHOICES.map(
24749
+ (choice) => choice.kind === "frizz" ? { ...choice, blurb: `${current.hostname} \u2014 kept and renewed; new custom names are paused` } : choice
24750
+ );
24751
+ }
24752
+ return CHOICES.filter((choice) => choice.kind !== "frizz");
24753
+ }
24653
24754
  var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
24654
24755
  var BACKSPACE = /* @__PURE__ */ new Set(["\x7F", "\b"]);
24655
24756
  var ESC = "\x1B";
@@ -24690,6 +24791,8 @@ function createRemotePane(options2) {
24690
24791
  output.write(fitScreen([{ text: "", drop: 0 }, ...indented], output.rows, output.columns));
24691
24792
  };
24692
24793
  const check = (ok, text) => ok ? `${text} \u2713` : text;
24794
+ const choices = () => choicesFor(options2.current());
24795
+ const rowOf = (kind) => Math.max(0, choices().findIndex((choice) => choice.kind === kind));
24693
24796
  const paint = () => {
24694
24797
  if (!open) return;
24695
24798
  const s = screen;
@@ -24702,7 +24805,7 @@ function createRemotePane(options2) {
24702
24805
  "Frizz binds 127.0.0.1 and has no login. Reaching it from another device means something in front of it does the authenticating. Whatever you pick here is remembered; from then on a plain `npx frizz` serves it."
24703
24806
  ),
24704
24807
  "",
24705
- ...CHOICES.map((choice, index) => {
24808
+ ...choices().map((choice, index) => {
24706
24809
  const marker = index === s.index ? "\u276F" : " ";
24707
24810
  const title = choice.title.padEnd(18);
24708
24811
  const now = choice.kind === current ? ` ${DIM}(current)${RESET2}` : "";
@@ -24911,6 +25014,21 @@ function createRemotePane(options2) {
24911
25014
  }
24912
25015
  paint();
24913
25016
  };
25017
+ const serveCurrent = async () => {
25018
+ const current = options2.current();
25019
+ const back = { name: "menu", index: rowOf("frizz") };
25020
+ if (!current) return;
25021
+ screen = { name: "busy", message: `serving ${current.hostname}\u2026` };
25022
+ paint();
25023
+ try {
25024
+ await options2.apply(current);
25025
+ options2.onChanged?.(current);
25026
+ screen = { name: "done", message: `Serving https://${current.hostname} (${describeCloudConfig(current)}).`, link: options2.issueLink(), config: current };
25027
+ } catch (error) {
25028
+ screen = { name: "error", message: error instanceof Error ? error.message : String(error), back };
25029
+ }
25030
+ paint();
25031
+ };
24914
25032
  const turnOff = async () => {
24915
25033
  screen = { name: "busy", message: "back to loopback only\u2026" };
24916
25034
  paint();
@@ -24919,15 +25037,14 @@ function createRemotePane(options2) {
24919
25037
  options2.onChanged?.(null);
24920
25038
  screen = { name: "done", message: "Loopback only. This board is reachable from this machine alone.", link: null, config: null };
24921
25039
  } catch (error) {
24922
- screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index: CHOICES.length - 1 } };
25040
+ screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index: rowOf("off") } };
24923
25041
  }
24924
25042
  paint();
24925
25043
  };
24926
25044
  return {
24927
25045
  open() {
24928
25046
  open = true;
24929
- const current = kindOf(options2.current());
24930
- screen = { name: "menu", index: Math.max(0, CHOICES.findIndex((choice) => choice.kind === current)) };
25047
+ screen = { name: "menu", index: rowOf(kindOf(options2.current())) };
24931
25048
  output.write(ALT_SCREEN_ON);
24932
25049
  output.write(HIDE_CURSOR);
24933
25050
  paint();
@@ -24950,13 +25067,15 @@ function createRemotePane(options2) {
24950
25067
  }
24951
25068
  if (s.name === "menu") {
24952
25069
  if (key === ESC || key === "q") return "close";
24953
- if (key === UP || key === "k") s.index = (s.index + CHOICES.length - 1) % CHOICES.length;
24954
- else if (key === DOWN || key === "j") s.index = (s.index + 1) % CHOICES.length;
24955
- else if (/^[1-6]$/.test(key)) s.index = Number(key) - 1;
25070
+ const rows = choices();
25071
+ if (key === UP || key === "k") s.index = (s.index + rows.length - 1) % rows.length;
25072
+ else if (key === DOWN || key === "j") s.index = (s.index + 1) % rows.length;
25073
+ else if (/^[1-9]$/.test(key) && Number(key) <= rows.length) s.index = Number(key) - 1;
24956
25074
  else if (ENTER.has(key)) {
24957
- const choice = CHOICES[s.index];
25075
+ const choice = rows[Math.min(s.index, rows.length - 1)];
24958
25076
  if (choice.kind === "off") void turnOff();
24959
25077
  else if (choice.kind === "private") void claimPrivate();
25078
+ else if (choice.kind === "frizz" && !customNamesOffered()) void serveCurrent();
24960
25079
  else openForm(choice.kind);
24961
25080
  return "keep";
24962
25081
  }
@@ -24964,7 +25083,7 @@ function createRemotePane(options2) {
24964
25083
  return "keep";
24965
25084
  }
24966
25085
  if (key === ESC) {
24967
- screen = { name: "menu", index: CHOICES.findIndex((choice) => choice.kind === s.kind) };
25086
+ screen = { name: "menu", index: rowOf(s.kind) };
24968
25087
  paint();
24969
25088
  return "keep";
24970
25089
  }
@@ -25339,6 +25458,7 @@ init_boot_progress();
25339
25458
  init_frizz_paths();
25340
25459
  import { execFileSync as execFileSync3 } from "node:child_process";
25341
25460
  import { createServer as createServer2 } from "node:net";
25461
+ import { randomUUID as randomUUID5 } from "node:crypto";
25342
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";
25343
25463
  import { homedir as homedir10, tmpdir as tmpdir2 } from "node:os";
25344
25464
  import { basename as basename5, dirname as dirname10, join as join13, resolve as resolve4 } from "node:path";
@@ -25352,7 +25472,7 @@ var LAUNCH_TIMEOUT_MS = BOOT_STALL_TIMEOUT_MS;
25352
25472
  var LAUNCH_HARD_TIMEOUT_MS = BOOT_HARD_TIMEOUT_MS;
25353
25473
  var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
25354
25474
  function retiredFlagMessage(flag) {
25355
- return `${flag} was retired \u2014 how a board is reached is set up from the running board now: press R in its terminal to pick a private or custom frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
25475
+ return `${flag} was retired \u2014 how a board is reached is set up from the running board now: press R in its terminal to pick a private frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
25356
25476
  }
25357
25477
  function parseCliArgs(argv) {
25358
25478
  const args2 = new Set(argv);
@@ -25478,6 +25598,7 @@ function prepareSandbox(env = process.env, realHome = homedir10()) {
25478
25598
  } catch {
25479
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");
25480
25600
  }
25601
+ writeProjectIdFile(project, randomUUID5());
25481
25602
  process.chdir(project);
25482
25603
  return { home, project };
25483
25604
  }
@@ -25521,16 +25642,23 @@ function cleanupSandbox(home) {
25521
25642
  function resolveLaunchIntent(cwd = process.cwd(), home = homedir10(), env = process.env) {
25522
25643
  const candidate = realpathSync5(discoverProjectRoot(cwd, home));
25523
25644
  const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
25524
- if ((known || hasProjectMarker(candidate)) && !isHomeDirectory(candidate, home))
25525
- return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
25526
- const host = mostRecentProject(home, env);
25527
- const reason = isHomeDirectory(candidate, home) ? "home" : "unadopted";
25528
- if (!host) return { kind: "empty", directory: candidate, reason };
25529
- return reason === "home" ? { 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" };
25530
25658
  }
25531
25659
  function mostRecentProject(home, env) {
25532
25660
  for (const entry of listProjects(home)) {
25533
- if (entry.stale || isHomeDirectory(entry.path, home)) continue;
25661
+ if (entry.stale || isHomeDirectory(entry.path, home) || isWorldWritable(entry.path)) continue;
25534
25662
  try {
25535
25663
  return resolveWorkspace(entry.path, home, env);
25536
25664
  } catch {
@@ -25553,6 +25681,7 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process
25553
25681
  gitRoot = void 0;
25554
25682
  }
25555
25683
  const root0 = realpathSync5(gitRoot ?? discoverProjectRoot(cwd, home));
25684
+ refuseWorldWritableProject(root0);
25556
25685
  const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
25557
25686
  const root = identity?.root ?? root0;
25558
25687
  const id = ensureProjectIdFile(root, home, identity?.id);
@@ -25882,7 +26011,7 @@ init_restart_supervisor();
25882
26011
  init_logging();
25883
26012
  init_boot_progress();
25884
26013
  import { fork } from "node:child_process";
25885
- import { randomUUID as randomUUID5 } from "node:crypto";
26014
+ import { randomUUID as randomUUID6 } from "node:crypto";
25886
26015
  import { existsSync as existsSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync11 } from "node:fs";
25887
26016
  import { createServer as createNetServer } from "node:net";
25888
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";
@@ -26127,7 +26256,7 @@ var Supervisor = class {
26127
26256
  watchSubscribe;
26128
26257
  reexec;
26129
26258
  supervisorLock;
26130
- statusPublisherToken = randomUUID5();
26259
+ statusPublisherToken = randomUUID6();
26131
26260
  processGeneration = currentProcessGeneration();
26132
26261
  ownerToken;
26133
26262
  launchTarget;
@@ -27083,7 +27212,7 @@ var npmRegistryReleaseAdapter = createNpmRegistryReleaseAdapter();
27083
27212
  // src/server-release.ts
27084
27213
  init_frizz_paths();
27085
27214
  import { execFile as execFile4 } from "node:child_process";
27086
- import { createHash as createHash4, randomUUID as randomUUID6 } from "node:crypto";
27215
+ import { createHash as createHash4, randomUUID as randomUUID7 } from "node:crypto";
27087
27216
  import {
27088
27217
  closeSync as closeSync5,
27089
27218
  existsSync as existsSync9,
@@ -27094,7 +27223,7 @@ import {
27094
27223
  realpathSync as realpathSync6,
27095
27224
  renameSync as renameSync6,
27096
27225
  rmSync as rmSync9,
27097
- statSync as statSync5,
27226
+ statSync as statSync6,
27098
27227
  writeFileSync as writeFileSync11
27099
27228
  } from "node:fs";
27100
27229
  import { delimiter as delimiter2, dirname as dirname13, isAbsolute as isAbsolute6, join as join15, relative as relative3, resolve as resolve6, sep as sep4 } from "node:path";
@@ -27138,7 +27267,7 @@ function containedFile(root, name, directory = false) {
27138
27267
  const rel = relative3(realpathSync6(root), path);
27139
27268
  if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute6(rel))
27140
27269
  throw new Error(`Frizz server artifact escapes its package: ${name}`);
27141
- const stat = statSync5(path);
27270
+ const stat = statSync6(path);
27142
27271
  if (directory ? !stat.isDirectory() : !stat.isFile()) throw new Error(`invalid Frizz server artifact: ${name}`);
27143
27272
  return path;
27144
27273
  }
@@ -27181,7 +27310,7 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
27181
27310
  } catch {
27182
27311
  }
27183
27312
  }
27184
- const found = candidates.find((path) => existsSync9(path) && statSync5(path).isFile());
27313
+ const found = candidates.find((path) => existsSync9(path) && statSync6(path).isFile());
27185
27314
  if (!found) throw new Error("npm was not found beside Node or on PATH; install Node with npm to install Frizz server releases");
27186
27315
  return resolve6(found);
27187
27316
  }
@@ -27239,7 +27368,7 @@ function npmServerPackageInstaller(env = process.env) {
27239
27368
  }
27240
27369
  function atomicJson2(path, value) {
27241
27370
  mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
27242
- const temp = `${path}.${randomUUID6()}.tmp`;
27371
+ const temp = `${path}.${randomUUID7()}.tmp`;
27243
27372
  try {
27244
27373
  const fd = openSync6(temp, "wx", 384);
27245
27374
  try {
@@ -27340,7 +27469,7 @@ var ServerReleaseStore = class {
27340
27469
  const spec = { ...this.spec, version };
27341
27470
  assertSpec(spec, this.compatibility);
27342
27471
  mkdirSync12(this.generations, { recursive: true, mode: 448 });
27343
- const id = randomUUID6();
27472
+ const id = randomUUID7();
27344
27473
  const staging = join15(this.generations, `${id}.staging`);
27345
27474
  const destination = join15(this.generations, id);
27346
27475
  mkdirSync12(staging, { mode: 448 });
@@ -27366,7 +27495,7 @@ var ServerReleaseStore = class {
27366
27495
  // src/server-owner.ts
27367
27496
  init_frizz_paths();
27368
27497
  init_project_launch();
27369
- import { randomUUID as randomUUID7 } from "node:crypto";
27498
+ import { randomUUID as randomUUID8 } from "node:crypto";
27370
27499
  import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, realpathSync as realpathSync7 } from "node:fs";
27371
27500
  import { join as join16 } from "node:path";
27372
27501
  var STABLE_SERVER_OWNER_NAME = "frizz-server";
@@ -27411,7 +27540,7 @@ function wrapLease(lease) {
27411
27540
  token: lease.token,
27412
27541
  pid: lease.pid,
27413
27542
  processStart: lease.processStart,
27414
- publisherToken: randomUUID7()
27543
+ publisherToken: randomUUID8()
27415
27544
  };
27416
27545
  }
27417
27546
  function acquireStableServerOwner(roots) {
@@ -27542,10 +27671,11 @@ if (options.help) {
27542
27671
 
27543
27672
  Usage: npx ${PACKAGE_NAME} [options]
27544
27673
 
27545
- Run it in the directory you want to work in. One server serves EVERY project on this machine,
27546
- each at its own /project/<name> URL, so a second run joins the one already going. Runs the
27547
- npm-resolved immutable Frizz package, then opens it in your default browser. Use frizz-dev only
27548
- 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.
27549
27679
 
27550
27680
  Options:
27551
27681
  --no-app print the URL without opening a browser
@@ -27563,7 +27693,7 @@ Options:
27563
27693
 
27564
27694
 
27565
27695
  To reach the board from a phone or another machine, press R in the terminal running it: a short
27566
- walkthrough sets up a private frizz.sh name (no account needed), a custom one, a Cloudflare
27696
+ walkthrough sets up a private frizz.sh name (no account needed), a Cloudflare
27567
27697
  Tunnel, Tailscale, or a proxy of your own, and
27568
27698
  remembers the choice, so a plain launch serves it from then on. The board stays on loopback and
27569
27699
  shows a single-use sign-in link as a QR; press L for a fresh one, or run --link from another shell.`
@@ -27596,10 +27726,6 @@ var workspace = (() => {
27596
27726
  return workspaceFromLaunchTarget(pinned);
27597
27727
  }
27598
27728
  const intent = resolveLaunchIntent();
27599
- if (intent.kind === "empty")
27600
- throw new Error(
27601
- intent.reason === "home" ? "frizz cannot open your home directory as a project, and there is no other project to show yet. cd into a repository and run frizz there." : `${intent.directory} is not a Frizz project yet, and there is no other project to show. Run frizz inside a repository, or add this one from the projects page once a board is open.`
27602
- );
27603
27729
  launchIntent = intent;
27604
27730
  return intent.workspace;
27605
27731
  } catch (error) {
@@ -27745,7 +27871,7 @@ async function openOrPrint(port, reused, path = "") {
27745
27871
  if (sandbox) warnings.push(`Sandbox: everything here is throwaway (${sandbox.home}) and is deleted when this terminal closes.`);
27746
27872
  if (sessionNotice && !reused) warnings.push(sessionNotice);
27747
27873
  if (!readout) {
27748
- console.log(`${reused ? "reusing" : "started"} Frizz for ${workspace.root}`);
27874
+ console.log(`${reused ? "reusing" : "started"} Frizz${launchIntent && launchIntent.kind !== "open" ? "" : ` for ${workspace.root}`}`);
27749
27875
  console.log(url);
27750
27876
  if (publicOrigin) console.log(activeAccessLink?.url ?? `${publicOrigin}/`);
27751
27877
  for (const warning of warnings) console.log(warning);
@@ -27756,7 +27882,9 @@ async function openOrPrint(port, reused, path = "") {
27756
27882
  [
27757
27883
  { label: "Local", value: boardAddress(url), accent: true },
27758
27884
  ...publicOrigin ? [{ label: "Public", value: activeAccessLink?.url ?? `${publicOrigin}/`, accent: true }] : [],
27759
- { 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)}` }],
27760
27888
  ...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
27761
27889
  ],
27762
27890
  reused ? (