usebeeline 0.1.10 → 0.1.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.
package/README.md CHANGED
@@ -96,6 +96,8 @@ A Room and a corner are the same conversation surface with different permissions
96
96
 
97
97
  **A corner** is a fresh worktree, its own branch, one owning agent, and one fixed objective of at most 24 words. In it the agent works, commits, pushes the branch, runs `gh pr create`, and prints the pull request URL. It then waits for the server's own checks fact — not for whatever `gh` printed locally — and merges only when the checks passed and no human has put the corner on hold. The merge webhook archives the corner and reaps the worktree.
98
98
 
99
+ When Beeline reads that a Room's repository allows GitHub auto-merge, it warns a Workspace manager while they choose a reviewer and shows an inline notice when one is already set. The warning does not prevent setting a reviewer. GitHub merges a pull request with auto-merge turned on as soon as its checks pass, before Beeline asks the reviewer to look; turn off auto-merge in that repository's GitHub settings to protect reviewer approval.
100
+
99
101
  The corner receives a GitHub App token scoped to **that one repository**, installed as a worktree-local git credential helper. Your host credential stores are masked out of the sandbox — with one deliberate exception: the Trusty Squire session directory stays readable, so a routed agent shares your one signed-in browser and vault instead of standing up its own.
100
102
 
101
103
  **Direct messages** are strictly conversational: no repository binding, no corners.
@@ -13834,10 +13834,10 @@ function squireLoginWallIn(value, depth = 0) {
13834
13834
  if (!item)
13835
13835
  return void 0;
13836
13836
  const needsUser = record(item.needs_user);
13837
- if (needsUser?.wall === "google_session") {
13837
+ if (needsUser?.wall === "google_session" || needsUser?.wall === "oauth_sign_in") {
13838
13838
  const message = shortString(needsUser.message);
13839
13839
  if (message)
13840
- return { wall: "google_session", message };
13840
+ return { wall: needsUser.wall, message };
13841
13841
  }
13842
13842
  const oauth = record(item.oauth);
13843
13843
  if (oauth?.state === "awaiting_human") {
@@ -16756,8 +16756,10 @@ var StdioSquireMcpClient = class {
16756
16756
  continue;
16757
16757
  }
16758
16758
  const id = typeof message.id === "number" ? message.id : void 0;
16759
- if (id === void 0)
16759
+ if (id === void 0) {
16760
+ this.options.onNotification?.(message);
16760
16761
  continue;
16762
+ }
16761
16763
  const entry = this.pending.get(id);
16762
16764
  if (!entry)
16763
16765
  continue;
@@ -16845,6 +16847,7 @@ var SquireTaskRelay = class {
16845
16847
  contextFile;
16846
16848
  authorize;
16847
16849
  makeClient;
16850
+ onApprovalDecided;
16848
16851
  server;
16849
16852
  url;
16850
16853
  token = randomUUID3();
@@ -16860,13 +16863,16 @@ var SquireTaskRelay = class {
16860
16863
  processGroup: true,
16861
16864
  onSpawn: callbacks.onSpawn,
16862
16865
  onExit: callbacks.onExit,
16863
- log: callbacks.onDiagnostic
16864
- })) {
16866
+ log: callbacks.onDiagnostic,
16867
+ onNotification: callbacks.onNotification
16868
+ }), onApprovalDecided = () => {
16869
+ }) {
16865
16870
  this.agentId = agentId;
16866
16871
  this.roomId = roomId;
16867
16872
  this.contextFile = contextFile;
16868
16873
  this.authorize = authorize;
16869
16874
  this.makeClient = makeClient;
16875
+ this.onApprovalDecided = onApprovalDecided;
16870
16876
  }
