@ordewell/core 0.4.3 → 0.4.5

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/dist/index.mjs CHANGED
@@ -9890,8 +9890,12 @@ var Block = /* @__PURE__ */ ((Block2) => {
9890
9890
  Block2[Block2["IfInteractive"] = 7] = "IfInteractive";
9891
9891
  Block2[Block2["IfHeadlessSession"] = 8] = "IfHeadlessSession";
9892
9892
  Block2[Block2["IfInteractiveVariant"] = 9] = "IfInteractiveVariant";
9893
+ Block2[Block2["IfProjectTrust"] = 10] = "IfProjectTrust";
9893
9894
  return Block2;
9894
9895
  })(Block || {});
9896
+ function isInteractive(ctx) {
9897
+ return ctx.interactive ?? !ctx.headless;
9898
+ }
9895
9899
  function resolveArgs(manifest, ctx) {
9896
9900
  const args = [];
9897
9901
  const env = {};
@@ -9960,6 +9964,8 @@ function parseBlockOpen(raw) {
9960
9964
  return 8 /* IfHeadlessSession */;
9961
9965
  case "{{if interactiveVariant}}":
9962
9966
  return 9 /* IfInteractiveVariant */;
9967
+ case "{{if projectTrust}}":
9968
+ return 10 /* IfProjectTrust */;
9963
9969
  default:
9964
9970
  return null;
9965
9971
  }
@@ -9981,19 +9987,24 @@ function shouldIncludeBlock(block, ctx) {
9981
9987
  // `--variant` is a headless-only flag: it exists on `opencode run` but not
9982
9988
  // on the interactive TUI, which exits immediately if handed an unknown flag.
9983
9989
  case 6 /* IfVariant */:
9984
- return !!ctx.thinkingEffort && !!ctx.headless;
9990
+ return !!ctx.thinkingEffort && !isInteractive(ctx);
9985
9991
  // Session-shape blocks: unlike IfHeadless (a permission-flag gate with a
9986
9992
  // plan-mode carve-out), these pick the CLI invocation shape — e.g.
9987
9993
  // opencode's `run` subcommand vs its interactive TUI — so no mode carve-out.
9988
9994
  case 7 /* IfInteractive */:
9989
- return !ctx.headless;
9995
+ return isInteractive(ctx);
9990
9996
  case 8 /* IfHeadlessSession */:
9991
- return !!ctx.headless;
9997
+ return !isInteractive(ctx);
9992
9998
  // Interactive counterpart of IfVariant, for runners whose TUI has no
9993
9999
  // variant flag and must receive the variant via env config instead.
9994
10000
  // Requires a provider-prefixed model: the config pins variant per model.
9995
10001
  case 9 /* IfInteractiveVariant */:
9996
- return !!ctx.thinkingEffort && !!ctx.model?.includes("/") && !ctx.headless;
10002
+ return !!ctx.thinkingEffort && !!ctx.model?.includes("/") && isInteractive(ctx);
10003
+ // Guards the flag/value pair that pre-trusts the workspace directory. The
10004
+ // condition, not the token, is what keeps a bare `-c` out of the args when
10005
+ // there is no cwd to name.
10006
+ case 10 /* IfProjectTrust */:
10007
+ return isInteractive(ctx) && !!ctx.cwd;
9997
10008
  default:
9998
10009
  return true;
9999
10010
  }
@@ -10014,6 +10025,10 @@ function resolveToken(token, manifest, ctx) {
10014
10025
  if (token === "{{model}}") return ctx.model || "";
10015
10026
  if (token === "{{thinkingEffort}}") return ctx.thinkingEffort || "";
10016
10027
  if (token === "{{mode}}") return ctx.mode || "build";
10028
+ if (token === "{{projectTrust}}") {
10029
+ if (!ctx.cwd) return "";
10030
+ return `projects.${JSON.stringify(ctx.cwd)}.trust_level="trusted"`;
10031
+ }
10017
10032
  if (token === "{{feature:thinking}}") {
10018
10033
  return manifest.features.thinkingFlag || "";
10019
10034
  }
@@ -10100,7 +10115,9 @@ function buildRunnerInvocation(opts) {
10100
10115
  thinkingEffort: opts.thinkingEffort,
10101
10116
  modelVariants: opts.modelVariants,
10102
10117
  mode: opts.mode ?? "build",
10103
- headless: opts.headless
10118
+ headless: opts.headless,
10119
+ interactive: opts.interactive,
10120
+ cwd: opts.cwd
10104
10121
  });
10105
10122
  }
10106
10123
 
@@ -10221,8 +10238,14 @@ var HeadlessRunner = class extends AbstractRunner {
10221
10238
  hasScriptCmd;
10222
10239
  resolvePath;
10223
10240
  launchDeps;
10224
- /** Session shape when the caller names none; the VS Code runner overrides it to interactive. */
10225
- defaultHeadless = true;
10241
+ /**
10242
+ * Session shape: a piped subprocess is not a terminal, so runners get their
10243
+ * non-interactive subcommand. The VS Code runner owns a pseudoterminal and
10244
+ * overrides this to true. Autonomy is a separate axis (see `ResolveContext`)
10245
+ * and stays on either way — no surface has a human answering permission
10246
+ * prompts on the orchestrator's behalf.
10247
+ */
10248
+ defaultInteractive = false;
10226
10249
  constructor(deps = {}) {
10227
10250
  super();
10228
10251
  this.spawnImpl = deps.spawnImpl ?? nodeSpawn2;
@@ -10235,7 +10258,7 @@ var HeadlessRunner = class extends AbstractRunner {
10235
10258
  }
10236
10259
  /** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
10237
10260
  async prepareLaunch(opts) {
10238
- const headless = opts.headless ?? this.defaultHeadless;
10261
+ const interactive = this.defaultInteractive;
10239
10262
  const invocation = buildRunnerInvocation({
10240
10263
  runner: opts.runner,
10241
10264
  prompt: opts.prompt,
@@ -10243,11 +10266,13 @@ var HeadlessRunner = class extends AbstractRunner {
10243
10266
  thinkingEffort: opts.thinkingEffort,
10244
10267
  modelVariants: opts.modelVariants,
10245
10268
  mode: opts.mode,
10246
- headless,
10269
+ headless: opts.headless ?? true,
10270
+ interactive,
10271
+ cwd: opts.cwd,
10247
10272
  registry: opts.registry
10248
10273
  });
10249
10274
  const manifest = opts.registry?.get(opts.runner)?.manifest;
10250
- const pty = (manifest?.runner.requiresTty === true || !headless) && this.hasScriptCmd();
10275
+ const pty = (manifest?.runner.requiresTty === true || interactive) && this.hasScriptCmd();
10251
10276
  const resolvedPath = await this.resolvePath();
10252
10277
  const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args) : { command: invocation.command, args: invocation.args };
10253
10278
  return {
@@ -10300,6 +10325,28 @@ function hasTmux(probe = defaultProbe) {
10300
10325
  return false;
10301
10326
  }
10302
10327
  }
10328
+ var defaultHasBin = (bin) => {
10329
+ try {
10330
+ execSync2(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
10331
+ return true;
10332
+ } catch {
10333
+ return false;
10334
+ }
10335
+ };
10336
+ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platform, env = process.env) {
10337
+ const candidates = platform === "darwin" ? [["pbcopy", "pbcopy"]] : platform === "win32" ? [["clip.exe", "clip.exe"]] : [
10338
+ // Only prefer wl-copy when there is a Wayland display to talk to —
10339
+ // on X11 it is often installed but fails at copy time.
10340
+ ...env.WAYLAND_DISPLAY ? [["wl-copy", "wl-copy"]] : [],
10341
+ ["xclip", "xclip -selection clipboard"],
10342
+ ["xsel", "xsel --clipboard --input"],
10343
+ ["wl-copy", "wl-copy"]
10344
+ ];
10345
+ for (const [bin, command] of candidates) {
10346
+ if (hasBin(bin)) return command;
10347
+ }
10348
+ return null;
10349
+ }
10303
10350
 
10304
10351
  // src/services/TmuxRunner.ts
10305
10352
  var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
@@ -10409,6 +10456,7 @@ var TmuxRunner = class extends AbstractRunner {
10409
10456
  resolvePath;
10410
10457
  pollIntervalMs;
10411
10458
  logDir;
10459
+ clipboardCommand;
10412
10460
  /** Retries get a freshly named window so a failed attempt's output stays inspectable. */
10413
10461
  attempts = /* @__PURE__ */ new Map();
10414
10462
  ready = null;
@@ -10420,6 +10468,7 @@ var TmuxRunner = class extends AbstractRunner {
10420
10468
  this.resolvePath = deps.resolvePath ?? augmentedPath;
10421
10469
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
10422
10470
  this.logDir = deps.logDir ?? tmpdir();
10471
+ this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
10423
10472
  }
10424
10473
  /**
10425
10474
  * Reaps a session orphaned by a crashed prior daemon on the same port, then
@@ -10448,25 +10497,44 @@ var TmuxRunner = class extends AbstractRunner {
10448
10497
  await this.configureScrolling();
10449
10498
  }
10450
10499
  /**
10451
- * Makes an attached runner terminal scrollable with the mouse wheel and
10452
- * Page Up/Down. A fresh tmux session ships with `mouse off`, a 2000-line
10453
- * scrollback, and no PageUp binding — none of which lets a user page back
10454
- * through a finished task's output, which is the whole point of opening its
10455
- * window. Scoping is harmless: the daemon owns a private socket
10456
- * (`tmuxSocketName`), so these server-wide options touch nothing but its
10457
- * own session. Best-effort so an ancient or stripped tmux cannot stop tasks
10458
- * from spawning — the session still works without the scroll comforts.
10500
+ * Gives an attached runner terminal wheel/PageUp scrolling over a deep
10501
+ * scrollback, and a mouse selection that reaches the system clipboard — none
10502
+ * of which a default tmux session does. Server-wide scoping is harmless: the
10503
+ * daemon owns a private socket (`tmuxSocketName`).
10504
+ *
10505
+ * `mouse on` is what costs the emulator's own drag-select, so tmux must hand
10506
+ * the selection back out itself: via a clipboard binary when there is one,
10507
+ * and via OSC 52 (`set-clipboard`) for what that cannot cover, such as
10508
+ * viewing over SSH. Naming the binary on the binding as well as in
10509
+ * `copy-command` keeps drag-to-copy working on tmux older than 3.2, which has
10510
+ * no `copy-command`.
10459
10511
  */
10460
10512
  async configureScrolling() {
10461
- try {
10462
- await this.tmux(["set-option", "-g", "history-limit", "100000"]);
10463
- await this.tmux(["set-option", "-g", "mouse", "on"]);
10464
- await this.tmux(["bind-key", "-n", "PageUp", "copy-mode", "-u"]);
10465
- await this.tmux(["bind-key", "-T", "copy-mode", "PageUp", "send-keys", "-X", "page-up"]);
10466
- await this.tmux(["bind-key", "-T", "copy-mode", "PageDown", "send-keys", "-X", "page-down"]);
10467
- await this.tmux(["bind-key", "-T", "copy-mode-vi", "PageUp", "send-keys", "-X", "page-up"]);
10468
- await this.tmux(["bind-key", "-T", "copy-mode-vi", "PageDown", "send-keys", "-X", "page-down"]);
10469
- } catch {
10513
+ const clip = this.clipboardCommand();
10514
+ const copySelection = clip ? ["copy-pipe-and-cancel", clip] : ["copy-pipe-and-cancel"];
10515
+ const commands = [
10516
+ // `history-limit` only governs windows created after it is set, so it
10517
+ // must run before any `new-window` — once, here at session creation.
10518
+ ["set-option", "-g", "history-limit", "100000"],
10519
+ ["set-option", "-g", "mouse", "on"],
10520
+ ["set-option", "-g", "set-clipboard", "on"],
10521
+ ...clip ? [["set-option", "-g", "copy-command", clip]] : [],
10522
+ ["bind-key", "-T", "copy-mode", "MouseDragEnd1Pane", "send-keys", "-X", ...copySelection],
10523
+ ["bind-key", "-T", "copy-mode-vi", "MouseDragEnd1Pane", "send-keys", "-X", ...copySelection],
10524
+ // Root-table PageUp; otherwise the key goes to the inner app and never
10525
+ // reaches tmux's scrollback. Both mode tables, so `mode-keys` can't
10526
+ // decide whether paging works.
10527
+ ["bind-key", "-n", "PageUp", "copy-mode", "-u"],
10528
+ ["bind-key", "-T", "copy-mode", "PageUp", "send-keys", "-X", "page-up"],
10529
+ ["bind-key", "-T", "copy-mode", "PageDown", "send-keys", "-X", "page-down"],
10530
+ ["bind-key", "-T", "copy-mode-vi", "PageUp", "send-keys", "-X", "page-up"],
10531
+ ["bind-key", "-T", "copy-mode-vi", "PageDown", "send-keys", "-X", "page-down"]
10532
+ ];
10533
+ for (const args of commands) {
10534
+ try {
10535
+ await this.tmux(args);
10536
+ } catch {
10537
+ }
10470
10538
  }
10471
10539
  }
10472
10540
  /**
@@ -10491,7 +10559,14 @@ var TmuxRunner = class extends AbstractRunner {
10491
10559
  thinkingEffort: opts.thinkingEffort,
10492
10560
  modelVariants: opts.modelVariants,
10493
10561
  mode: opts.mode,
10562
+ // A tmux window is a real TTY the user can attach to (ADR-0007), so the
10563
+ // runner's own TUI is the point — `interactive`. It is still an
10564
+ // unattended run, so autonomy stays on: the two are separate axes, and
10565
+ // conflating them is what made Codex tasks stream `codex exec` output
10566
+ // into a window that was supposed to show its TUI.
10494
10567
  headless: opts.headless ?? true,
10568
+ interactive: true,
10569
+ cwd: opts.cwd,
10495
10570
  registry: opts.registry
10496
10571
  });
10497
10572
  await this.ensureSession();
@@ -10729,18 +10804,33 @@ var CODEX_MANIFEST = {
10729
10804
  version: "1.0.0",
10730
10805
  runner: {
10731
10806
  command: "codex",
10732
- // Headless surfaces (CLI/web) use the non-interactive `exec` subcommand
10733
- // (`--skip-git-repo-check`: task workspaces aren't always git repos, and
10734
- // exec hard-fails outside one by default); interactive surfaces (VS Code
10735
- // terminal) launch the TUI with the prompt as its positional argument.
10736
- // Reasoning effort has no dedicated flag — it travels as a `-c` config
10737
- // override (see {{feature:reasoningEffortConfig}}). Both `exec` and the
10738
- // TUI accept `-m`, `-c`, and `--sandbox`.
10807
+ // Headless surfaces (a piped subprocess) use the non-interactive `exec`
10808
+ // subcommand (`--skip-git-repo-check`: task workspaces aren't always git
10809
+ // repos, and exec hard-fails outside one by default); interactive surfaces
10810
+ // (a tmux window, the VS Code terminal) launch the TUI with the prompt as
10811
+ // its positional argument. Reasoning effort has no dedicated flag — it
10812
+ // travels as a `-c` config override (see {{feature:reasoningEffortConfig}}).
10813
+ // Both `exec` and the TUI accept `-m`, `-c`, and `--sandbox`.
10814
+ //
10815
+ // The two interactive-only entries restore what `exec` gives implicitly and
10816
+ // the TUI does not: `exec` is non-interactive, so it can neither ask for
10817
+ // command approval nor ask whether the directory is trusted. The TUI asks
10818
+ // for both by default, and an orchestrated task has nobody to answer —
10819
+ // without these it stalls on a menu instead of working. `-a` does not exist
10820
+ // on `exec`, hence the shape gate rather than a `{{if headless}}` one.
10739
10821
  argsTemplate: [
10740
10822
  "{{if headlessSession}}",
10741
10823
  "exec",
10742
10824
  "--skip-git-repo-check",
10743
10825
  "{{/if}}",
10826
+ "{{if interactive}}",
10827
+ "-a",
10828
+ "never",
10829
+ "{{/if}}",
10830
+ "{{if projectTrust}}",
10831
+ "-c",
10832
+ "{{projectTrust}}",
10833
+ "{{/if}}",
10744
10834
  "{{if model}}",
10745
10835
  "-m",
10746
10836
  "{{model}}",
@@ -10760,9 +10850,9 @@ var CODEX_MANIFEST = {
10760
10850
  planMode: true,
10761
10851
  planModeFlag: "--sandbox",
10762
10852
  permissionModeValues: {
10763
- // Map mode IDs to --sandbox CLI values. `codex exec` is non-interactive
10764
- // and has no approval flag, so the sandbox axis is the whole permission
10765
- // story for headless runs.
10853
+ // Map mode IDs to --sandbox CLI values. Approvals are off on both shapes
10854
+ // (implicitly under `exec`, via `-a never` in the TUI), so the sandbox
10855
+ // axis is the whole permission story for a Codex task.
10766
10856
  "agent": "workspace-write",
10767
10857
  "plan": "read-only",
10768
10858
  "fullAccess": "danger-full-access",
@@ -11369,6 +11459,7 @@ export {
11369
11459
  clearAugmentedPathCache,
11370
11460
  clearResearchShellCache,
11371
11461
  clearState,
11462
+ clipboardCopyCommand,
11372
11463
  coerceAssignments,
11373
11464
  collectDirectDependencyOutputs,
11374
11465
  collectProviderCredentials,