@4pm/cli 1.5.22 → 1.5.26

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/index.js +93 -31
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -33013,6 +33013,15 @@ var dispatchCommandRequestSchema = external_exports.object({
33013
33013
  * agent run (the Console tab + Git merge, which legitimately use tools).
33014
33014
  */
33015
33015
  aiOneShot: external_exports.boolean().optional(),
33016
+ /**
33017
+ * Read-only agent AI mode (ADR-0265): the prompt is a task that must **read + inspect the repo
33018
+ * but write nothing** (the project-template "Analyze impact"). Unlike `aiOneShot` it keeps the
33019
+ * read/search tools (`Read`/`Glob`/`Grep`/`Bash`) and runs multi-turn, but the cli blocks the
33020
+ * write/orchestration tools and runs claude under `--permission-mode plan` (codex `--sandbox
33021
+ * read-only`) so it can diff files without editing them. Only meaningful with `ai:true`;
33022
+ * **mutually exclusive with `aiOneShot`** (a run is one-shot | read-only | full-agent).
33023
+ */
33024
+ aiReadOnly: external_exports.boolean().optional(),
33016
33025
  /**
33017
33026
  * Per-run AI execution overrides (ADR-0261): model/thinking/temperature chosen in the web
33018
33027
  * "AI settings" modal (per-user localStorage), layered over the profile default by the cli.
@@ -33027,6 +33036,13 @@ var dispatchCommandRequestSchema = external_exports.object({
33027
33036
  message: "Provide exactly one of machineLinkId or pick."
33028
33037
  });
33029
33038
  }
33039
+ if (v.aiOneShot && v.aiReadOnly) {
33040
+ ctx.addIssue({
33041
+ code: external_exports.ZodIssueCode.custom,
33042
+ path: ["aiReadOnly"],
33043
+ message: "aiOneShot and aiReadOnly are mutually exclusive."
33044
+ });
33045
+ }
33030
33046
  if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
33031
33047
  ctx.addIssue({
33032
33048
  code: external_exports.ZodIssueCode.too_big,
@@ -33318,16 +33334,6 @@ var MEMO_MAX_LENGTH = 65536;
33318
33334
  var putMemoRequestSchema = external_exports.object({
33319
33335
  content: external_exports.string().max(MEMO_MAX_LENGTH)
33320
33336
  });
33321
- var MEMO_ITEM_MAX_LENGTH = 2e3;
33322
- var createMemoItemSchema = external_exports.object({
33323
- content: external_exports.string().trim().min(1).max(MEMO_ITEM_MAX_LENGTH)
33324
- });
33325
- var updateMemoItemSchema = external_exports.object({
33326
- content: external_exports.string().trim().min(1).max(MEMO_ITEM_MAX_LENGTH).optional(),
33327
- done: external_exports.boolean().optional()
33328
- }).refine((v) => v.content !== void 0 || v.done !== void 0, {
33329
- message: "content or done required"
33330
- });
33331
33337
  var HTTP_JSON_BODY_LIMIT_BYTES = 2 * 1048576;
33332
33338
  var putSpecDraftRequestSchema = external_exports.object({
33333
33339
  spec: external_exports.record(external_exports.unknown()),
@@ -34839,9 +34845,57 @@ var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
34839
34845
  "SendMessage",
34840
34846
  "PushNotification"
34841
34847
  ].join(",");
34848
+ var ONE_SHOT_MAX_TURNS = 4;
34842
34849
  function oneShotArgs(cmd) {
34843
34850
  if (!cmd.includes("claude")) return [];
34844
- return ["--max-turns=1", "--disallowedTools", ONE_SHOT_DISALLOWED_CLAUDE_TOOLS, "--permission-mode", "default"];
34851
+ return [
34852
+ `--max-turns=${ONE_SHOT_MAX_TURNS}`,
34853
+ "--disallowedTools",
34854
+ ONE_SHOT_DISALLOWED_CLAUDE_TOOLS,
34855
+ "--permission-mode",
34856
+ "default"
34857
+ ];
34858
+ }
34859
+ var READ_ONLY_DISALLOWED_CLAUDE_TOOLS = [
34860
+ "Edit",
34861
+ "Write",
34862
+ "NotebookEdit",
34863
+ "Task",
34864
+ "SlashCommand",
34865
+ "Skill",
34866
+ "WebSearch",
34867
+ "WebFetch",
34868
+ "TaskCreate",
34869
+ "TaskGet",
34870
+ "TaskList",
34871
+ "TaskOutput",
34872
+ "TaskStop",
34873
+ "TaskUpdate",
34874
+ "Monitor",
34875
+ "DesignSync",
34876
+ "CronCreate",
34877
+ "CronDelete",
34878
+ "CronList",
34879
+ "EnterWorktree",
34880
+ "ExitWorktree",
34881
+ "RemoteTrigger",
34882
+ "ScheduleWakeup",
34883
+ "SendMessage",
34884
+ "PushNotification"
34885
+ ].join(",");
34886
+ var READ_ONLY_MAX_TURNS = 40;
34887
+ function readOnlyArgs(cmd) {
34888
+ if (cmd.includes("claude")) {
34889
+ return [
34890
+ `--max-turns=${READ_ONLY_MAX_TURNS}`,
34891
+ "--disallowedTools",
34892
+ READ_ONLY_DISALLOWED_CLAUDE_TOOLS,
34893
+ "--permission-mode",
34894
+ "plan"
34895
+ ];
34896
+ }
34897
+ if (cmd.includes("codex")) return ["--sandbox", "read-only"];
34898
+ return [];
34845
34899
  }
34846
34900
  function requiredArgs(cmd) {
34847
34901
  if (cmd.includes("claude")) return REQUIRED_AI_ARGS.claude;
@@ -34916,21 +34970,19 @@ function overrideEnv(cmd, o) {
34916
34970
  const budget = THINKING_TOKENS[o.thinking];
34917
34971
  return budget ? { MAX_THINKING_TOKENS: String(budget) } : {};
34918
34972
  }
34919
- function buildRunArgs(profile, cmd, resumeId, oneShot = false, override) {
34973
+ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnly = false) {
34920
34974
  const extras = profile.args ?? [];
34921
34975
  const model = override?.model?.trim() || profile.model?.trim();
34922
34976
  const modelArgs = model ? ["--model", model] : [];
34923
34977
  const resumeArgs = resumeId && cmd.includes("claude") ? ["--resume", resumeId] : [];
34924
- const turns = profile.maxTurns;
34925
- const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
34926
- const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
34978
+ const modeExtra = oneShot ? oneShotArgs(cmd) : readOnly ? readOnlyArgs(cmd) : [];
34927
34979
  return [
34928
34980
  ...requiredArgs(cmd),
34929
34981
  ...resumeArgs,
34930
34982
  ...extras,
34931
34983
  ...modelArgs,
34932
34984
  ...overrideArgs(cmd, override),
34933
- ...oneShotExtra
34985
+ ...modeExtra
34934
34986
  ];
34935
34987
  }
34936
34988
  function claudeHomeDirs(config) {
@@ -34970,10 +35022,10 @@ function resolveClaudeProfiles(config, workingDir) {
34970
35022
  if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
34971
35023
  return profiles;
34972
35024
  }
34973
- function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map(), oneShot = false, override) {
34974
- return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume, oneShot, override) : planLegacyRun(prompt, config, hint.dir ?? null, resume, oneShot, override);
35025
+ function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map(), oneShot = false, override, readOnly = false) {
35026
+ return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume, oneShot, override, readOnly) : planLegacyRun(prompt, config, hint.dir ?? null, resume, oneShot, override, readOnly);
34975
35027
  }
34976
- function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, override) {
35028
+ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, override, readOnly = false) {
34977
35029
  const baseEnv = config.aiEnv ?? {};
34978
35030
  const active = activeProvider(config);
34979
35031
  const usable = config.aiProfiles.filter(isUsableCredential);
@@ -34982,7 +35034,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, over
34982
35034
  return {
34983
35035
  cmd: active,
34984
35036
  attempts: [
34985
- { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot, override), stdin: prompt, env: { ...baseEnv, ...overrideEnv(active, override) } }
35037
+ { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot, override, readOnly), stdin: prompt, env: { ...baseEnv, ...overrideEnv(active, override) } }
34986
35038
  ]
34987
35039
  };
34988
35040
  }
@@ -35000,20 +35052,20 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, over
35000
35052
  label: cred.label?.trim() || profileDisplayLabel(dir2),
35001
35053
  dir: dir2,
35002
35054
  key,
35003
- args: buildRunArgs(cred, cmd, resume.get(key), oneShot, override),
35055
+ args: buildRunArgs(cred, cmd, resume.get(key), oneShot, override, readOnly),
35004
35056
  stdin: prompt,
35005
35057
  env: envVar ? { ...baseEnv, [envVar]: dir2, ...overrideEnv(cmd, override) } : { ...baseEnv, ...overrideEnv(cmd, override) }
35006
35058
  }));
35007
35059
  return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
35008
35060
  }
35009
- function planLegacyRun(prompt, config, workingDir, resume, oneShot, override) {
35061
+ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, readOnly = false) {
35010
35062
  const cmd = config.aiCli || DEFAULT_AI_CLI;
35011
35063
  const baseEnv = config.aiEnv ?? {};
35012
35064
  const envVar = profileEnvVar(cmd);
35013
35065
  const defaultPlan = {
35014
35066
  cmd,
35015
35067
  attempts: [
35016
- { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot, override), stdin: prompt, env: { ...baseEnv, ...overrideEnv(cmd, override) } }
35068
+ { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot, override, readOnly), stdin: prompt, env: { ...baseEnv, ...overrideEnv(cmd, override) } }
35017
35069
  ]
35018
35070
  };
35019
35071
  if (!envVar) return defaultPlan;
@@ -35030,7 +35082,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override) {
35030
35082
  label: profileDisplayLabel(dir2),
35031
35083
  dir: dir2,
35032
35084
  key,
35033
- args: buildRunArgs(profile, cmd, resume.get(key), oneShot, override),
35085
+ args: buildRunArgs(profile, cmd, resume.get(key), oneShot, override, readOnly),
35034
35086
  stdin: prompt,
35035
35087
  env: { ...baseEnv, [envVar]: dir2, ...overrideEnv(cmd, override) }
35036
35088
  };
@@ -37922,7 +37974,9 @@ function toDtoEntry(e) {
37922
37974
  level: e.level,
37923
37975
  resultKind: e.resultKind,
37924
37976
  // Processing time on a terminal `exit` entry (ADR-0249) — the web renders it inline.
37925
- ...e.durationMs != null ? { durationMs: e.durationMs } : {}
37977
+ ...e.durationMs != null ? { durationMs: e.durationMs } : {},
37978
+ // AI-run metadata on an `aireq` marker — powers the web's clickable CLI name → details modal.
37979
+ ...e.aiMeta ? { aiMeta: e.aiMeta } : {}
37926
37980
  };
37927
37981
  }
37928
37982
  var WsClient = class {
@@ -38443,7 +38497,8 @@ var WsClient = class {
38443
38497
  "server",
38444
38498
  dispatch.aiOneShot ?? false,
38445
38499
  dispatch.images,
38446
- dispatch.aiConfig
38500
+ dispatch.aiConfig,
38501
+ dispatch.aiReadOnly ?? false
38447
38502
  );
38448
38503
  }
38449
38504
  break;
@@ -38940,7 +38995,7 @@ var WsClient = class {
38940
38995
  * exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
38941
38996
  * of spawning the text as a raw command. Streams output to the transcript AND the server.
38942
38997
  */
38943
- async runAiPrompt(prompt, commandId, origin, oneShot = false, images, aiConfig) {
38998
+ async runAiPrompt(prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false) {
38944
38999
  const config = readProfileConfig(this.context.profileDir);
38945
39000
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
38946
39001
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -39012,7 +39067,7 @@ var WsClient = class {
39012
39067
  const guardedPrompt = this.restrictToFolder && this.physicRoot ? folderScopeGuard(this.physicRoot, aiBodyPrompt) : aiBodyPrompt;
39013
39068
  const hint = unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir };
39014
39069
  const memCfg = resolveMemoryConfig(config);
39015
- const firstAttempt = planAiRun(guardedPrompt, config, hint, /* @__PURE__ */ new Map(), oneShot, aiConfig).attempts[0];
39070
+ const firstAttempt = planAiRun(guardedPrompt, config, hint, /* @__PURE__ */ new Map(), oneShot, aiConfig, readOnly).attempts[0];
39016
39071
  const resumeId = memCfg.enabled && firstAttempt?.key && firstAttempt.cmd === "claude" ? this.sessionIdByKey.get(firstAttempt.key) : void 0;
39017
39072
  let effectivePrompt = guardedPrompt;
39018
39073
  let resume = /* @__PURE__ */ new Map();
@@ -39024,10 +39079,17 @@ ${this.aiMemory}
39024
39079
 
39025
39080
  ${guardedPrompt}`;
39026
39081
  }
39027
- const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot, aiConfig);
39082
+ const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot, aiConfig, readOnly);
39028
39083
  const markerArgs = plan.attempts[0]?.args ?? [];
39029
39084
  const startStamp = formatTimestampInZone(/* @__PURE__ */ new Date(), this.orgTimezone);
39030
- this.bus.push({ source: origin, kind: "aireq", text: `${startStamp} ${plan.cmd} \u2039 ${prompt}` });
39085
+ this.bus.push({
39086
+ source: origin,
39087
+ kind: "aireq",
39088
+ text: `${startStamp} ${plan.cmd} \u2039 ${prompt}`,
39089
+ // Carry the run's prompt + resolved flags so the web Console's clickable CLI name can open a
39090
+ // details modal (the representative first-attempt argv; failover may run a different profile's).
39091
+ aiMeta: { cmd: plan.cmd, args: markerArgs, prompt }
39092
+ });
39031
39093
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
39032
39094
  if (origin === "local") {
39033
39095
  this.send(WsChannels.COMMAND_ANNOUNCE, {
@@ -39085,7 +39147,7 @@ ${guardedPrompt}`;
39085
39147
  `, log: true });
39086
39148
  },
39087
39149
  onAttemptFail: (label, reason) => {
39088
- const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : reason === "credits" ? `profile "${label}" is out of usage credits \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
39150
+ const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : reason === "credits" ? `profile "${label}" is out of usage credits \u2014 trying next` : reason === "auth" ? `profile "${label}" failed to authenticate \u2014 trying next` : `profile "${label}" run failed \u2014 trying next`;
39089
39151
  this.bus.push({ source: origin, kind: "log", text, level: "warn" });
39090
39152
  this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
39091
39153
  `, log: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.5.22",
3
+ "version": "1.5.26",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",