16871
16877
  async listen() {
16872
16878
  if (this.closed)
@@ -16992,6 +16998,7 @@ var SquireTaskRelay = class {
16992
16998
  };
16993
16999
  task.client = this.makeClient({
16994
17000
  onSpawn: (pid) => this.log("relay-spawn", task, { pid }),
17001
+ onNotification: (message) => this.handleApprovalNotification(task, message),
16995
17002
  onDiagnostic: (message) => {
16996
17003
  if (message.startsWith("squire mcp exited"))
16997
17004
  return;
@@ -17128,7 +17135,10 @@ var SquireTaskRelay = class {
17128
17135
  if (pending && ids.length) {
17129
17136
  task.pendingApprovals.set(approvalKey, {
17130
17137
  requestId: active.requestId,
17131
- sessionIds: new Set(ids)
17138
+ sessionIds: new Set(ids),
17139
+ tool: approval.tool,
17140
+ title: approval.title,
17141
+ detail: approval.detail
17132
17142
  });
17133
17143
  this.log("approval-pending", task, {
17134
17144
  tool: safeToolName ?? null,
@@ -17169,6 +17179,37 @@ var SquireTaskRelay = class {
17169
17179
  task.pendingApprovals.delete(id);
17170
17180
  }
17171
17181
  }
17182
+ /**
17183
+ * Squire's own unsolicited decision on an approval this task is still
17184
+ * holding a connection open for. Matched back to the pending entry by the
17185
+ * same hash `handleSerial` keyed it under (the approval id Squire itself
17186
+ * returned), so a notification for an approval this relay never tracked —
17187
+ * or one already released — is silently ignored, never trusted blind.
17188
+ */
17189
+ handleApprovalNotification(task, message) {
17190
+ if (message.method !== "notifications/approval_decided")
17191
+ return;
17192
+ const params = message.params;
17193
+ const approvalId = params?.approval_id;
17194
+ const status = params?.status;
17195
+ if (typeof approvalId !== "string" || !approvalId || status !== "approved" && status !== "denied")
17196
+ return;
17197
+ const approvalKey = createHash("sha256").update(approvalId).digest("hex");
17198
+ const pending = task.pendingApprovals.get(approvalKey);
17199
+ if (!pending)
17200
+ return;
17201
+ task.pendingApprovals.delete(approvalKey);
17202
+ this.onApprovalDecided({
17203
+ requestId: pending.requestId,
17204
+ approvalId,
17205
+ status,
17206
+ tool: pending.tool,
17207
+ title: pending.title,
17208
+ detail: pending.detail
17209
+ });
17210
+ if (!this.active && !this.approvalRequestId && !task.pendingApprovals.size)
17211
+ this.scheduleIdle();
17212
+ }
17172
17213
  };
17173
17214
  function runSquireTaskProxy(env = process.env) {
17174
17215
  const url = env.BEELINE_SQUIRE_RELAY_URL;
@@ -18013,6 +18054,7 @@ var SERVER_EVENT_KINDS = [
18013
18054
  "check-failed",
18014
18055
  "merged",
18015
18056
  "grant-decided",
18057
+ "squire-approval-decided",
18016
18058
  "connector-offer-decided",
18017
18059
  "turn-cancelled",
18018
18060
  "choice-answered",
@@ -18020,6 +18062,25 @@ var SERVER_EVENT_KINDS = [
18020
18062
  "poll-closed",
18021
18063
  "workflow-handoff"
18022
18064
  ];
18065
+ var SERVER_EVENT_KIND_DETAIL = {
18066
+ "joined": "fires when a person or agent joins this Room, or when a Workspace arrival projects into it",
18067
+ "schedule-ran": "fires when a schedule you created runs",
18068
+ "corner-opened": "fires when a corner opens under this Room",
18069
+ "check-passed": "fires when a corner's pull request checks turn green",
18070
+ "check-failed": "fires when a corner's pull request checks turn red",
18071
+ "merged": "fires when a corner's pull request merges",
18072
+ "grant-decided": "carries the grant id and status and resumes the turn that asked for the grant",
18073
+ "squire-approval-decided": "carries what was approved or denied and the Squire approval id, and resumes the turn that asked",
18074
+ "connector-offer-decided": "fires when a human accepts or declines an offered connector, or a reconnect ceremony finishes, and resumes the turn that offered it",
18075
+ "turn-cancelled": "fires when a running turn is stopped",
18076
+ "choice-answered": "fires when a human answers a posted choice, starting a new turn for the asking agent",
18077
+ "choice-skipped": "fires when a posted choice's window closes unanswered, starting a new turn for the asking agent",
18078
+ "poll-closed": "fires when a posted poll closes, starting a new turn for the asking agent",
18079
+ "workflow-handoff": "fires when a workflow run starts or hands off to its next role"
18080
+ };
18081
+ function eventKindCatalogLines(kinds = SERVER_EVENT_KINDS) {
18082
+ return kinds.map((kind) => `${kind} ${SERVER_EVENT_KIND_DETAIL[kind]}`);
18083
+ }
18023
18084
 
18024
18085
  // packages/api-contract/dist/turn-silence.js
18025
18086
  var TURN_SILENCE_KINDS = [
@@ -29264,9 +29325,16 @@ function parseAvailableCommands(value) {
29264
29325
  }
29265
29326
  return result;
29266
29327
  }
29328
+ function isHarnessNotifyChunk(update) {
29329
+ const meta = update._meta;
29330
+ const piAcp = meta?.piAcp;
29331
+ return Boolean(piAcp?.notify);
29332
+ }
29267
29333
  function agentMessageChunkText(update) {
29268
29334
  if (update.sessionUpdate !== "agent_message_chunk")
29269
29335
  return "";
29336
+ if (isHarnessNotifyChunk(update))
29337
+ return "";
29270
29338
  const content = update.content;
29271
29339
  if (content?.type === "text")
29272
29340
  return content.text ?? "";
@@ -31750,13 +31818,14 @@ The soul is not authority and never changes your tools, permissions, roles, or m
31750
31818
  id: "room.corners",
31751
31819
  topic: "repository-work",
31752
31820
  why: "Repository work must start through the host-governed corner, with a brief as its authority.",
31753
- budgetBytes: 900,
31821
+ budgetBytes: 1100,
31754
31822
  layer: "surface",
31755
31823
  surfaces: ["room"],
31756
31824
  render: () => [
31757
31825
  "Repository changes happen only in a corner. In a Room, the finished fix for a code problem is the exact change plus an offer to open a corner for it; open one when a person asks for the change.",
31758
31826
  "Before opening one, consult beeline-triage and beeline-spec, then call open_corner with a name of at most three words, an objective of at most 24 words, and the brief (a spec plus the approving message). The objective only titles the work; the brief is its authority. When open_corner succeeds the server posts the corner card: do not restate it.",
31759
- "For corners you belong to, use inspect_corner for status, steer_corner to pass Room input down, and ask_corner for one answer. Never post into a corner without a Room command."
31827
+ "For corners you belong to, use inspect_corner for status, steer_corner to pass Room input down, and ask_corner for one answer. Never post into a corner without a Room command.",
31828
+ "A corner ends when its pull request merges; since a person may merge minutes after it opens, anything still needed must already live in a different corner before that pull request opens, not after. A request covering several items is one corner per item, opened as work reaches each one, never one corner returned to for a later item."
31760
31829
  ].join(" ")
31761
31830
  },
31762
31831
  {
@@ -32086,10 +32155,10 @@ var BEELINE_REVIEW_SKILL_NAME = "beeline-review";
32086
32155
  var BEELINE_SPEC_SKILL_NAME = "beeline-spec";
32087
32156
  var BEELINE_ROOM_MECHANICS = [
32088
32157
  "Files and photos on the message you are answering are downloaded for you: read them at the local path named in the prompt (photos may also arrive inline). A file on an earlier message shows only as a \u{1F4CE} marker with an id; call beeline-agent download_attachment with that message id and attachment id, then read the returned path. Never fetch a media URL; if the download fails, tell the requester you could not open the file.",
32089
- "To create a file you can send, call beeline-agent write_scratch_file with a relative path and content - text by default, or base64 for bytes you computed; it returns a path in your writable session home. To send a file, call beeline-agent post_artifact with a path inside your checkout or anywhere in your writable session home (wherever a file you or your harness generated actually landed, including one you just wrote), or with html/bytes content directly; it is uploaded and attached to your reply, and title and mime default from the file when you post by path. write_scratch_file produces the file, not a picture. To put a real photograph in an artifact, call beeline-agent fetch_image with the photo URL; it writes the bytes to your session scratch and returns the path, mime, and size \u2014 read them, base64-encode, and embed as a data: URL. The validator still refuses every http(s) image reference, and drawing an SVG stand-in is not a photograph.",
32158
+ "To create a file you can send, call beeline-agent write_scratch_file with a relative path and content - text by default, or base64 for bytes you computed; it returns a path in your writable session home. To send a file, call beeline-agent post_artifact with a path inside your checkout or anywhere in your writable session home (wherever a file you or your harness generated actually landed, including one you just wrote), or with html/bytes content directly; it is uploaded and attached to your reply, and title and mime default from the file when you post by path. write_scratch_file produces the file, not a picture.",
32090
32159
  "To run something later or repeatedly, call beeline-agent create_schedule (interval in minutes or a 5-field cron, optional maxRuns); list_schedules / update_schedule / delete_schedule manage them, including schedules other agents created for themselves.",
32091
32160
  `To react to a message in this Room, call beeline-agent react_to_message with its message id and one supported emoji (${MESSAGE_REACTION_EMOJIS.join(" ")}).`,
32092
- `To react to things that HAPPEN in this Room rather than only to what is said to you, call beeline-agent subscribe_events with the kinds you want (${SERVER_EVENT_KINDS.join(", ")}); each one then wakes you for a turn. Subscriptions are per Room and cover every way the event lands: joining a Room you subscribed to wakes you, and so does a person arriving in the Workspace when that arrival projects into this Room - subscribe to joined in an onboarding Room and every newcomer wakes you, exactly like a greeter. It replaces your list, so send every kind you want - list_event_subscriptions shows the current one. You do this yourself: nobody has to configure it for you. grant-decided carries the grant id and status and resumes the turn that asked for the grant. choice-answered, choice-skipped, and poll-closed start a new input turn for the asking agent; they do not resume a paused grant. A corner merging wakes nobody in its parent Room by default. If you still have work to do after a corner merges (a hand-off, a release step, a reply that has to describe landed code), subscribe to merged in the parent Room, from a turn there, before the merge. Each merge in that Room then wakes you and names the corner and pull request: act only on the corner you are waiting for, post nothing for any other merge, and take merged back out of your list once nothing is left to wait for. Never post just to acknowledge a merge: the merge card already announces it.`,
32161
+ `To react to things that HAPPEN in this Room rather than only to what is said to you, call beeline-agent subscribe_events with the kinds you want; each one then wakes you for a turn. Available kinds: ${eventKindCatalogLines().join("; ")}. Subscriptions are per Room and cover every way the event lands: joining a Room you subscribed to wakes you, and so does a person arriving in the Workspace when that arrival projects into this Room - subscribe to joined in an onboarding Room and every newcomer wakes you, exactly like a greeter. It replaces your list, so send every kind you want - list_event_subscriptions shows the current one. You do this yourself: nobody has to configure it for you. A corner merging wakes nobody in its parent Room by default. If you still have work to do after a corner merges (a hand-off, a release step, a reply that has to describe landed code), subscribe to merged in the parent Room, from a turn there, before the merge. Each merge in that Room then wakes you and names the corner and pull request: act only on the corner you are waiting for, post nothing for any other merge, and take merged back out of your list once nothing is left to wait for. Never post just to acknowledge a merge: the merge card already announces it. If you are waiting on a human decision - a grant, a connector offer, a posted choice or poll, or a Squire approval (payment, credential, sign-in, or vouch) - subscribe to its kind instead of asking them to type a confirmation in the Room; the decision wakes you with the answer.`,
32093
32162
  "If you need reach outside the sandbox, call beeline-agent request_grant. If you already know discrete options, call beeline-agent ask_choice (one human, optional) or open_poll (every human in this Room, required deadline). A poll is refused below two electors and above fifty, and in a DM. A plurality is a fact, never permission to deploy, delete, merge, or spend. Open-ended asks stay tagged prose. Never put Always / Once / No on a preference.",
32094
32163
  "To state something that happened so the Room and other agents can act on it, call beeline-agent emit_event with your own agent:<slug> kind, one sentence, and optionally the agent members to wake. Chains of events are bounded and a refused emit posts nothing."
32095
32164
  ].join(" ");
@@ -32126,6 +32195,10 @@ A **tool** is something you can use once a human adds it; a **key** is the crede
32126
32195
 
32127
32196
  The tools this build knows, what each is for, and which the owner of this machine already has are one call away: beeline-agent workbench_status. That read is the Workbench of the machine and owner you actually run on, not the person who asked. Connected apps are listed once per app with a stable ID. Their sign-in and credentials stay with the server. Trusty Squire is vaulted credentials plus a browser for the separate key and browser routes. Tailscale installs its CLI on the selected helper and opens Tailscale's own browser sign-in; once connected, use the CLI for tailnet resources and tailscale file cp or tailscale file get for Taildrop. The wallet is created only from the Workbench page. When you cannot reach a tool, say: I can't reach X on this machine because Y; to fix it, Z. Give one explanation, then take or offer Z.
32128
32197
 
32198
+ **Inspect your own host before answering.** When a question is about a system on the machine you run on, inspect its live state with the tools you have: processes, logs, local endpoints or service status. Say what you checked. Memory and Room history are not evidence of current state.
32199
+
32200
+ **Use available tools before asking the person.** Before asking a person for a code, a status or a file, call workbench_status and check whether a tool or connected app you already have can get it, then use it. This includes an emailed verification code in an already-connected inbox. Ask only when none can, and say why. The key-ownership and grant rules still apply.
32201
+
32129
32202
  **Offer the tool at the moment you need it.** When the work in front of you needs a tool the person does not have, do not send them to a settings page and do not stop at naming it. Call workbench_status first - a tool they already have is used, not offered. In a Room or DM, call offer_connector with the connectorType and one short reason: a card appears there, spoken by you, addressed to the person you are answering, with one action. Connector offers are unavailable in corners; use a Room or DM for those. Your turn pauses on the card - say in prose what you are waiting for and end the turn; you are woken when it is added, and then you carry on.
32130
32203
 
32131
32204
  **Research before you offer, and say so.** Never offer a tool you cannot describe. If someone asks you to install or add something you do not already know - by name, by purpose, or by what it will hold - say plainly that you are looking it up first, find out what it is and what it does with credentials, and state what you learned in your reply BEFORE the card appears. A person reading "Add X?" must already have read, in your own words, what X is. Refusing to act blind is part of being trusted with keys.
@@ -32136,9 +32209,9 @@ Keys belong to the human who provisioned them. Your owner's keys serve whoever a
32136
32209
 
32137
32210
  ## Connect an app
32138
32211
 
32139
- When a request needs an app or service, call beeline-agent \`workbench_status\` first. For a connected app, use its ID with \`list_app_tools\`, then call \`execute_app_tool\` with the selected tool slug and arguments. The server executes against that person\u2019s account and decides permission on every call. An empty tool list or \`needs_connection\` means the account must be reconnected; \`needs_permission\` means wait for the connected person's decision. Do not switch paths after denial or provider unavailability. No app token, provider key, or sign-in URL belongs in chat or a model prompt.
32212
+ When a request needs an app or service, call beeline-agent \`workbench_status\` first. For a connected app, use its ID with \`list_app_tools\`, then call \`execute_app_tool\` with the selected tool slug and arguments. The server executes against that person\u2019s account and decides permission on every call. If tool discovery reports an error, follow its cause; \`needs_connection\` means the account must be reconnected, and \`needs_permission\` means wait for the connected person's decision. Do not switch paths after denial or provider unavailability. No app token, provider key, or sign-in URL belongs in chat or a model prompt.
32140
32213
 
32141
- For an app that is not connected, call \`connect_app\` with its name, one short reason, and a request-specific \`continuation\`: one sentence stating what you will do right after sign-in. Do not include a link, credential, account identifier, or private data in that sentence. The server chooses the route and returns \`next\`. In a corner, call it from that corner so the sign-in card and connection wake return there. A managed OAuth app presents a sign-in card to its owner; end the turn and wait for the connection wake. If the provider reports managed OAuth unsupported, follow the server's Squire route and its instructions. A provider outage is unavailable, not permission to change routes. After the person connects, read Workbench again and use the app ID. Trusty Squire handles the separate API key and browser routes; never ask for a raw key in chat. Every use is authorized as \`app:<key>\` and recorded once.
32214
+ For an app that is not connected, call \`connect_app\` with its name, one short reason, and a request-specific \`continuation\`: one sentence stating what you will do right after sign-in. Do not include a link, credential, account identifier, or private data in that sentence. The server chooses the route and returns \`next\`. In a corner, call it from that corner so the sign-in card and connection wake return there. A managed OAuth app presents a sign-in card to its owner; end the turn and wait for the connection wake. If the provider reports managed OAuth unsupported, follow the server's Squire route and its instructions. A provider outage is unavailable, not permission to change routes. After the person connects, read Workbench again and use the app ID. Every use is authorized as \`app:<key>\` and recorded once.
32142
32215
 
32143
32216
  ## Showing a mock
32144
32217
 
@@ -37991,7 +38064,7 @@ import { mkdir as mkdir28, rename as rename12, stat as stat9, writeFile as write
37991
38064
  import { dirname as dirname29 } from "node:path";
37992
38065
 
37993
38066
  // apps/body/dist/agent-runtime.js
37994
- import { homedir as homedir18 } from "node:os";
38067
+ import { homedir as homedir19 } from "node:os";
37995
38068
  import { dirname as dirname28, join as join19, resolve as resolve41 } from "node:path";
37996
38069
 
37997
38070
  // apps/body/dist/model-availability.js
@@ -39549,7 +39622,7 @@ async function runServerCommandIntake(options) {
39549
39622
  import { execFile as execFile12 } from "node:child_process";
39550
39623
  import { createHash as createHash12 } from "node:crypto";
39551
39624
  import { mkdir as mkdir25, readFile as readFile27, writeFile as writeFile20 } from "node:fs/promises";
39552
- import { homedir as homedir17, hostname as hostname3 } from "node:os";
39625
+ import { homedir as homedir18, hostname as hostname3 } from "node:os";
39553
39626
  import { dirname as dirname25, join as join18, resolve as resolve35 } from "node:path";
39554
39627
  import { promisify as promisify7 } from "node:util";
39555
39628
 
@@ -39889,6 +39962,7 @@ var AgentTurnStream = class {
39889
39962
  * one bounds the lane at ONE write in flight plus ONE waiting.
39890
39963
  */
39891
39964
  pending;
39965
+ latestChunk = "";
39892
39966
  /**
39893
39967
  * The stream as it stood when a corner handed everything up to that point to
39894
39968
  * its durable work ledger — this turn's persisted stream offset. Held as the
@@ -39920,6 +39994,7 @@ var AgentTurnStream = class {
39920
39994
  * it has.
39921
39995
  */
39922
39996
  onChunk = (_delta, full, currentRun) => {
39997
+ this.latestChunk = sanitizeAgentReply(currentRun ?? "");
39923
39998
  this.latest = full;
39924
39999
  this.latestRun = currentRun ?? "";
39925
40000
  const text2 = sanitizeAgentReply(this.tailOf(full));
@@ -40001,9 +40076,10 @@ var AgentTurnStream = class {
40001
40076
  if (this.inFlight || this.pending === void 0)
40002
40077
  return;
40003
40078
  const text2 = this.pending;
40079
+ const latestChunk = this.latestChunk;
40004
40080
  this.pending = void 0;
40005
40081
  const { api, agentId, roomId, requestId, label } = this.options;
40006
- this.inFlight = api.execute("postAgentDraft", { agentId, roomId, turnId: requestId, text: text2 }).then(() => void 0).catch((error) => console.error(`[thin-core] ${label} draft publish failed:`, error)).then(() => {
40082
+ this.inFlight = api.execute("postAgentDraft", { agentId, roomId, turnId: requestId, text: text2, latestChunk }).then(() => void 0).catch((error) => console.error(`[thin-core] ${label} draft publish failed:`, error)).then(() => {
40007
40083
  this.inFlight = void 0;
40008
40084
  this.publishPending();
40009
40085
  });
@@ -40031,6 +40107,7 @@ var AgentTurnStream = class {
40031
40107
  beginRun() {
40032
40108
  this.latest = "";
40033
40109
  this.latestRun = "";
40110
+ this.latestChunk = "";
40034
40111
  this.persisted = "";
40035
40112
  this.pending = void 0;
40036
40113
  }
@@ -40188,8 +40265,10 @@ import { promisify as promisify5 } from "node:util";
40188
40265
  var PI_MCP_BRIDGE_FILENAME = "beeline-mcp-bridge.js";
40189
40266
  var execFileAsync5 = promisify5(execFile9);
40190
40267
  var nativeMcpProbes = /* @__PURE__ */ new Map();
40191
- async function piHasNativeMcp(command) {
40192
- let probe = nativeMcpProbes.get(command);
40268
+ async function piHasNativeMcp(command, env) {
40269
+ const path = env?.PATH ?? process.env.PATH ?? "";
40270
+ const cacheKey = `${command}\0${path}`;
40271
+ let probe = nativeMcpProbes.get(cacheKey);
40193
40272
  if (!probe) {
40194
40273
  probe = (async () => {
40195
40274
  const home = await mkdtemp2(resolve28(tmpdir5(), "beeline-pi-mcp-probe-"));
@@ -40197,7 +40276,7 @@ async function piHasNativeMcp(command) {
40197
40276
  const { stdout: stdout6 } = await execFileAsync5(command, ["mcp", "list", "--json"], {
40198
40277
  cwd: home,
40199
40278
  timeout: 1200,
40200
- env: { PATH: process.env.PATH, HOME: home, PI_CODING_AGENT_DIR: home, PI_OFFLINE: "1" }
40279
+ env: { PATH: path, HOME: home, PI_CODING_AGENT_DIR: home, PI_OFFLINE: "1" }
40201
40280
  });
40202
40281
  const result = JSON.parse(stdout6);
40203
40282
  return Array.isArray(result.servers) && Array.isArray(result.errors);
@@ -40207,7 +40286,7 @@ async function piHasNativeMcp(command) {
40207
40286
  await rm11(home, { recursive: true, force: true });
40208
40287
  }
40209
40288
  })();
40210
- nativeMcpProbes.set(command, probe);
40289
+ nativeMcpProbes.set(cacheKey, probe);
40211
40290
  }
40212
40291
  return probe;
40213
40292
  }
@@ -40271,7 +40350,7 @@ async function installPiMcpBridge(input) {
40271
40350
  const path = resolve28(directory, PI_MCP_BRIDGE_FILENAME);
40272
40351
  try {
40273
40352
  const config = await isolatedPiConfig(input.piHome);
40274
- const nativeMcp = input.nativeMcp ?? await piHasNativeMcp(input.piCommand ?? "pi");
40353
+ const nativeMcp = input.nativeMcp ?? await piHasNativeMcp(input.piCommand ?? "pi", input.agentEnv);
40275
40354
  if (nativeMcp) {
40276
40355
  const mcpServers2 = { ...config.mcpServers };
40277
40356
  for (const server of input.servers)
@@ -40602,6 +40681,7 @@ async function contains(git, head, ancestor) {
40602
40681
 
40603
40682
  // apps/body/dist/room-session.js
40604
40683
  import { resolve as resolve30 } from "node:path";
40684
+ import { homedir as homedir16 } from "node:os";
40605
40685
 
40606
40686
  // apps/body/dist/read-only-policy.js
40607
40687
  var READ_ONLY_MCP_SERVER_NAME = "beeline-readonly-mcp";
@@ -40810,6 +40890,7 @@ function beelineAgentMcpServer(config, api, context) {
40810
40890
  { name: "BEELINE_DAEMON_TOKEN", value: connection.daemonToken },
40811
40891
  { name: "BEELINE_HELPER_VERSION", value: connection.helperVersion },
40812
40892
  { name: "BEELINE_DAEMON_AGENT_ID", value: connection.agentId },
40893
+ { name: "BEELINE_OPERATOR_HOME", value: config.operatorHome ?? homedir16() },
40813
40894
  { name: "BEELINE_DAEMON_ROOM_ID", value: context.roomId },
40814
40895
  { name: "BEELINE_DAEMON_WORKSPACE_ID", value: context.workspaceId },
40815
40896
  ...context.cornerId ? [{ name: "BEELINE_DAEMON_CORNER_ID", value: context.cornerId }] : [],
@@ -40979,7 +41060,7 @@ async function prepareCodegraphIndex(config, cwd) {
40979
41060
 
40980
41061
  // apps/body/dist/monolith-room-turn.js
40981
41062
  import { mkdir as mkdir22, writeFile as writeFile18 } from "node:fs/promises";
40982
- import { homedir as homedir16, hostname as hostname2 } from "node:os";
41063
+ import { homedir as homedir17, hostname as hostname2 } from "node:os";
40983
41064
  import { dirname as dirname23, join as join16 } from "node:path";
40984
41065
 
40985
41066
  // packages/api-contract/dist/scheduled-prompts.js
@@ -41427,6 +41508,14 @@ function deviceGrantResumePrompt(device, earlierReply) {
41427
41508
  ].join(" ");
41428
41509
  }
41429
41510
  function resumePrompt(item) {
41511
+ if (item.systemEvent?.kind === "squire-approval-decided") {
41512
+ return [
41513
+ `This is Trusty Squire's answer to the approval you were waiting on: ${item.body}.`,
41514
+ "Your paused work resumes now.",
41515
+ "If it was approved, continue exactly where you left off with Trusty Squire; do not ask the person to approve again, and do not restart the task.",
41516
+ "If it was denied, stop that Trusty Squire action and say plainly what you cannot do."
41517
+ ].join(" ");
41518
+ }
41430
41519
  if (item.systemEvent?.kind === "connector-offer-decided") {
41431
41520
  return [
41432
41521
  `This is the answer to your connector offer: ${item.body}.`,
@@ -41525,7 +41614,10 @@ var MonolithRoomTurnLoop = class {
41525
41614
  generationId: call.generationId,
41526
41615
  target: "squire",
41527
41616
  ...resourceCallFacts({ method: "tools/call", params: { name: call.tool, arguments: call.args } }, "squire")
41528
- })).allowed, options.config.operatorHome ?? homedir16());
41617
+ })).allowed, options.config.operatorHome ?? homedir17(), void 0, (decision2) => {
41618
+ options.api.execute("postSquireApprovalDecision", { roomId: options.roomId, ...decision2 }).catch(() => {
41619
+ });
41620
+ });
41529
41621
  this.options = { ...options, api: this.commandContext.bind(options.api) };
41530
41622
  options.grantRunner?.register(options.roomId, {
41531
41623
  workspaceId: options.workspaceId,
@@ -41560,7 +41652,7 @@ var MonolithRoomTurnLoop = class {
41560
41652
  ...this.sessionScratchDir ? { scratch: this.sessionScratchDir } : {},
41561
41653
  ...this.sessionStateDirs.length ? { harnessStateDirs: this.sessionStateDirs } : {},
41562
41654
  maskPaths: [
41563
- ...credentialMaskPaths(this.options.config.sandboxMaskPaths, this.options.config.operatorHome ?? homedir16()),
41655
+ ...credentialMaskPaths(this.options.config.sandboxMaskPaths, this.options.config.operatorHome ?? homedir17()),
41564
41656
  ...siblingAgentMaskPaths(runtimeDirectory(this.options.runtime.supervisorRoot, this.options.runtime.agent.publicKey))
41565
41657
  ]
41566
41658
  };
@@ -41808,7 +41900,7 @@ var MonolithRoomTurnLoop = class {
41808
41900
  effort: selectionEffort,
41809
41901
  fastMode: configuration.fastMode
41810
41902
  };
41811
- const operatorHome = this.options.config.operatorHome ?? homedir16();
41903
+ const operatorHome = this.options.config.operatorHome ?? homedir17();
41812
41904
  const registryHostDeclarations = registryMcpHostDeclarations(configuration.registryMcpRoutes, this.commandContext.path, this.options.config.registryMcpBrokerSocket);
41813
41905
  const mountedHostRoutes = [...grantedHostRoutes, ...Object.keys(registryHostDeclarations)];
41814
41906
  const squireScope = {
@@ -41942,6 +42034,7 @@ var MonolithRoomTurnLoop = class {
41942
42034
  agentCommand: harnessLabel,
41943
42035
  piHome: agentEnv.PI_CODING_AGENT_DIR,
41944
42036
  piCommand: agentEnv.PI_ACP_PI_COMMAND ?? process.env.PI_ACP_PI_COMMAND,
42037
+ agentEnv,
41945
42038
  servers: [...servers, ...grantedRouteServers]
41946
42039
  });
41947
42040
  const mountedServers = [
@@ -42693,7 +42786,7 @@ function cornerGitHubCommandRefusal(launcher, argv, featureBranch, targetBranch,
42693
42786
  }
42694
42787
  async function writeLauncher(path, config) {
42695
42788
  const source = `#!/usr/bin/env node
42696
- import { spawnSync } from 'node:child_process';
42789
+ import { spawn, spawnSync } from 'node:child_process';
42697
42790
  const config = ${JSON.stringify(config)};
42698
42791
  const cornerGitHubCommandRefusal = ${cornerGitHubCommandRefusal.toString()};
42699
42792
  const authFailure = /(?:authentication failed|bad credentials|could not read username|http(?:\\/\\d(?:\\.\\d)?)? 40[13]|status (?:code )?40[13])/i;
@@ -42708,7 +42801,28 @@ function resolvePushBranch(source) {
42708
42801
  return current.status === 0 ? { branch: current.stdout.trim() } : {};
42709
42802
  }
42710
42803
  const refusal = cornerGitHubCommandRefusal(config.launcher, process.argv.slice(2), config.featureBranch, config.targetBranch, resolvePushBranch);
42711
- if (refusal) { process.stderr.write(refusal + '\\n'); process.exit(1); }
42804
+ if (refusal) { process.stderr.write(refusal + '\\n'); process.exitCode = 1; }
42805
+ function needsToken(argv) {
42806
+ if (config.launcher === 'gh') return true;
42807
+ const optionsWithValue = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--super-prefix', '--config-env']);
42808
+ let index = 0;
42809
+ while (index < argv.length && argv[index].startsWith('-')) {
42810
+ if (argv[index] === '--version' || argv[index] === '--help') return false;
42811
+ index += optionsWithValue.has(argv[index]) ? 2 : 1;
42812
+ }
42813
+ // Unknown commands (including aliases) may contact a remote.
42814
+ const local = new Set([
42815
+ 'add', 'am', 'apply', 'bisect', 'blame', 'branch', 'cat-file', 'check-attr',
42816
+ 'check-ignore', 'check-ref-format', 'checkout', 'cherry-pick', 'clean', 'commit',
42817
+ 'config', 'describe', 'diff', 'diff-files', 'diff-index', 'diff-tree', 'for-each-ref',
42818
+ 'format-patch', 'fsck', 'gc', 'grep', 'hash-object', 'help', 'init', 'log', 'ls-files',
42819
+ 'ls-tree', 'merge', 'merge-base', 'mv', 'notes', 'rebase', 'reflog', 'reset',
42820
+ 'restore', 'rev-list', 'rev-parse', 'revert', 'rm', 'show', 'show-ref', 'stash',
42821
+ 'status', 'switch', 'symbolic-ref', 'tag', 'update-index', 'update-ref',
42822
+ 'verify-commit', 'verify-tag', 'worktree',
42823
+ ]);
42824
+ return !local.has(argv[index]);
42825
+ }
42712
42826
  function token() {
42713
42827
  let result;
42714
42828
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -42717,19 +42831,40 @@ function token() {
42717
42831
  if (attempt < 2) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250 * (attempt + 1));
42718
42832
  }
42719
42833
  process.stderr.write(result.stderr || 'Beeline could not refresh the repository credential.\\n');
42720
- process.exit(result.status || 1);
42834
+ process.exitCode = result.status || 1;
42835
+ return undefined;
42721
42836
  }
42722
- function run(value) {
42837
+ async function run(value) {
42723
42838
  const env = { ...process.env, GH_TOKEN: value, GITHUB_TOKEN: value, GIT_TERMINAL_PROMPT: '0' };
42724
- return spawnSync(config.command, process.argv.slice(2), { env, encoding: 'buffer', stdio: ['inherit', 'pipe', 'pipe'] });
42725
- }
42726
- let result = run(token());
42727
- const diagnostic = Buffer.concat([result.stdout || Buffer.alloc(0), result.stderr || Buffer.alloc(0)]).toString('utf8');
42728
- if (result.status !== 0 && authFailure.test(diagnostic)) result = run(token());
42729
- if (result.stdout) process.stdout.write(result.stdout);
42730
- if (result.stderr) process.stderr.write(result.stderr);
42731
- if (result.error) throw result.error;
42732
- process.exit(result.status ?? 1);
42839
+ const child = spawn(config.command, process.argv.slice(2), { env, stdio: ['inherit', 'pipe', 'pipe'] });
42840
+ let authenticationFailed = false;
42841
+ // Keep only enough diagnostic overlap to recognize an auth failure across chunks.
42842
+ for (const [source, destination] of [[child.stdout, process.stdout], [child.stderr, process.stderr]]) {
42843
+ let tail = '';
42844
+ source.on('data', (chunk) => {
42845
+ const diagnostic = tail + chunk.toString('utf8');
42846
+ authenticationFailed ||= authFailure.test(diagnostic);
42847
+ tail = diagnostic.slice(-1024);
42848
+ });
42849
+ source.pipe(destination, { end: false });
42850
+ }
42851
+ return new Promise((resolve, reject) => {
42852
+ child.once('error', reject);
42853
+ child.once('close', (status) => resolve({ status, authenticationFailed }));
42854
+ });
42855
+ }
42856
+ if (!refusal) {
42857
+ const authenticated = needsToken(process.argv.slice(2));
42858
+ const value = authenticated ? token() : '';
42859
+ if (value !== undefined) {
42860
+ let result = await run(value);
42861
+ if (authenticated && result.status !== 0 && result.authenticationFailed) {
42862
+ const refreshed = token();
42863
+ if (refreshed !== undefined) result = await run(refreshed);
42864
+ }
42865
+ process.exitCode ??= result.status ?? 1;
42866
+ }
42867
+ }
42733
42868
  `;
42734
42869
  await writeFile19(path, source, { mode: 448 });
42735
42870
  await chmod9(path, 448);
@@ -43327,7 +43462,10 @@ var MonolithCornerTurnLoop = class {
43327
43462
  generationId: call.generationId,
43328
43463
  target: "squire",
43329
43464
  ...resourceCallFacts({ method: "tools/call", params: { name: call.tool, arguments: call.args } }, "squire")
43330
- })).allowed, options.config.operatorHome ?? homedir17());
43465
+ })).allowed, options.config.operatorHome ?? homedir18(), void 0, (decision2) => {
43466
+ options.api.execute("postSquireApprovalDecision", { roomId: options.cornerId, ...decision2 }).catch(() => {
43467
+ });
43468
+ });
43331
43469
  this.options = { ...options, api: this.commandContext.bind(options.api) };
