@scalequality/cli 0.4.10 → 0.4.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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "c24a69671fea8929ec550bbb8672d703980af3c6389cf97f878cc71083b4972c",
4
- "sourceCommit": "011350388f19c1ce50250458dd2810efa3113e84"
3
+ "sha256": "c2e9fefe31fcd4f9b46e4f5323336571b20380b6756c90b494584c1727d9f787",
4
+ "sourceCommit": "6437124ad656162948b2c83adfd87e5afc2d82a7"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -1290,13 +1290,14 @@ async function decideToolUse(toolName, input, ctx) {
1290
1290
  }
1291
1291
 
1292
1292
  // src/application/services/workspaceSandbox/protections.ts
1293
- var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null });
1293
+ var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null, toolApproval: false });
1294
1294
  var KNOWN_RULES = {
1295
1295
  "secret-files": ["block"],
1296
1296
  "protected-push": ["require"],
1297
1297
  "deploy-approval": ["require"],
1298
1298
  "destructive-commands": ["require"],
1299
- "unknown-package": ["block", "warn"]
1299
+ "unknown-package": ["block", "warn"],
1300
+ "tool-approval": ["require"]
1300
1301
  };
1301
1302
  function protectionsWireOf(raw) {
1302
1303
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
@@ -1318,7 +1319,8 @@ function parseProtections(raw) {
1318
1319
  protectedPush: r["protected-push"] === "require",
1319
1320
  deployApproval: r["deploy-approval"] === "require",
1320
1321
  destructiveCommands: r["destructive-commands"] === "require",
1321
- unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null
1322
+ unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null,
1323
+ toolApproval: r["tool-approval"] === "require"
1322
1324
  };
1323
1325
  }
1324
1326
  var SECRET_FILE_MESSAGE = "Blocked by your organization's protection: secret files are never sent to the model. Do not try to read this file another way; ask the user for the specific value your task needs, or use the example file (such as .env.example).";
@@ -1857,6 +1859,7 @@ var RegistryChecker = class {
1857
1859
  }
1858
1860
  };
