frizz 0.13.3 → 0.13.6

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 (2) hide show
  1. package/dist/frizz.js +146 -27
  2. package/package.json +3 -2
package/dist/frizz.js CHANGED
@@ -12493,7 +12493,7 @@ function requireEffortOutsideAcp(profile, ctx) {
12493
12493
  function fallbackPort(base) {
12494
12494
  return base + 1e4;
12495
12495
  }
12496
- 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, 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, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, 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;
12496
+ 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, 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, 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;
12497
12497
  var init_src = __esm({
12498
12498
  "packages/shared/src/index.ts"() {
12499
12499
  "use strict";
@@ -12907,12 +12907,12 @@ var init_src = __esm({
12907
12907
  " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
12908
12908
  " **bolded verb phrase**.",
12909
12909
  "- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. FRONTMATTER, THEN MARKDOWN:",
12910
- " one structural line per thing you are waiting on, a REQUIRED `for:` duration, then a `---` line and",
12910
+ " one YAML list per kind of thing you are waiting on, a REQUIRED `for:` duration, then a `---` line and",
12911
12911
  " as much prose as you want. The prose is OPTIONAL; the lines above it are not.",
12912
12912
  "",
12913
12913
  " ```awaiting",
12914
- " shell: <the id your runtime gave you>",
12915
- " pr: owner/repo#123",
12914
+ " shells: [<the id your runtime gave you>]",
12915
+ " prs: [owner/repo#123]",
12916
12916
  " for: 2h",
12917
12917
  " ---",
12918
12918
  " What you are waiting for, in your own words \u2014 this is what the human reads on your card.",
@@ -13230,6 +13230,17 @@ var init_src = __esm({
13230
13230
  dismissed: external_exports.array(external_exports.string()),
13231
13231
  open: external_exports.array(RegisteredQuestionView)
13232
13232
  }).strict();
13233
+ SettledQuestionView = external_exports.object({
13234
+ id: external_exports.string(),
13235
+ spec: AskedQuestionSchema,
13236
+ askedAt: external_exports.string(),
13237
+ /** When the human sent the answer — what decides which rest the card stood at when it was answered. */
13238
+ settledAt: external_exports.string(),
13239
+ answer: QuestionAnswerSchema
13240
+ }).strict();
13241
+ ThreadSettledQuestionsResult = external_exports.object({
13242
+ questions: external_exports.array(SettledQuestionView)
13243
+ }).strict();
13233
13244
  MarkOwnDoneInput = external_exports.object({
13234
13245
  slug: ThreadSlug,
13235
13246
  body: external_exports.string().trim().min(1).max(2e4)
@@ -13296,6 +13307,11 @@ var init_src = __esm({
13296
13307
  UpsertOwnLinkResult = external_exports.object({ link: ThreadLinkView }).strict();
13297
13308
  DropOwnLinkInput = external_exports.object({ slug: ThreadSlug, id: external_exports.string().min(1) }).strict();
13298
13309
  DropOwnLinkResult = external_exports.object({ dropped: external_exports.boolean() }).strict();
13310
+ LiveTool = external_exports.object({
13311
+ name: external_exports.string(),
13312
+ desc: external_exports.string().optional(),
13313
+ detail: external_exports.string().optional()
13314
+ });
13299
13315
  ThreadView = external_exports.object({
13300
13316
  id: ThreadSlug,
13301
13317
  // slug; filename is <slug>.md
@@ -13334,6 +13350,15 @@ var init_src = __esm({
13334
13350
  // user hid the row from the nav; respawn/resume un-archives
13335
13351
  lastAssistant: external_exports.string().optional(),
13336
13352
  // trimmed preview of last assistant text
13353
+ // The FIRST non-empty line of that same text, markdown intact and newlines honoured (capped) — the
13354
+ // handoff's verdict line ("**Fixed** — …"), which `lastAssistant` cannot give back because its preview
13355
+ // collapses every newline to a space. The phone board's rested row reads it. Optional so old
13356
+ // snapshots parse.
13357
+ lastAssistantLine: external_exports.string().optional(),
13358
+ // The newest tool call the agent has issued and not yet had a result for (Claude session threads),
13359
+ // in the shape `toolActivityLabel` reads, so a list row can say "Running the focused tests" with the
13360
+ // gerund the chat's working indicator shows. Absent between calls, at rest, and for other backends.
13361
+ liveTool: LiveTool.optional(),
13337
13362
  spawnedAt: external_exports.string().optional(),
13338
13363
  // ISO8601
13339
13364
  lastActivityAt: external_exports.string().optional(),
@@ -13392,6 +13417,11 @@ var init_src = __esm({
13392
13417
  * same card and the swap is invisible. Absent whenever there is nothing in flight, which is almost
13393
13418
  * always — it exists for the seconds between the human sending and the worker being handed it. */
13394
13419
  answersInFlight: external_exports.string().optional(),
13420
+ /** The human's message — a follow-up or a registered answer — is on its way to the worker and the turn
13421
+ * has not started yet (board.deriveDeliveryInFlight). The queue has already let the thread go; this
13422
+ * tells the rail to draw it as working in every tab, not only the one that sent it (web
13423
+ * lib/steering.ts). Absent otherwise. */
13424
+ deliveryInFlight: external_exports.boolean().optional(),
13395
13425
  // ISO8601 of the newest REAL user interaction (answer/steer/dispatch) — the chronological listing
13396
13426
  // sort key. Optional; the listing falls back to spawnedAt when absent (a dispatch IS an interaction).
13397
13427
  lastUserAt: external_exports.string().optional(),
@@ -13629,13 +13659,6 @@ var init_src = __esm({
13629
13659
  activeThreads: external_exports.number().int().positive().optional(),
13630
13660
  detail: external_exports.string().max(200).optional()
13631
13661
  });
13632
- AccountLoginStartInput = external_exports.object({ backend: AccountBackend }).strict();
13633
- AccountLoginStartResult = external_exports.object({ attemptId: ThreadSlug });
13634
- AccountLoginStatusInput = external_exports.object({ attemptId: ThreadSlug }).strict();
13635
- AccountLoginStatusResult = external_exports.object({
13636
- state: external_exports.enum(["running", "exited", "unknown"]),
13637
- auth: ProviderAuth
13638
- });
13639
13662
  PermissionMode = external_exports.enum(["auto", "default", "acceptEdits", "plan", "bypassPermissions"]);
13640
13663
  LocalFileOpener = external_exports.enum(["system", "cursor", "vscode", "finder", "copy"]);
13641
13664
  Settings = external_exports.object({
@@ -13831,8 +13854,18 @@ var init_src = __esm({
13831
13854
  // Broker-backed Claude only — that is every Claude thread dispatched since the broker cutover. On
13832
13855
  // any other runtime the message is delivered normally and this is ignored, never refused: a send
13833
13856
  // that arrives is always better than a send that errors.
13834
- interrupt: external_exports.boolean().optional()
13857
+ interrupt: external_exports.boolean().optional(),
13858
+ // The deliveryId of a FAILED send this one re-sends — the failed bubble's Retry. The server drops that
13859
+ // failed entry in the same write that opens this send's own, so the retry replaces the failed bubble
13860
+ // rather than standing beside it. A new deliveryId, never the old one: the old send's failure may be
13861
+ // ambiguous, and only the operator decides to risk a second copy.
13862
+ supersedes: external_exports.string().min(1).max(200).optional()
13835
13863
  });
13864
+ DismissFailedFollowUpInput = external_exports.object({
13865
+ slug: ThreadSlug,
13866
+ deliveryId: external_exports.string().min(1).max(200)
13867
+ }).strict();
13868
+ DismissFailedFollowUpResult = external_exports.object({ dismissed: external_exports.boolean() }).strict();
13836
13869
  UnqueueFollowUpInput = external_exports.object({
13837
13870
  slug: ThreadSlug,
13838
13871
  // Same staleness guard as followUp: a stale tab must not unqueue against a re-dispatched session.
@@ -13852,6 +13885,11 @@ var init_src = __esm({
13852
13885
  interrupted: external_exports.boolean(),
13853
13886
  reason: external_exports.string().optional()
13854
13887
  }).strict();
13888
+ CompactThreadInput = external_exports.object({
13889
+ slug: ThreadSlug,
13890
+ // Same staleness guard as followUp: a stale tab must not compact a re-dispatched session.
13891
+ sessionId: external_exports.string().min(1)
13892
+ }).strict();
13855
13893
  SetThreadSnoozeInput = external_exports.object({
13856
13894
  slug: ThreadSlug,
13857
13895
  sessionId: external_exports.string().min(1),
@@ -14460,8 +14498,13 @@ ${DISPATCH_TASK_BANNER}
14460
14498
  // yet — renders as an ordinary (un-grayed) user bubble. "unconfirmed": no evidence appeared within
14461
14499
  // the timeout — the injection likely mutated/never landed; the client renders a quiet warning.
14462
14500
  // Once the real transcript record lands the ledger drops the item and this field goes with it.
14463
- // Additive + optional.
14464
- deliveryState: external_exports.enum(["pending", "enqueued", "delivered", "unconfirmed"]).optional(),
14501
+ // "sending": the server's WRITE-AHEAD entry, opened before any transport is touched, so the text is
14502
+ // the server's from the instant it arrives. "failed": the transport threw, or never answered — the
14503
+ // text is kept until the operator retries, edits or dismisses it, and nothing retries it on its own.
14504
+ // Additive + optional: an older client renders both as a plain bubble (gray for "sending").
14505
+ deliveryState: external_exports.enum(["sending", "pending", "enqueued", "delivered", "unconfirmed", "failed"]).optional(),
14506
+ // Why a "failed" send failed, verbatim from the transport. Set only alongside deliveryState "failed".
14507
+ deliveryError: external_exports.string().optional(),
14465
14508
  // FRIZZ wrote this user turn, not the human: it is a scheduler wake delivery (isWakeDelivery). The
14466
14509
  // client renders it as a first-party card rather than the human's off-white right-justified bubble,
14467
14510
  // which was claiming the operator had typed a message the watcher composed. Additive + optional: an
@@ -14613,7 +14656,8 @@ ${DISPATCH_TASK_BANNER}
14613
14656
  });
14614
14657
  ProjectRailCounts = external_exports.object({
14615
14658
  queued: external_exports.number().int().nonnegative(),
14616
- running: external_exports.number().int().nonnegative()
14659
+ running: external_exports.number().int().nonnegative(),
14660
+ asks: external_exports.number().int().nonnegative()
14617
14661
  });
14618
14662
  PROJECT_ICON_MAX_BASE64_CHARS = 4 * 1024 * 1024;
14619
14663
  DirectoryPickResult = external_exports.discriminatedUnion("kind", [
@@ -19955,13 +19999,6 @@ var init_project_registry = __esm({
19955
19999
  }
19956
20000
  });
19957
20001
 
19958
- // packages/server/src/native-helper.ts
19959
- var init_native_helper = __esm({
19960
- "packages/server/src/native-helper.ts"() {
19961
- "use strict";
19962
- }
19963
- });
19964
-
19965
20002
  // src/production.ts
19966
20003
  init_local_origin();
19967
20004
  import { readFileSync as readFileSync15 } from "node:fs";
@@ -20561,6 +20598,29 @@ var AccessStore = class _AccessStore {
20561
20598
  const id = _AccessStore.idOf(payload);
20562
20599
  return !(id && this.sessions.isRevoked(id));
20563
20600
  }
20601
+ /**
20602
+ * Sign out the device HOLDING this session — the "Sign out this device" row, not `--sign-out`.
20603
+ *
20604
+ * The id comes from the session itself, and only after its signature verifies, so the one thing this
20605
+ * can ever revoke is the credential the caller just proved it holds. There is deliberately no
20606
+ * parameter naming any other id: signing out SOMEONE ELSE stays loopback-only, because a stolen
20607
+ * session must not be able to evict the owner and keep the board.
20608
+ *
20609
+ * - `revoked`: the id is on the denylist now, so a surviving copy of this cookie is refused too.
20610
+ * - `legacy`: a session minted before ids existed verifies but has no id to revoke on its own. The
20611
+ * caller can still clear the cookie; rotating the key remains the way to kill a copy of it.
20612
+ * - `no-session`: nothing valid was presented — absent, forged, expired, or already signed out.
20613
+ */
20614
+ signOutSession(session) {
20615
+ if (!session || !this.verifySession(session)) return { result: "no-session" };
20616
+ const id = _AccessStore.idOf(session.slice(0, session.lastIndexOf(".")));
20617
+ if (!id) return { result: "legacy" };
20618
+ if (!this.sessions.revoke(id)) {
20619
+ this.sessions.record({ id, label: "unknown device", createdAt: this.options.now() });
20620
+ this.sessions.revoke(id);
20621
+ }
20622
+ return this.sessions.isRevoked(id) ? { result: "revoked", id } : { result: "no-session" };
20623
+ }
20564
20624
  /**
20565
20625
  * Drop codes past their expiry so a long-lived board does not accumulate them forever.
20566
20626
  *
@@ -22586,6 +22646,7 @@ var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restar
22586
22646
  var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
22587
22647
  var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
22588
22648
  var SUPERVISOR_SESSIONS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sessions`;
22649
+ var SUPERVISOR_SIGN_OUT_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sign-out`;
22589
22650
  var SUPERVISOR_CONTROL_PROTOCOL = 1;
22590
22651
  function responseJson(res, status, value) {
22591
22652
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
@@ -22614,7 +22675,7 @@ function readCookie(header, name) {
22614
22675
  }
22615
22676
  function isControlRequest(req) {
22616
22677
  const url = new URL(req.url ?? "/", "http://frizz.invalid");
22617
- 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;
22678
+ 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;
22618
22679
  }
22619
22680
  function isLocalImageRequest(req) {
22620
22681
  if (req.method !== "GET" && req.method !== "HEAD") return false;
@@ -22820,6 +22881,11 @@ var RestartSupervisorProxy = class {
22820
22881
  }
22821
22882
  async handleControl(req, res) {
22822
22883
  const pathname = new URL(req.url ?? "/", "http://frizz.invalid").pathname;
22884
+ if (pathname !== SUPERVISOR_SIGN_OUT_PATH && this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
22885
+ res.writeHead(401, { "cache-control": "no-store" });
22886
+ res.end();
22887
+ return;
22888
+ }
22823
22889
  const sameOrigin = req.headers["sec-fetch-site"] === "same-origin" || this.vouchesSameOrigin(req);
22824
22890
  const allowMissingOrigin = pathname === SUPERVISOR_STATUS_PATH && req.method === "GET" && sameOrigin;
22825
22891
  if (!this.authorityAccepted(req, allowMissingOrigin)) {
@@ -22883,8 +22949,24 @@ var RestartSupervisorProxy = class {
22883
22949
  responseJson(res, 200, { signedOut: 1 });
22884
22950
  return;
22885
22951
  }
22952
+ if (pathname === SUPERVISOR_SIGN_OUT_PATH) {
22953
+ if (req.method !== "POST") {
22954
+ res.writeHead(405, { allow: "POST" });
22955
+ res.end();
22956
+ return;
22957
+ }
22958
+ this.signOutThisDevice(req, res);
22959
+ return;
22960
+ }
22886
22961
  if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
22887
- responseJson(res, 200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...this.status() });
22962
+ responseJson(res, 200, {
22963
+ protocol: SUPERVISOR_CONTROL_PROTOCOL,
22964
+ ...this.status(),
22965
+ // Sent ONLY when true, so a loopback tab's payload is byte-identical to what it was. The web
22966
+ // app cannot tell on its own: the session cookie is HttpOnly, and the page is the same bundle
22967
+ // whichever way it was reached. It is a fact about THIS request, never about the board.
22968
+ ...this.carriesRemoteSession(req) ? { remoteSession: true } : {}
22969
+ });
22888
22970
  return;
22889
22971
  }
22890
22972
  if (pathname !== SUPERVISOR_RESTART_PATH && pathname !== SUPERVISOR_UPDATE_RESTART_PATH || req.method !== "POST") {
@@ -22962,6 +23044,42 @@ var RestartSupervisorProxy = class {
22962
23044
  if (!this.publicOrigin) return true;
22963
23045
  return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
22964
23046
  }
23047
+ /** Did this request arrive through the public origin carrying a live session? Loopback never does. */
23048
+ carriesRemoteSession(req) {
23049
+ return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
23050
+ }
23051
+ /**
23052
+ * End the session this request carries, and nothing else — see SUPERVISOR_SIGN_OUT_PATH in
23053
+ * handleControl for why that makes it safe to reach from the tunnel.
23054
+ *
23055
+ * A loopback request is a no-op even if it somehow carries a cookie: the operator's own tab has no
23056
+ * remote session to end, and loopback is the place `--sign-out` already covers.
23057
+ */
23058
+ signOutThisDevice(req, res) {
23059
+ const reply = (status, body, clearCookie) => {
23060
+ res.writeHead(status, {
23061
+ "content-type": "application/json; charset=utf-8",
23062
+ "cache-control": "no-store",
23063
+ // Same attributes the exchange set, so the browser matches and drops that exact cookie.
23064
+ ...clearCookie ? { "set-cookie": `${SESSION_COOKIE}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0` } : {}
23065
+ });
23066
+ res.end(JSON.stringify(body));
23067
+ };
23068
+ if (!this.arrivedPublicly(req)) {
23069
+ reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, false);
23070
+ return;
23071
+ }
23072
+ const outcome = this.access.signOutSession(readCookie(req.headers.cookie, SESSION_COOKIE));
23073
+ if (outcome.result === "revoked") {
23074
+ reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "signed-out", id: outcome.id }, true);
23075
+ return;
23076
+ }
23077
+ if (outcome.result === "legacy") {
23078
+ reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "cookie-cleared" }, true);
23079
+ return;
23080
+ }
23081
+ reply(401, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, true);
23082
+ }
22965
23083
  /**
22966
23084
  * Trade `?frizz_code=…` for a cookie, then bounce to the same URL
22967
23085
  * without the secret, so it never lands in history, a Referer header, or a screenshot.
@@ -26464,7 +26582,8 @@ function createNpmRegistryReleaseAdapter(options2 = {}) {
26464
26582
  return reject(new Error(`could not check npm for ${packageName}: ${why}`));
26465
26583
  }
26466
26584
  try {
26467
- const parsed = JSON.parse(stdout);
26585
+ const answer = JSON.parse(stdout);
26586
+ const parsed = Array.isArray(answer) && answer.length === 1 ? answer[0] : answer;
26468
26587
  resolveVersion(typeof parsed === "string" ? parsed : String(parsed));
26469
26588
  } catch {
26470
26589
  resolveVersion(stdout.trim().replaceAll('"', ""));
@@ -26669,7 +26788,8 @@ function npmServerPackageInstaller(env = process.env) {
26669
26788
  },
26670
26789
  async latestVersion(packageName) {
26671
26790
  if (!PACKAGE.test(packageName)) throw new Error("invalid Frizz server package name");
26672
- const version = JSON.parse(await run3(["view", `${packageName}@latest`, "version", "--json"]));
26791
+ const parsed = JSON.parse(await run3(["view", `${packageName}@latest`, "version", "--json"]));
26792
+ const version = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
26673
26793
  if (typeof version !== "string" || !VERSION.test(version)) throw new Error("npm returned an invalid Frizz server version");
26674
26794
  return version;
26675
26795
  }
@@ -26897,7 +27017,6 @@ function releaseHeldLease(lease) {
26897
27017
  }
26898
27018
 
26899
27019
  // src/preflight.ts
26900
- init_native_helper();
26901
27020
  import { spawnSync } from "node:child_process";
26902
27021
  var SUPPORTED_NODE_LINES = [
26903
27022
  { major: 22, minor: 13 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frizz",
3
- "version": "0.13.3",
3
+ "version": "0.13.6",
4
4
  "private": false,
5
5
  "description": "A local web UI for running many coding agents at once",
6
6
  "keywords": [
@@ -49,6 +49,7 @@
49
49
  "@types/ws": "^8.5",
50
50
  "esbuild": "^0.25.0",
51
51
  "jsqr": "^1.4.0",
52
+ "node-pty": "1.2.0-beta.15",
52
53
  "puppeteer": "^25.3.0",
53
54
  "typescript": "^5.7",
54
55
  "ws": "^8.18"
@@ -65,7 +66,7 @@
65
66
  },
66
67
  "frizzServer": {
67
68
  "package": "frizz-server",
68
- "version": "0.15.1",
69
+ "version": "0.15.7",
69
70
  "protocol": 1,
70
71
  "dataEpoch": 1
71
72
  }