43332
43470
  options.grantRunner?.register(options.cornerId, {
43333
43471
  workspaceId: options.workspaceId,
@@ -43542,7 +43680,7 @@ var MonolithCornerTurnLoop = class {
43542
43680
  effort: configuration.effort ?? this.options.config.modelSelection?.effort,
43543
43681
  fastMode: configuration.fastMode
43544
43682
  };
43545
- const operatorHome = this.options.config.operatorHome ?? homedir17();
43683
+ const operatorHome = this.options.config.operatorHome ?? homedir18();
43546
43684
  const registryHostDeclarations = registryMcpHostDeclarations(configuration.registryMcpRoutes, this.commandContext.path, this.options.config.registryMcpBrokerSocket);
43547
43685
  const mountedHostRoutes = [...grantedHostRoutes, ...Object.keys(registryHostDeclarations)];
43548
43686
  const squireScope = {
@@ -43755,6 +43893,7 @@ var MonolithCornerTurnLoop = class {
43755
43893
  agentCommand: harnessLabel,
43756
43894
  piHome: agentEnv.PI_CODING_AGENT_DIR,
43757
43895
  piCommand: agentEnv.PI_ACP_PI_COMMAND ?? process.env.PI_ACP_PI_COMMAND,
43896
+ agentEnv,
43758
43897
  servers: [...servers, ...grantedRouteServers]
43759
43898
  });
43760
43899
  const persona = configuration.soul ?? self?.soul;
@@ -47173,7 +47312,7 @@ async function runAgentRuntime(configPath, host, signal) {
47173
47312
  return exitReason ?? lifecycle.stopReason ?? "stopped";
47174
47313
  }
47175
47314
  function registryBrokerSocketPath(home, agentId) {
47176
- return join19(home ?? homedir18(), ".beeline", "registry-mcp-broker", `${process.pid}-${agentId.slice(0, 8)}.sock`);
47315
+ return join19(home ?? homedir19(), ".beeline", "registry-mcp-broker", `${process.pid}-${agentId.slice(0, 8)}.sock`);
47177
47316
  }
47178
47317
  function interruptHostedAgent(agent) {
47179
47318
  agent.lifecycle.quiesce("force-update");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usebeeline",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Connect an AI coding agent to Beeline with one command.",
5
5
  "homepage": "https://usebeeline.app",
6
6
  "bugs": {