1859
1861
  function mayNeedProtection(command, p) {
1862
+ if (p.toolApproval) return true;
1860
1863
  if (p.destructiveCommands && isDestructiveCommand(command)) return true;
1861
1864
  if (p.deployApproval && isDeployCommand(command)) return true;
1862
1865
  if (p.protectedPush && gitPushes(command).length > 0) return true;
@@ -1889,6 +1892,7 @@ async function protectionVerdict(command, p, deps) {
1889
1892
  if (p.destructiveCommands && isDestructiveCommand(command)) reasons.push("PROTECTION_DESTRUCTIVE_COMMAND");
1890
1893
  if (p.deployApproval && isDeployCommand(command)) reasons.push("PROTECTION_DEPLOY");
1891
1894
  if (p.protectedPush && await isProtectedPush(command, deps.currentBranch)) reasons.push("PROTECTION_PROTECTED_PUSH");
1895
+ if (p.toolApproval && !reasons.length) reasons.push("PROTECTION_TOOL_APPROVAL");
1892
1896
  if (!reasons.length) return { kind: "allow" };
1893
1897
  return { kind: "ask", reasons, ...pkg ? { package: pkg.name, packageReason: pkg.reason } : {} };
1894
1898
  }
@@ -1896,7 +1900,12 @@ var PROTECTION_MESSAGES = {
1896
1900
  rejected: "Your organization's protection requires a person to approve this command, and the user denied it. It did not run. Do not run it again or another way unless the user asks.",
1897
1901
  timedOut: (minutes) => `Your organization's protection requires a person to approve this command, and nobody answered within ${minutes} minutes. It did not run. Ask the user before trying it again.`,
1898
1902
  stopped: "The request was stopped before the command ran.",
1899
- unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way."
1903
+ unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way.",
1904
+ // A file change under tool-approval.
1905
+ editRejected: (path) => `Your organization's protection requires a person to approve every change, and the user denied this change to ${path}. The file was not changed. Do not change it another way unless the user asks.`,
1906
+ editTimedOut: (path, minutes) => `Your organization's protection requires a person to approve every change, and nobody answered about ${path} within ${minutes} minutes. The file was not changed. Ask the user before trying again.`,
1907
+ editStopped: "The request was stopped before the file was changed.",
1908
+ editUnavailable: (path) => `Your organization's protection requires a person to approve every change, and the approval for ${path} could not be requested. The file was not changed. Tell the user; do not change it another way.`
1900
1909
  };
1901
1910
 
1902
1911
  // src/application/services/workspaceSandbox/SessionTransport.ts
@@ -3031,6 +3040,15 @@ var BrowserCommandGate = class {
3031
3040
  this.chain = next.catch(() => void 0);
3032
3041
  return next;
3033
3042
  }
3043
+ checkProtectedEdit(path, opts = {}) {
3044
+ const next = this.chain.then(() => this.ask(`edit ${path}`, {
3045
+ signal: opts.signal,
3046
+ edit: { path, ...opts.preview ? { preview: opts.preview } : {} },
3047
+ protection: { reasons: ["PROTECTION_TOOL_APPROVAL"] }
3048
+ }));
3049
+ this.chain = next.catch(() => void 0);
3050
+ return next;
3051
+ }
3034
3052
  checkEdit(path, opts = {}) {
3035
3053
  const next = this.chain.then(() => this.ask(`edit ${path}`, { signal: opts.signal, edit: { path, ...opts.preview ? { preview: opts.preview } : {} } }));
3036
3054
  this.chain = next.catch(() => void 0);
@@ -3047,7 +3065,7 @@ var BrowserCommandGate = class {
3047
3065
  command,
3048
3066
  cwd: this.o.root,
3049
3067
  ...opts.description ? { description: opts.description.slice(0, 1e3) } : {},
3050
- reasons: opts.edit ? [] : protection ? [...protection.reasons, ...approvalReasons(command)] : approvalReasons(command),
3068
+ reasons: protection ? [...protection.reasons, ...opts.edit ? [] : approvalReasons(command)] : opts.edit ? [] : approvalReasons(command),
3051
3069
  suggestedRule: opts.edit || protection ? null : suggestRule(command),
3052
3070
  ...opts.edit ? { edit: { path: opts.edit.path, ...opts.edit.preview ? { preview: opts.edit.preview.slice(0, 4e3) } : {} } } : {},
3053
3071
  ...protection?.package ? { package: protection.package, ...protection.packageReason ? { packageReason: protection.packageReason } : {} } : {}
@@ -3055,6 +3073,7 @@ var BrowserCommandGate = class {
3055
3073
  } catch (e) {
3056
3074
  this.o.log?.(`command approval could not be requested: ${e.message}`);
3057
3075
  const code = e instanceof CommandApprovalError ? e.code : null;
3076
+ if (protection && opts.edit) return { allow: false, message: PROTECTION_MESSAGES.editUnavailable(opts.edit.path) };
3058
3077
  if (protection && code !== "COMMAND_NOT_ALLOWED") return { allow: false, message: PROTECTION_MESSAGES.unavailable };
3059
3078
  return {
3060
3079
  allow: false,
@@ -3072,10 +3091,12 @@ var BrowserCommandGate = class {
3072
3091
  if (!decision) {
3073
3092
  const reason = opts.signal?.aborted ? "STOPPED" : "TIMED_OUT";
3074
3093
  void this.o.api.cancel(approvalId, reason).catch(() => void 0);
3094
+ if (protection && opts.edit) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.editTimedOut(opts.edit.path, Math.round(timeoutMs / 6e4)) };
3075
3095
  if (protection) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
3076
3096
  return reason === "STOPPED" ? { allow: false, message: STOPPED } : { allow: false, message: `The user did not answer the approval for this command within ${Math.round(timeoutMs / 6e4)} minutes, so it did not run. Ask the user before trying it again.` };
3077
3097
  }
3078
3098
  if (decision.decision === "REJECT") {
3099
+ if (protection && opts.edit) return { allow: false, message: PROTECTION_MESSAGES.editRejected(opts.edit.path) };
3079
3100
  if (protection) return { allow: false, message: PROTECTION_MESSAGES.rejected };
3080
3101
  return opts.edit ? { allow: false, message: `The user declined this change to ${opts.edit.path} in the browser. The file was not changed. Ask the user what to do differently.` } : { allow: false, message: "The user denied this command in the browser. It did not run. Do not run the same command again unless the user asks; continue another way or ask the user." };
3081
3102
  }
@@ -4821,6 +4842,8 @@ function sandboxTestEnv() {
4821
4842
  "BUNDLE_PATH",
4822
4843
  "PUB_CACHE",
4823
4844
  "FLUTTER_ROOT",
4845
+ "MIX_HOME",
4846
+ "HEX_HOME",
4824
4847
  "MISE_DATA_DIR",
4825
4848
  "MISE_CACHE_DIR",
4826
4849
  "ELIXIR_ERL_OPTIONS"
@@ -4943,6 +4966,8 @@ var ENGINE_MESSAGES = {
4943
4966
  // Pull requests.
4944
4967
  BASE_ADVANCED: "The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.",
4945
4968
  PR_FAILED: "The pull request could not be opened.",
4969
+ PROVIDER_CONNECTION_REQUIRED: "The pull request could not be opened: the provider connection of {repoFullName} needs to be reconnected in ScaleQuality.",
4970
+ REPOSITORY_NOT_IN_PROJECT: "The pull request could not be opened: {repoFullName} is no longer a repository of this project.",
4946
4971
  // Attachments and mentions.
4947
4972
  ATTACHMENT_SKIPPED: "An attached image could not be read and was left out.",
4948
4973
  MENTION_SKIPPED: "{path} could not be attached ({reason}).",
@@ -10000,12 +10025,16 @@ var TurnTiming = class {
10000
10025
  // src/application/services/workspaceSandbox/protectionApproval.ts
10001
10026
  function gateProtectionApprover(gate) {
10002
10027
  if (!gate.checkProtected) return null;
10003
- return { ask: (command, check) => gate.checkProtected(command, check) };
10028
+ return {
10029
+ ask: (command, check) => gate.checkProtected(command, check),
10030
+ ...gate.checkProtectedEdit ? { askEdit: (path, check) => gate.checkProtectedEdit(path, check) } : {}
10031
+ };
10004
10032
  }
10005
10033
  function cloudProtectionApprover(o) {
10006
10034
  let chain = Promise.resolve();
10007
10035
  const ask = async (command, check) => {
10008
- if (check.signal?.aborted) return { allow: false, message: PROTECTION_MESSAGES.stopped };
10036
+ const edit = check.edit;
10037
+ if (check.signal?.aborted) return { allow: false, message: edit ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.stopped };
10009
10038
  let approvalId;
10010
10039
  try {
10011
10040
  ({ approvalId } = await o.calls.requestCommandApproval({
@@ -10014,11 +10043,12 @@ function cloudProtectionApprover(o) {
10014
10043
  ...check.description ? { description: check.description.slice(0, 1e3) } : {},
10015
10044
  reasons: [...check.reasons],
10016
10045
  suggestedRule: null,
10046
+ ...edit ? { edit: { path: edit.path, ...edit.preview ? { preview: edit.preview.slice(0, 4e3) } : {} } } : {},
10017
10047
  ...check.package ? { package: check.package, ...check.packageReason ? { packageReason: check.packageReason } : {} } : {}
10018
10048
  }));
10019
10049
  } catch (e) {
10020
10050
  o.log?.(`protection approval could not be requested: ${e.message}`);
10021
- return { allow: false, message: PROTECTION_MESSAGES.unavailable };
10051
+ return { allow: false, message: edit ? PROTECTION_MESSAGES.editUnavailable(edit.path) : PROTECTION_MESSAGES.unavailable };
10022
10052
  }
10023
10053
  const timeoutMs = o.timeoutMs ?? COMMAND_APPROVAL_TIMEOUT_MS;
10024
10054
  const timer = new AbortController();
@@ -10029,16 +10059,21 @@ function cloudProtectionApprover(o) {
10029
10059
  if (!decision) {
10030
10060
  const reason = check.signal?.aborted ? "STOPPED" : "TIMED_OUT";
10031
10061
  void o.calls.cancelCommandApproval(approvalId, reason).catch(() => void 0);
10032
- return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
10062
+ const minutes = Math.round(timeoutMs / 6e4);
10063
+ if (edit) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.editTimedOut(edit.path, minutes) };
10064
+ return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(minutes) };
10033
10065
  }
10034
- return decision.decision === "APPROVE" ? { allow: true } : { allow: false, message: PROTECTION_MESSAGES.rejected };
10066
+ if (decision.decision === "APPROVE") return { allow: true };
10067
+ return { allow: false, message: edit ? PROTECTION_MESSAGES.editRejected(edit.path) : PROTECTION_MESSAGES.rejected };
10068
+ };
10069
+ const queued = (run2) => {
10070
+ const next = chain.then(run2);
10071
+ chain = next.catch(() => void 0);
10072
+ return next;
10035
10073
  };
10036
10074
  return {
10037
- ask(command, check) {
10038
- const next = chain.then(() => ask(command, check));
10039
- chain = next.catch(() => void 0);
10040
- return next;
10041
- }
10075
+ ask: (command, check) => queued(() => ask(command, check)),
10076
+ askEdit: (path, check) => queued(() => ask(`edit ${path}`, { reasons: ["PROTECTION_TOOL_APPROVAL"], signal: check.signal, edit: { path, ...check.preview ? { preview: check.preview } : {} } }))
10042
10077
  };
10043
10078
  }
10044
10079
 
@@ -13150,16 +13185,33 @@ _Measured on an earlier version of this change._`);
13150
13185
  if (e instanceof TransportError && e.code === REPOSITORY_NOT_IN_SCOPE) {
13151
13186
  return text(`${REPOSITORY_NOT_IN_SCOPE}: ${target} left this session's scope before the pull request was opened. It was not opened.`, true);
13152
13187
  }
13153
- if (status === 409) {
13154
- const where = this.local ? " Update this folder to the latest commit of the base branch (git pull), then ask again." : "";
13155
- this.error("BASE_ADVANCED", { repoFullName: target, local: this.local });
13156
- return text(`The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.${where} Tell the user; do not retry without being asked.`, true);
13157
- }
13158
- this.error("PR_FAILED", { repoFullName: target });
13159
- return text("The pull request could not be opened. Tell the user; do not retry without being asked.", true);
13188
+ const failure = pullRequestFailure(status, e instanceof TransportError ? e.code : void 0, target, this.local);
13189
+ this.error(failure.code, failure.params);
13190
+ return text(`${failure.say} Tell the user; do not retry without being asked.`, true);
13160
13191
  }
13161
13192
  }
13162
13193
  };
13194
+ function pullRequestFailure(status, code, repoFullName, local) {
13195
+ if (status === 409 && code === "PROVIDER_CONNECTION_REQUIRED") return {
13196
+ code: "PROVIDER_CONNECTION_REQUIRED",
13197
+ params: { repoFullName },
13198
+ say: `The pull request could not be opened: the provider connection of ${repoFullName} needs to be reconnected in ScaleQuality.`
13199
+ };
13200
+ if (status === 409 && code === "REPOSITORY_NOT_IN_PROJECT") return {
13201
+ code: "REPOSITORY_NOT_IN_PROJECT",
13202
+ params: { repoFullName },
13203
+ say: `The pull request could not be opened: ${repoFullName} is no longer a repository of this project.`
13204
+ };
13205
+ if (status === 409 && (!code || code === "BASE_ADVANCED")) {
13206
+ const where = local ? " Update this folder to the latest commit of the base branch (git pull), then ask again." : "";
13207
+ return {
13208
+ code: "BASE_ADVANCED",
13209
+ params: { repoFullName, local },
13210
+ say: `The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.${where}`
13211
+ };
13212
+ }
13213
+ return { code: "PR_FAILED", params: { repoFullName, ...code ? { reason: code } : {} }, say: "The pull request could not be opened." };
13214
+ }
13163
13215
  var LOCAL_ENV_PASSTHROUGH = [
13164
13216
  "LOGNAME",
13165
13217
  "COLORTERM",
@@ -13312,11 +13364,11 @@ function buildQueryOptions(o) {
13312
13364
  }
13313
13365
  const protections = o.protections?.() ?? NO_PROTECTIONS;
13314
13366
  const d = await decideToolUse(name, input.tool_input ?? {}, { ...o.policy, protections });
13315
- const guarded = d.behavior === "allow" && name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections);
13367
+ const guarded = d.behavior === "allow" && (name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections) || protections.toolApproval && !o.plan && name in WRITE_TOOL_PATHS);
13316
13368
  const gated = guarded || d.behavior === "allow" && !!gate && (name === "Bash" || !!gate.checkEdit && !o.plan && name in WRITE_TOOL_PATHS);
13317
13369
  if (d.behavior === "allow" && !gated) await beforeWrite(name, d.updatedInput);
13318
13370
  if (gated) {
13319
- return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This command follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
13371
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This action follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
13320
13372
  }
13321
13373
  return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
13322
13374
  };
@@ -13354,10 +13406,18 @@ function buildQueryOptions(o) {
13354
13406
  protectionApproved = true;
13355
13407
  }
13356
13408
  }
13357
- if (gate?.checkEdit && !o.plan && toolName in WRITE_TOOL_PATHS) {
13409
+ const changes = !o.plan && toolName in WRITE_TOOL_PATHS;
13410
+ const changePath = () => {
13358
13411
  const field = WRITE_TOOL_PATHS[toolName];
13359
- const path = typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
13360
- const r = await gate.checkEdit(path, { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13412
+ return typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
13413
+ };
13414
+ if (changes && protections.toolApproval) {
13415
+ const path = changePath(), askEdit = o.protection?.approver?.askEdit;
13416
+ if (!askEdit) return { behavior: "deny", message: PROTECTION_MESSAGES.editUnavailable(path) };
13417
+ const r = await askEdit(path, { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13418
+ if (!r.allow) return { behavior: "deny", message: r.message };
13419
+ } else if (gate?.checkEdit && changes) {
13420
+ const r = await gate.checkEdit(changePath(), { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13361
13421
  if (!r.allow) return { behavior: "deny", message: r.message };
13362
13422
  }
13363
13423
  await beforeWrite(toolName, d.updatedInput);
@@ -13420,7 +13480,8 @@ function timedGate(gate, took) {
13420
13480
  return {
13421
13481
  check: (command, opts) => timed(() => gate.check(command, opts)),
13422
13482
  ...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {},
13423
- ...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {}
13483
+ ...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {},
13484
+ ...gate.checkProtectedEdit ? { checkProtectedEdit: (path, opts) => timed(() => gate.checkProtectedEdit(path, opts)) } : {}
13424
13485
  };
13425
13486
  }
13426
13487
  async function hasLocalTranscript(configDir, sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in any folder of your computer, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {