@pikaa-ai/pikaa 0.3.6 → 0.3.8

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/bin/pikaa.js CHANGED
@@ -26,10 +26,10 @@ const rootDir = join(__dirname, "..");
26
26
  const platform = process.platform;
27
27
  const arch = process.arch;
28
28
 
29
- let version = "0.2.3";
29
+ let version = "";
30
30
  try {
31
31
  const pkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8"));
32
- version = pkg.version;
32
+ version = pkg.version || "";
33
33
  } catch {}
34
34
 
35
35
  function getBinaryName() {
@@ -41,7 +41,7 @@ function getBinaryName() {
41
41
 
42
42
  const binaryName = getBinaryName();
43
43
  const userBinDir = join(homedir(), ".pikaa", "bin");
44
- const userBinaryPath = binaryName ? join(userBinDir, `pikaa-v${version}-${binaryName}`) : null;
44
+ const userBinaryPath = binaryName && version ? join(userBinDir, `pikaa-v${version}-${binaryName}`) : null;
45
45
 
46
46
  // Look in package directory or user cache directory
47
47
  function findExistingBinary() {
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli/index.ts
5
- import { resolve as resolve22 } from "path";
6
- import { existsSync as existsSync20 } from "fs";
5
+ import { resolve as resolve21 } from "path";
6
+ import { existsSync as existsSync19 } from "fs";
7
7
  import { createInterface } from "readline";
8
8
 
9
9
  // src/auth/store.ts
@@ -975,6 +975,7 @@ async function runTurn(session, turnContext, input) {
975
975
  signal,
976
976
  execPolicy: session.execPolicy,
977
977
  mode: session.collaborationMode,
978
+ permissionMode: session.permissionMode,
978
979
  onPlanUpdate: (plan, explanation) => {
979
980
  session.emitEvent({
980
981
  type: "PlanUpdated",
@@ -1125,9 +1126,17 @@ async function submissionLoop(session, queue) {
1125
1126
  // src/security/exec-policy.ts
1126
1127
  class ExecPolicy {
1127
1128
  rules = [];
1128
- constructor() {
1129
+ mode = "auto";
1130
+ constructor(initialMode = "auto") {
1131
+ this.mode = initialMode;
1129
1132
  this.initDefaultRules();
1130
1133
  }
1134
+ getMode() {
1135
+ return this.mode;
1136
+ }
1137
+ setMode(mode) {
1138
+ this.mode = mode;
1139
+ }
1131
1140
  initDefaultRules() {
1132
1141
  this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
1133
1142
  this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
@@ -1139,8 +1148,47 @@ class ExecPolicy {
1139
1148
  addRule(pattern, decision, description) {
1140
1149
  this.rules.unshift({ pattern, decision, description });
1141
1150
  }
1151
+ shouldPromptFileEdit(filePath) {
1152
+ if (this.mode === "plan") {
1153
+ return {
1154
+ prompt: false,
1155
+ isPlanBlocked: true,
1156
+ reason: "Plan Mode is active. Mutating files is not allowed while planning."
1157
+ };
1158
+ }
1159
+ if (this.mode === "manual") {
1160
+ return {
1161
+ prompt: true,
1162
+ reason: `Manual mode requires approval to modify '${filePath || "file"}'`
1163
+ };
1164
+ }
1165
+ return { prompt: false };
1166
+ }
1142
1167
  evaluate(command) {
1143
1168
  const trimmed = command.trim();
1169
+ if (this.mode === "plan") {
1170
+ const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
1171
+ if (isReadOnly) {
1172
+ return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
1173
+ }
1174
+ return { decision: "deny", reason: "Cannot execute mutating shell commands in Plan mode" };
1175
+ }
1176
+ if (this.mode === "manual") {
1177
+ return {
1178
+ decision: "prompt",
1179
+ reason: "Manual mode requires confirmation for all shell commands"
1180
+ };
1181
+ }
1182
+ if (this.mode === "accept-edits") {
1183
+ const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
1184
+ if (isReadOnly) {
1185
+ return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
1186
+ }
1187
+ return {
1188
+ decision: "prompt",
1189
+ reason: "Accept-edits mode requires approval for active shell commands"
1190
+ };
1191
+ }
1144
1192
  for (const rule of this.rules) {
1145
1193
  if (rule.pattern.test(trimmed)) {
1146
1194
  return {
@@ -1150,8 +1198,8 @@ class ExecPolicy {
1150
1198
  }
1151
1199
  }
1152
1200
  return {
1153
- decision: "prompt",
1154
- reason: "Command is not in the automatic allowlist"
1201
+ decision: "allow",
1202
+ reason: "Auto mode allows execution"
1155
1203
  };
1156
1204
  }
1157
1205
  }
@@ -1169,6 +1217,17 @@ class Session {
1169
1217
  mcpManager;
1170
1218
  execPolicy;
1171
1219
  collaborationMode = "default";
1220
+ get permissionMode() {
1221
+ return this.execPolicy.getMode();
1222
+ }
1223
+ setPermissionMode(mode) {
1224
+ this.execPolicy.setMode(mode);
1225
+ if (mode === "plan") {
1226
+ this.collaborationMode = "plan";
1227
+ } else if (this.collaborationMode === "plan") {
1228
+ this.collaborationMode = "default";
1229
+ }
1230
+ }
1172
1231
  history = [];
1173
1232
  activeTurn = null;
1174
1233
  status = "idle";
@@ -1415,6 +1474,22 @@ var applyPatchTool = {
1415
1474
  const filePath = resolve4(ctx.cwd, rawPath);
1416
1475
  const targetContent = typeof args.targetContent === "string" ? args.targetContent : "";
1417
1476
  const replacementContent = String(args.replacementContent ?? "");
1477
+ if (ctx.execPolicy) {
1478
+ const evalResult = ctx.execPolicy.shouldPromptFileEdit(rawPath);
1479
+ if (evalResult.isPlanBlocked || ctx.mode === "plan") {
1480
+ return {
1481
+ output: "Error: Cannot mutate files while in Plan Mode. Please present the implementation plan first.",
1482
+ isError: true
1483
+ };
1484
+ }
1485
+ if (evalResult.prompt && ctx.requestApproval) {
1486
+ const approval = await ctx.requestApproval(`Apply patch to: ${rawPath}`, `apply_patch ${rawPath}`);
1487
+ const allowed = typeof approval === "object" ? approval.allowed : Boolean(approval);
1488
+ if (!allowed) {
1489
+ return { output: `Action rejected by user: apply_patch '${rawPath}'`, isError: true };
1490
+ }
1491
+ }
1492
+ }
1418
1493
  if (!existsSync4(filePath)) {
1419
1494
  if (targetContent) {
1420
1495
  return {
@@ -2049,7 +2124,24 @@ var writeFileTool = {
2049
2124
  required: ["path", "content"]
2050
2125
  },
2051
2126
  async execute(args, ctx) {
2052
- const filePath = resolve7(ctx.cwd, String(args.path || ""));
2127
+ const rawPath = String(args.path || "");
2128
+ const filePath = resolve7(ctx.cwd, rawPath);
2129
+ if (ctx.execPolicy) {
2130
+ const evalResult = ctx.execPolicy.shouldPromptFileEdit(rawPath);
2131
+ if (evalResult.isPlanBlocked || ctx.mode === "plan") {
2132
+ return {
2133
+ output: "Error: Cannot write or mutate files while in Plan Mode. Please present the implementation plan first.",
2134
+ isError: true
2135
+ };
2136
+ }
2137
+ if (evalResult.prompt && ctx.requestApproval) {
2138
+ const approval = await ctx.requestApproval(`Write file: ${rawPath}`, `write_file ${rawPath}`);
2139
+ const allowed = typeof approval === "object" ? approval.allowed : Boolean(approval);
2140
+ if (!allowed) {
2141
+ return { output: `Action rejected by user: write_file '${rawPath}'`, isError: true };
2142
+ }
2143
+ }
2144
+ }
2053
2145
  try {
2054
2146
  mkdirSync4(dirname4(filePath), { recursive: true });
2055
2147
  writeFileSync3(filePath, String(args.content ?? ""), "utf8");
@@ -5938,12 +6030,63 @@ function parsePatch(oldSrc, newSrc, contextLines = 3) {
5938
6030
  }
5939
6031
  return result;
5940
6032
  }
6033
+ // package.json
6034
+ var package_default = {
6035
+ name: "@pikaa-ai/pikaa",
6036
+ version: "0.3.8",
6037
+ description: "PIKAA CLI - AI coding agent that runs locally in your terminal.",
6038
+ main: "./dist/index.js",
6039
+ module: "./dist/index.js",
6040
+ types: "./dist/index.d.ts",
6041
+ bin: {
6042
+ pikaa: "./bin/pikaa.js",
6043
+ groupy: "./bin/pikaa.js"
6044
+ },
6045
+ type: "module",
6046
+ files: [
6047
+ "dist/index.js",
6048
+ "dist/cli.js",
6049
+ "bin",
6050
+ "templates",
6051
+ "assets",
6052
+ "skills/**/SKILL.md",
6053
+ "README.md",
6054
+ "LICENSE"
6055
+ ],
6056
+ scripts: {
6057
+ start: "bun run src/cli/index.ts",
6058
+ test: "bun test --timeout 30000",
6059
+ typecheck: "tsc --noEmit",
6060
+ "build:js": "bun build ./src/index.ts --outdir ./dist --target=bun && bun build ./src/cli/index.ts --outfile ./dist/cli.js --target=bun",
6061
+ "build:exe": "bun build ./src/cli/index.ts --compile --outfile pikaa.exe",
6062
+ "build:binaries": "bun run scripts/build-binaries.ts",
6063
+ "release:prepare": "bun run scripts/prepare-release.ts",
6064
+ build: "bun run build:js && bun run build:exe",
6065
+ prepublishOnly: "bun run build:js"
6066
+ },
6067
+ keywords: [
6068
+ "ai",
6069
+ "coding-agent",
6070
+ "codex",
6071
+ "claude-code",
6072
+ "cli",
6073
+ "agentic",
6074
+ "sub-agents",
6075
+ "mcp",
6076
+ "worktree"
6077
+ ],
6078
+ author: "Mesosfer",
6079
+ license: "MIT",
6080
+ devDependencies: {
6081
+ "@types/bun": "latest",
6082
+ "@types/node": "^22.0.0",
6083
+ "@types/react": "^19.2.18",
6084
+ react: "^19.2.8",
6085
+ typescript: "^5.7.0"
6086
+ }
6087
+ };
5941
6088
 
5942
6089
  // src/cli/version.ts
5943
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
5944
- import { dirname as dirname8, join as join8, resolve as resolve17 } from "path";
5945
- import { fileURLToPath } from "url";
5946
- var __dirname = "/home/runner/work/agent-cli/agent-cli/src/cli";
5947
6090
  var cachedMetadata = null;
5948
6091
  function getPackageMetadata() {
5949
6092
  if (cachedMetadata) {
@@ -5951,59 +6094,13 @@ function getPackageMetadata() {
5951
6094
  }
5952
6095
  const envVersion = process.env.PIKAA_VERSION || process.env.GROUPY_VERSION;
5953
6096
  const envName = process.env.PIKAA_NAME || process.env.GROUPY_NAME;
5954
- if (envVersion) {
5955
- cachedMetadata = {
5956
- name: envName || "pikaa",
5957
- version: envVersion
5958
- };
5959
- return cachedMetadata;
5960
- }
5961
- const searchDirs = [];
5962
- try {
5963
- const currentDir = typeof __dirname !== "undefined" ? __dirname : dirname8(fileURLToPath(import.meta.url));
5964
- searchDirs.push(currentDir);
5965
- } catch {}
5966
- for (const startDir of searchDirs) {
5967
- let current = resolve17(startDir);
5968
- for (let depth = 0;depth < 5; depth++) {
5969
- const candidate = join8(current, "package.json");
5970
- if (existsSync17(candidate)) {
5971
- try {
5972
- const content = JSON.parse(readFileSync12(candidate, "utf8"));
5973
- if (content && typeof content.version === "string" && (content.name === "@pikaa-ai/pikaa" || content.name === "pikaa" || content.bin?.pikaa || content.bin?.groupy)) {
5974
- const cleanName = content.name?.startsWith("@") ? content.name.split("/")[1] || content.name : content.name || "pikaa";
5975
- cachedMetadata = {
5976
- name: cleanName,
5977
- version: content.version,
5978
- description: content.description
5979
- };
5980
- return cachedMetadata;
5981
- }
5982
- } catch {}
5983
- }
5984
- const parent = dirname8(current);
5985
- if (parent === current)
5986
- break;
5987
- current = parent;
5988
- }
5989
- }
5990
- try {
5991
- const cwdCandidate = join8(process.cwd(), "package.json");
5992
- if (existsSync17(cwdCandidate)) {
5993
- const content = JSON.parse(readFileSync12(cwdCandidate, "utf8"));
5994
- if (content && typeof content.version === "string" && (content.name === "@pikaa-ai/pikaa" || content.name === "pikaa" || content.bin?.pikaa || content.bin?.groupy)) {
5995
- cachedMetadata = {
5996
- name: "pikaa",
5997
- version: content.version,
5998
- description: content.description
5999
- };
6000
- return cachedMetadata;
6001
- }
6002
- }
6003
- } catch {}
6097
+ const rawName = package_default?.name || "pikaa";
6098
+ const cleanName = rawName.startsWith("@") ? rawName.split("/")[1] || rawName : rawName;
6099
+ const autoVersion = package_default && typeof package_default.version === "string" ? package_default.version : "0.0.0";
6004
6100
  cachedMetadata = {
6005
- name: "pikaa",
6006
- version: "0.3.5"
6101
+ name: envName || cleanName,
6102
+ version: envVersion || autoVersion,
6103
+ description: package_default?.description
6007
6104
  };
6008
6105
  return cachedMetadata;
6009
6106
  }
@@ -6445,10 +6542,41 @@ ${preview}${more}`);
6445
6542
  console.log();
6446
6543
  }
6447
6544
  static formatClaudeUserPrompt(text) {
6448
- const cols = typeof process.stdout?.columns === "number" ? process.stdout.columns : 80;
6449
- const width = Math.min(76, Math.max(48, cols - 6));
6450
- const pad = Math.max(1, width - (text.length + 5));
6451
- return ` \x1B[48;2;50;50;52m\x1B[38;2;145;145;150m \u276F \x1B[38;2;255;255;255m\x1B[1m${text}${" ".repeat(pad)}\x1B[0m`;
6545
+ const cols = typeof process.stdout?.columns === "number" && process.stdout.columns > 0 ? process.stdout.columns : 80;
6546
+ const boxWidth = Math.max(20, cols - 2);
6547
+ const contentWidth = Math.max(10, boxWidth - 3);
6548
+ const BG = "\x1B[48;2;43;43;45m";
6549
+ const CHEVRON = "\x1B[38;2;128;128;133m";
6550
+ const TEXT = "\x1B[38;2;240;240;242m";
6551
+ const RESET2 = "\x1B[0m";
6552
+ const rawLines = text.split(`
6553
+ `);
6554
+ const wrappedLines = [];
6555
+ for (const rawLine of rawLines) {
6556
+ if (rawLine.length === 0) {
6557
+ wrappedLines.push("");
6558
+ continue;
6559
+ }
6560
+ let current = rawLine;
6561
+ while (current.length > contentWidth) {
6562
+ let breakIdx = current.lastIndexOf(" ", contentWidth);
6563
+ if (breakIdx <= 0) {
6564
+ breakIdx = contentWidth;
6565
+ }
6566
+ wrappedLines.push(current.slice(0, breakIdx));
6567
+ current = current.slice(breakIdx).trimStart();
6568
+ }
6569
+ if (current.length > 0) {
6570
+ wrappedLines.push(current);
6571
+ }
6572
+ }
6573
+ const formatted = wrappedLines.map((line, idx) => {
6574
+ const prefix = idx === 0 ? `${CHEVRON} \u276F ` : `${CHEVRON} `;
6575
+ const padLen = Math.max(0, contentWidth - line.length);
6576
+ return ` ${BG}${prefix}${TEXT}${line}${" ".repeat(padLen)}${RESET2}`;
6577
+ });
6578
+ return formatted.join(`
6579
+ `);
6452
6580
  }
6453
6581
  static formatClaudeAssistantResponse(text) {
6454
6582
  return ` \x1B[38;2;192;202;245m${text}\x1B[0m`;
@@ -6481,25 +6609,62 @@ function ensureKeypressInitialized() {
6481
6609
  class InteractiveLineEditor {
6482
6610
  promptSymbol;
6483
6611
  cwd;
6612
+ mode;
6484
6613
  onInterrupt;
6614
+ onModeChange;
6485
6615
  searchEngine = new FileSearchEngine;
6486
6616
  constructor(options = {}) {
6487
6617
  this.promptSymbol = options.promptSymbol || " \x1B[38;2;192;202;245m\u276F\x1B[0m ";
6488
6618
  this.cwd = options.cwd || process.cwd();
6619
+ this.mode = options.initialMode || "auto";
6489
6620
  this.onInterrupt = options.onInterrupt;
6621
+ this.onModeChange = options.onModeChange;
6622
+ }
6623
+ getMode() {
6624
+ return this.mode;
6625
+ }
6626
+ setMode(mode) {
6627
+ this.mode = mode;
6628
+ this.onModeChange?.(mode);
6629
+ }
6630
+ cycleMode() {
6631
+ const modes = ["auto", "manual", "accept-edits", "plan"];
6632
+ const nextIdx = (modes.indexOf(this.mode) + 1) % modes.length;
6633
+ const next = modes[nextIdx];
6634
+ this.setMode(next);
6635
+ return next;
6636
+ }
6637
+ cycleModeReverse() {
6638
+ const modes = ["auto", "manual", "accept-edits", "plan"];
6639
+ const nextIdx = (modes.indexOf(this.mode) - 1 + modes.length) % modes.length;
6640
+ const next = modes[nextIdx];
6641
+ this.setMode(next);
6642
+ return next;
6643
+ }
6644
+ getModeLine() {
6645
+ switch (this.mode) {
6646
+ case "auto":
6647
+ return ` \x1B[38;2;255;215;0m\u23F5\u23F5 auto mode on\x1B[0m \x1B[38;2;148;148;148m(shift+tab to cycle) \xB7 \u21E0 for agents\x1B[0m`;
6648
+ case "manual":
6649
+ return ` \x1B[38;2;148;148;148m\u23F8 manual mode on \xB7 ? for shortcuts \xB7 \u21E0 for agents\x1B[0m`;
6650
+ case "accept-edits":
6651
+ return ` \x1B[38;2;175;175;215m\u23F5\u23F5 accept edits on\x1B[0m \x1B[38;2;148;148;148m(shift+tab to cycle) \xB7 \u21E0 for agents\x1B[0m`;
6652
+ case "plan":
6653
+ return ` \x1B[38;2;95;175;175m\u23F8 plan mode on\x1B[0m \x1B[38;2;148;148;148m(shift+tab to cycle) \xB7 \u21E0 for agents\x1B[0m`;
6654
+ }
6490
6655
  }
6491
6656
  async readLine() {
6492
6657
  if (!process.stdin.isTTY) {
6493
- return new Promise((resolve18) => {
6658
+ return new Promise((resolve17) => {
6494
6659
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6495
6660
  rl.question(this.promptSymbol, (answer) => {
6496
6661
  rl.close();
6497
- resolve18(answer);
6662
+ resolve17(answer);
6498
6663
  });
6499
6664
  });
6500
6665
  }
6501
6666
  ensureKeypressInitialized();
6502
- return new Promise((resolve18) => {
6667
+ return new Promise((resolve17) => {
6503
6668
  let buffer = "";
6504
6669
  let cursor = 0;
6505
6670
  let selectedIndex = 0;
@@ -6540,6 +6705,14 @@ class InteractiveLineEditor {
6540
6705
  return [];
6541
6706
  }
6542
6707
  };
6708
+ const getTerminalCols = () => {
6709
+ return typeof process.stdout?.columns === "number" && process.stdout.columns > 0 ? process.stdout.columns : 80;
6710
+ };
6711
+ const getRule = () => {
6712
+ const cols = getTerminalCols();
6713
+ const ruleLen = Math.max(10, cols - 4);
6714
+ return "\u2500".repeat(ruleLen);
6715
+ };
6543
6716
  const ensureVisible = (totalItems, visibleRows) => {
6544
6717
  if (totalItems === 0 || visibleRows === 0) {
6545
6718
  scrollTop = 0;
@@ -6567,37 +6740,39 @@ class InteractiveLineEditor {
6567
6740
  }
6568
6741
  };
6569
6742
  const redraw = () => {
6570
- clearMenu();
6743
+ process.stdout.write("\x1B[J");
6571
6744
  process.stdout.write(`\r\x1B[2K${this.promptSymbol}${buffer}`);
6572
6745
  const slashMatches = getMatchingCommands();
6573
6746
  const activeFile = getActiveFileQuery();
6574
6747
  const fileMatches = activeFile ? getMatchingFiles(activeFile.query) : [];
6575
6748
  if (buffer.startsWith("/") && !popupDismissed && slashMatches.length > 0) {
6576
- const BOX_WIDTH = 70;
6749
+ const termCols = getTerminalCols();
6750
+ const BOX_WIDTH = Math.max(20, Math.min(termCols - 4, 120));
6577
6751
  const maxVisible = Math.min(slashMatches.length, 7);
6578
6752
  ensureVisible(slashMatches.length, maxVisible);
6579
6753
  const visibleMatches = slashMatches.slice(scrollTop, scrollTop + maxVisible);
6580
6754
  const menuLines = [];
6581
- const rule = "\u2500".repeat(Math.min(BOX_WIDTH, 68));
6582
- const RULE_COLOR = "\x1B[38;2;80;80;88m";
6755
+ const rule = "\u2500".repeat(BOX_WIDTH);
6756
+ const RULE_COLOR2 = "\x1B[38;2;80;80;88m";
6583
6757
  const ACTIVE_COLOR = "\x1B[38;2;225;225;225m";
6584
6758
  const INACTIVE_COLOR = "\x1B[38;2;139;139;144m";
6585
6759
  const RESET2 = "\x1B[0m";
6586
- menuLines.push(` ${RULE_COLOR}${rule}${RESET2}`);
6760
+ menuLines.push(` ${RULE_COLOR2}${rule}${RESET2}`);
6761
+ const maxDescLen = Math.max(10, BOX_WIDTH - 22);
6587
6762
  for (let i = 0;i < visibleMatches.length; i++) {
6588
6763
  const cmd = visibleMatches[i];
6589
6764
  const actualIdx = scrollTop + i;
6590
6765
  const isSelected = actualIdx === selectedIndex;
6591
6766
  const marker = isSelected ? `${ACTIVE_COLOR}\u276F${RESET2}` : " ";
6592
6767
  const rawName = cmd.name.padEnd(16).slice(0, 16);
6593
- const rawDesc = cmd.description.length > 48 ? cmd.description.slice(0, 45) + "..." : cmd.description;
6768
+ const rawDesc = cmd.description.length > maxDescLen ? cmd.description.slice(0, maxDescLen - 3) + "..." : cmd.description;
6594
6769
  if (isSelected) {
6595
6770
  menuLines.push(` ${marker} \x1B[1m${ACTIVE_COLOR}${rawName}${RESET2} \x1B[1m${ACTIVE_COLOR}${rawDesc}${RESET2}`);
6596
6771
  } else {
6597
6772
  menuLines.push(` ${marker} ${INACTIVE_COLOR}${rawName}${RESET2} ${INACTIVE_COLOR}${rawDesc}${RESET2}`);
6598
6773
  }
6599
6774
  }
6600
- menuLines.push(` ${RULE_COLOR}${rule}${RESET2}`);
6775
+ menuLines.push(` ${RULE_COLOR2}${rule}${RESET2}`);
6601
6776
  for (const line of menuLines) {
6602
6777
  process.stdout.write(`
6603
6778
  \x1B[2K${line}`);
@@ -6605,18 +6780,20 @@ class InteractiveLineEditor {
6605
6780
  renderedMenuLines = menuLines.length;
6606
6781
  process.stdout.write(`\x1B[${renderedMenuLines}A`);
6607
6782
  } else if (activeFile && !popupDismissed && fileMatches.length > 0) {
6608
- const BOX_WIDTH = 70;
6783
+ const termCols = getTerminalCols();
6784
+ const BOX_WIDTH = Math.max(20, Math.min(termCols - 4, 120));
6609
6785
  const maxVisible = Math.min(fileMatches.length, 7);
6610
6786
  ensureVisible(fileMatches.length, maxVisible);
6611
6787
  const visibleMatches = fileMatches.slice(scrollTop, scrollTop + maxVisible);
6612
6788
  const menuLines = [];
6613
6789
  menuLines.push(` ${style.dim("\u250C\u2500\u2500")} ${style.brandBold("Files")} ${style.dim("\u2500".repeat(Math.max(10, BOX_WIDTH - 9)) + "\u2510")}`);
6790
+ const maxPathLen = Math.max(10, BOX_WIDTH - 8);
6614
6791
  for (let i = 0;i < visibleMatches.length; i++) {
6615
6792
  const filePath = visibleMatches[i];
6616
6793
  const actualIdx = scrollTop + i;
6617
6794
  const isSelected = actualIdx === selectedIndex;
6618
6795
  const marker = isSelected ? style.brand("\u276F") : " ";
6619
- const rawPath = filePath.length > 62 ? "..." + filePath.slice(filePath.length - 59) : filePath.padEnd(62);
6796
+ const rawPath = filePath.length > maxPathLen ? "..." + filePath.slice(filePath.length - (maxPathLen - 3)) : filePath.padEnd(maxPathLen);
6620
6797
  const coloredPath = isSelected ? style.brandBold(rawPath) : style.cyan(rawPath);
6621
6798
  menuLines.push(` ${style.dim("\u2502")} ${marker} ${coloredPath} ${style.dim("\u2502")}`);
6622
6799
  }
@@ -6641,6 +6818,15 @@ class InteractiveLineEditor {
6641
6818
  }
6642
6819
  renderedMenuLines = menuLines.length;
6643
6820
  process.stdout.write(`\x1B[${renderedMenuLines}A`);
6821
+ } else {
6822
+ const RULE_COLOR2 = "\x1B[38;2;60;60;68m";
6823
+ const bottomRule = ` ${RULE_COLOR2}${getRule()}\x1B[0m`;
6824
+ const modeLine = this.getModeLine();
6825
+ process.stdout.write(`
6826
+ \x1B[2K${bottomRule}
6827
+ \x1B[2K${modeLine}`);
6828
+ renderedMenuLines = 2;
6829
+ process.stdout.write(`\x1B[2A`);
6644
6830
  }
6645
6831
  const visiblePromptLength = this.promptSymbol.replace(/\x1b\[[0-9;]*m/g, "").length;
6646
6832
  const cursorCol = visiblePromptLength + cursor;
@@ -6650,7 +6836,16 @@ class InteractiveLineEditor {
6650
6836
  process.stdout.write("\r");
6651
6837
  }
6652
6838
  };
6839
+ const onResize = () => {
6840
+ redraw();
6841
+ };
6842
+ if (process.stdout && typeof process.stdout.on === "function") {
6843
+ process.stdout.on("resize", onResize);
6844
+ }
6653
6845
  const cleanupAndResolve = (result) => {
6846
+ if (process.stdout && typeof process.stdout.removeListener === "function") {
6847
+ process.stdout.removeListener("resize", onResize);
6848
+ }
6654
6849
  clearMenu();
6655
6850
  process.stdin.removeListener("keypress", onKeypress);
6656
6851
  if (process.stdin.isTTY) {
@@ -6659,14 +6854,14 @@ class InteractiveLineEditor {
6659
6854
  } catch {}
6660
6855
  }
6661
6856
  if (result.trim().length > 0 && !result.startsWith("/")) {
6662
- process.stdout.write(`\r\x1B[2K${CliFormatter.formatClaudeUserPrompt(result)}
6857
+ process.stdout.write(`\x1B[1A\r\x1B[2K${CliFormatter.formatClaudeUserPrompt(result)}
6663
6858
 
6664
6859
  `);
6665
6860
  } else {
6666
- process.stdout.write(`
6861
+ process.stdout.write(`\x1B[1A\r\x1B[2K
6667
6862
  `);
6668
6863
  }
6669
- resolve18(result);
6864
+ resolve17(result);
6670
6865
  };
6671
6866
  const onKeypress = (_str, key) => {
6672
6867
  if (!key)
@@ -6681,6 +6876,9 @@ class InteractiveLineEditor {
6681
6876
  redraw();
6682
6877
  return;
6683
6878
  }
6879
+ if (process.stdout && typeof process.stdout.removeListener === "function") {
6880
+ process.stdout.removeListener("resize", onResize);
6881
+ }
6684
6882
  clearMenu();
6685
6883
  if (process.stdin.isTTY) {
6686
6884
  try {
@@ -6731,6 +6929,12 @@ class InteractiveLineEditor {
6731
6929
  cleanupAndResolve(buffer);
6732
6930
  return;
6733
6931
  }
6932
+ const isShiftTab = key.name === "tab" && Boolean(key.shift) || key.name === "backtab" || key.sequence === "\x1B[Z" || _str === "\x1B[Z";
6933
+ if (isShiftTab) {
6934
+ this.cycleMode();
6935
+ redraw();
6936
+ return;
6937
+ }
6734
6938
  if (key.name === "tab") {
6735
6939
  if (activeFile && !popupDismissed && fileMatches.length > 0) {
6736
6940
  const chosen = fileMatches[selectedIndex] || fileMatches[0];
@@ -6753,8 +6957,14 @@ class InteractiveLineEditor {
6753
6957
  scrollTop = 0;
6754
6958
  popupDismissed = false;
6755
6959
  redraw();
6960
+ return;
6756
6961
  }
6757
6962
  }
6963
+ if (buffer.trim().length === 0) {
6964
+ this.cycleMode();
6965
+ redraw();
6966
+ return;
6967
+ }
6758
6968
  return;
6759
6969
  }
6760
6970
  if (key.name === "up") {
@@ -6818,7 +7028,11 @@ class InteractiveLineEditor {
6818
7028
  }
6819
7029
  };
6820
7030
  process.stdin.on("keypress", onKeypress);
6821
- process.stdout.write(this.promptSymbol);
7031
+ const RULE_COLOR = "\x1B[38;2;60;60;68m";
7032
+ process.stdout.write(`
7033
+ ${RULE_COLOR}${getRule()}\x1B[0m
7034
+ `);
7035
+ redraw();
6822
7036
  });
6823
7037
  }
6824
7038
  }
@@ -6831,7 +7045,7 @@ async function promptChoice(config) {
6831
7045
  if (!process.stdin.readable) {
6832
7046
  return choices[defaultIndex]?.value ?? choices[0].value;
6833
7047
  }
6834
- return new Promise((resolve18) => {
7048
+ return new Promise((resolve17) => {
6835
7049
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
6836
7050
  const hint = choices.map((c3) => c3.isDefault ? `[${c3.key.toUpperCase()}]` : `[${c3.key}]`).join("/");
6837
7051
  let resolved = false;
@@ -6839,7 +7053,7 @@ async function promptChoice(config) {
6839
7053
  if (!resolved) {
6840
7054
  resolved = true;
6841
7055
  rl.close();
6842
- resolve18(val);
7056
+ resolve17(val);
6843
7057
  }
6844
7058
  };
6845
7059
  rl.question(`${message} ${hint}: `, (answer) => {
@@ -6852,7 +7066,7 @@ async function promptChoice(config) {
6852
7066
  });
6853
7067
  });
6854
7068
  }
6855
- return new Promise((resolve18) => {
7069
+ return new Promise((resolve17) => {
6856
7070
  let selectedIndex = defaultIndex;
6857
7071
  if (selectedIndex < 0 || selectedIndex >= choices.length)
6858
7072
  selectedIndex = 0;
@@ -6881,7 +7095,7 @@ async function promptChoice(config) {
6881
7095
  } catch {}
6882
7096
  process.stdout.write(`\r\x1B[1A\r\x1B[2K ${style.bold(message)} ${style.cyan(`[${confirmedChoice.key}] ${confirmedChoice.label}`)}
6883
7097
  \r\x1B[2K`);
6884
- resolve18(confirmedChoice.value);
7098
+ resolve17(confirmedChoice.value);
6885
7099
  };
6886
7100
  const onKeypress = (_str, key) => {
6887
7101
  if (!key)
@@ -6956,7 +7170,7 @@ async function promptToolApproval(params) {
6956
7170
  renderCard(1);
6957
7171
  return "yes";
6958
7172
  }
6959
- return new Promise((resolve18) => {
7173
+ return new Promise((resolve17) => {
6960
7174
  let selectedIndex = 1;
6961
7175
  readline2.emitKeypressEvents(process.stdin);
6962
7176
  const wasRaw = process.stdin.isRaw;
@@ -6973,7 +7187,7 @@ async function promptToolApproval(params) {
6973
7187
  process.stdin.setRawMode(wasRaw ?? false);
6974
7188
  } catch {}
6975
7189
  console.log();
6976
- resolve18(val);
7190
+ resolve17(val);
6977
7191
  };
6978
7192
  const onKeypress = (_str, key) => {
6979
7193
  if (!key)
@@ -7056,7 +7270,7 @@ async function promptUserQuestion(params) {
7056
7270
  if (!process.stdin.isTTY || false || !process.stdin.readable) {
7057
7271
  return options[0] || "yes";
7058
7272
  }
7059
- return new Promise((resolve18) => {
7273
+ return new Promise((resolve17) => {
7060
7274
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
7061
7275
  const promptLabel = options.length > 0 ? `Select [1-${options.length}] or type custom response: ` : `Your response: `;
7062
7276
  rl.question(` ${style.bold(promptLabel)}`, (answer) => {
@@ -7066,7 +7280,7 @@ async function promptUserQuestion(params) {
7066
7280
  const fallback = options[0] || "";
7067
7281
  console.log(style.dim(` \u21B3 Default: ${fallback || "(empty)"}
7068
7282
  `));
7069
- resolve18(fallback);
7283
+ resolve17(fallback);
7070
7284
  return;
7071
7285
  }
7072
7286
  const num = parseInt(trimmed, 10);
@@ -7074,26 +7288,26 @@ async function promptUserQuestion(params) {
7074
7288
  const picked = options[num - 1];
7075
7289
  console.log(style.green(` \u2714 Selected: ${picked}
7076
7290
  `));
7077
- resolve18(picked);
7291
+ resolve17(picked);
7078
7292
  return;
7079
7293
  }
7080
7294
  if (options.length === 2) {
7081
7295
  if (/^(y|yes)$/i.test(trimmed)) {
7082
7296
  console.log(style.green(` \u2714 Selected: ${options[0]}
7083
7297
  `));
7084
- resolve18(options[0]);
7298
+ resolve17(options[0]);
7085
7299
  return;
7086
7300
  }
7087
7301
  if (/^(n|no)$/i.test(trimmed)) {
7088
7302
  console.log(style.green(` \u2714 Selected: ${options[1]}
7089
7303
  `));
7090
- resolve18(options[1]);
7304
+ resolve17(options[1]);
7091
7305
  return;
7092
7306
  }
7093
7307
  }
7094
7308
  console.log(style.green(` \u2714 Answer: ${trimmed}
7095
7309
  `));
7096
- resolve18(trimmed);
7310
+ resolve17(trimmed);
7097
7311
  });
7098
7312
  });
7099
7313
  }
@@ -7122,7 +7336,7 @@ async function promptInteractiveList(config) {
7122
7336
  `);
7123
7337
  return { selectedIndex: -1, action: "close" };
7124
7338
  }
7125
- return new Promise((resolve18) => {
7339
+ return new Promise((resolve17) => {
7126
7340
  let selectedIndex = Math.max(0, Math.min(defaultIndex, items.length - 1));
7127
7341
  let scrollTop = 0;
7128
7342
  let renderedLines = 0;
@@ -7214,7 +7428,7 @@ async function promptInteractiveList(config) {
7214
7428
  try {
7215
7429
  process.stdin.setRawMode(wasRaw ?? false);
7216
7430
  } catch {}
7217
- resolve18(res);
7431
+ resolve17(res);
7218
7432
  };
7219
7433
  const onKeypress = async (_str, key) => {
7220
7434
  if (!key)
@@ -7285,8 +7499,8 @@ async function promptInteractiveList(config) {
7285
7499
  }
7286
7500
 
7287
7501
  // src/security/scanner.ts
7288
- import { existsSync as existsSync18, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
7289
- import { join as join9, relative as relative2, resolve as resolve18 } from "path";
7502
+ import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
7503
+ import { join as join8, relative as relative2, resolve as resolve17 } from "path";
7290
7504
  var SECURITY_RULES = [
7291
7505
  {
7292
7506
  id: "SEC-001",
@@ -7418,13 +7632,13 @@ var IGNORED_FILES = new Set([
7418
7632
  ]);
7419
7633
  async function runSecurityScan(targetDir, options = {}) {
7420
7634
  const startTime = performance.now();
7421
- const root = resolve18(targetDir);
7635
+ const root = resolve17(targetDir);
7422
7636
  const isDirectTestDir = targetDir.includes("test") || Boolean(options.includeTests);
7423
7637
  const maxFiles = options.maxFiles || 2000;
7424
7638
  const findings = [];
7425
7639
  let scannedCount = 0;
7426
7640
  function walk(current) {
7427
- if (scannedCount >= maxFiles || !existsSync18(current))
7641
+ if (scannedCount >= maxFiles || !existsSync17(current))
7428
7642
  return;
7429
7643
  let entries;
7430
7644
  try {
@@ -7435,7 +7649,7 @@ async function runSecurityScan(targetDir, options = {}) {
7435
7649
  for (const entry of entries) {
7436
7650
  if (scannedCount >= maxFiles)
7437
7651
  break;
7438
- const fullPath = join9(current, entry);
7652
+ const fullPath = join8(current, entry);
7439
7653
  let stat;
7440
7654
  try {
7441
7655
  stat = statSync3(fullPath);
@@ -7466,7 +7680,7 @@ async function runSecurityScan(targetDir, options = {}) {
7466
7680
  function scanFile(filePath, baseRoot, ext, out) {
7467
7681
  let content;
7468
7682
  try {
7469
- content = readFileSync13(filePath, "utf8");
7683
+ content = readFileSync12(filePath, "utf8");
7470
7684
  } catch {
7471
7685
  return;
7472
7686
  }
@@ -7513,34 +7727,34 @@ async function runSecurityScan(targetDir, options = {}) {
7513
7727
  }
7514
7728
 
7515
7729
  // src/mcp/servers/chrome-devtools/index.ts
7516
- import { resolve as resolve19 } from "path";
7730
+ import { resolve as resolve18 } from "path";
7517
7731
 
7518
7732
  // src/mcp/servers/chrome-devtools/server.ts
7519
7733
  if (false) {}
7520
7734
 
7521
7735
  // src/mcp/servers/chrome-devtools/index.ts
7522
7736
  var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/chrome-devtools";
7523
- var CHROME_DEVTOOLS_MCP_SERVER_PATH = resolve19(__dirname, "server.ts");
7737
+ var CHROME_DEVTOOLS_MCP_SERVER_PATH = resolve18(__dirname, "server.ts");
7524
7738
 
7525
7739
  // src/mcp/servers/web-search/index.ts
7526
- import { resolve as resolve20 } from "path";
7740
+ import { resolve as resolve19 } from "path";
7527
7741
 
7528
7742
  // src/mcp/servers/web-search/server.ts
7529
7743
  if (false) {}
7530
7744
 
7531
7745
  // src/mcp/servers/web-search/index.ts
7532
7746
  var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/web-search";
7533
- var WEB_SEARCH_MCP_SERVER_PATH = resolve20(__dirname, "server.ts");
7747
+ var WEB_SEARCH_MCP_SERVER_PATH = resolve19(__dirname, "server.ts");
7534
7748
 
7535
7749
  // src/mcp/servers/sqlite/index.ts
7536
- import { resolve as resolve21 } from "path";
7750
+ import { resolve as resolve20 } from "path";
7537
7751
 
7538
7752
  // src/mcp/servers/sqlite/server.ts
7539
7753
  if (false) {}
7540
7754
 
7541
7755
  // src/mcp/servers/sqlite/index.ts
7542
7756
  var __dirname = "/home/runner/work/agent-cli/agent-cli/src/mcp/servers/sqlite";
7543
- var SQLITE_MCP_SERVER_PATH = resolve21(__dirname, "server.ts");
7757
+ var SQLITE_MCP_SERVER_PATH = resolve20(__dirname, "server.ts");
7544
7758
 
7545
7759
  // src/cli/commands.ts
7546
7760
  var AVAILABLE_SLASH_COMMANDS = [
@@ -7550,6 +7764,11 @@ var AVAILABLE_SLASH_COMMANDS = [
7550
7764
  { name: "/reasoning", description: "Toggle internal reasoning chain visibility" },
7551
7765
  { name: "/login", description: "Authenticate with backend provider" },
7552
7766
  { name: "/whoami", description: "Check backend authentication status" },
7767
+ { name: "/mode", description: "Cycle or set execution permission mode (auto, manual, accept-edits, plan)" },
7768
+ { name: "/auto", description: "Switch to Auto Mode (tools execute automatically)" },
7769
+ { name: "/manual", description: "Switch to Manual Mode (all tools require user confirmation)" },
7770
+ { name: "/accept-edits", description: "Switch to Accept Edits Mode (file edits auto-approved, shell prompts)" },
7771
+ { name: "/plan", description: "Switch to Plan Mode (read-only planning, mutations blocked)" },
7553
7772
  { name: "/skills", description: "List domain skills in workspace & global" },
7554
7773
  { name: "/memories", description: "View learned preferences & memories" },
7555
7774
  { name: "/worktrees", description: "List active isolated Git Worktrees" },
@@ -7601,6 +7820,37 @@ async function handleSlashCommand(input, ctx) {
7601
7820
  case "/roles":
7602
7821
  await handleRolesCommand(ctx, args[0]);
7603
7822
  return true;
7823
+ case "/mode":
7824
+ case "/modes":
7825
+ case "/permission":
7826
+ case "/permissions":
7827
+ await handleModeCommand(ctx, args[0]);
7828
+ return true;
7829
+ case "/auto":
7830
+ ctx.session.setPermissionMode("auto");
7831
+ console.log(`
7832
+ \x1B[38;2;255;215;0m\u23F5\u23F5 Switched to Auto Mode (tools execute automatically)\x1B[0m
7833
+ `);
7834
+ return true;
7835
+ case "/manual":
7836
+ ctx.session.setPermissionMode("manual");
7837
+ console.log(`
7838
+ \x1B[38;2;148;148;148m\u23F8 Switched to Manual Mode (all tools require user confirmation)\x1B[0m
7839
+ `);
7840
+ return true;
7841
+ case "/accept-edits":
7842
+ case "/acceptedits":
7843
+ ctx.session.setPermissionMode("accept-edits");
7844
+ console.log(`
7845
+ \x1B[38;2;175;175;215m\u23F5\u23F5 Switched to Accept Edits Mode (file edits auto-approved, shell prompts)\x1B[0m
7846
+ `);
7847
+ return true;
7848
+ case "/plan":
7849
+ ctx.session.setPermissionMode("plan");
7850
+ console.log(`
7851
+ \x1B[38;2;95;175;175m\u23F8 Switched to Plan Mode (read-only planning, mutations blocked)\x1B[0m
7852
+ `);
7853
+ return true;
7604
7854
  case "/agents":
7605
7855
  printAgents(ctx);
7606
7856
  return true;
@@ -8517,6 +8767,84 @@ function printReleaseNotes() {
8517
8767
  console.log(" " + ROSE2 + "\u2514" + "\u2500".repeat(totalInnerWidth) + "\u2518" + RESET2);
8518
8768
  console.log("");
8519
8769
  }
8770
+ async function handleModeCommand(ctx, arg) {
8771
+ const validModes = [
8772
+ {
8773
+ mode: "auto",
8774
+ title: "Auto Mode",
8775
+ desc: "Tools run automatically without confirmation (fastest autonomous workflow)",
8776
+ glyph: "\u23F5\u23F5",
8777
+ color: "\x1B[38;2;255;215;0m"
8778
+ },
8779
+ {
8780
+ mode: "manual",
8781
+ title: "Manual Mode",
8782
+ desc: "Every file edit and shell command asks for user confirmation",
8783
+ glyph: "\u23F8",
8784
+ color: "\x1B[38;2;148;148;148m"
8785
+ },
8786
+ {
8787
+ mode: "accept-edits",
8788
+ title: "Accept Edits Mode",
8789
+ desc: "File edits are auto-approved, but shell commands require approval",
8790
+ glyph: "\u23F5\u23F5",
8791
+ color: "\x1B[38;2;175;175;215m"
8792
+ },
8793
+ {
8794
+ mode: "plan",
8795
+ title: "Plan Mode",
8796
+ desc: "Read-only mode. Blocks all mutations and focuses strictly on planning",
8797
+ glyph: "\u23F8",
8798
+ color: "\x1B[38;2;95;175;175m"
8799
+ }
8800
+ ];
8801
+ const currentMode = ctx.session.permissionMode;
8802
+ if (arg) {
8803
+ const normalized = arg.trim().toLowerCase();
8804
+ const match = validModes.find((m) => m.mode === normalized || m.mode.replace("-", "") === normalized);
8805
+ if (match) {
8806
+ ctx.session.setPermissionMode(match.mode);
8807
+ console.log(`
8808
+ ${match.color}${match.glyph} Switched to ${match.title}\x1B[0m
8809
+ ${style.dim(match.desc)}
8810
+ `);
8811
+ return;
8812
+ }
8813
+ }
8814
+ if (!process.stdin.isTTY || false || !process.stdin.readable) {
8815
+ console.log(`
8816
+ Current Permission Mode: ${style.bold(currentMode)}`);
8817
+ for (const m of validModes) {
8818
+ const active = m.mode === currentMode ? " (active)" : "";
8819
+ console.log(` \u2022 ${m.title}${active}: ${m.desc}`);
8820
+ }
8821
+ console.log();
8822
+ return;
8823
+ }
8824
+ const items = validModes.map((m) => ({
8825
+ id: m.mode,
8826
+ label: `${m.glyph} ${m.title}`,
8827
+ badge: m.mode === currentMode ? "ACTIVE" : undefined,
8828
+ description: m.desc,
8829
+ checked: m.mode === currentMode
8830
+ }));
8831
+ const res = await promptInteractiveList({
8832
+ title: "\uD83C\uDF9B\uFE0F Select Execution Permission Mode (Shift+Tab in prompt to cycle)",
8833
+ items,
8834
+ mode: "select",
8835
+ customKeyHints: "\u2191/\u2193: navigate \xB7 Enter: switch mode \xB7 Esc: cancel"
8836
+ });
8837
+ if (res.action === "select" && res.selectedItem) {
8838
+ const chosen = validModes.find((m) => m.mode === res.selectedItem?.id);
8839
+ if (chosen) {
8840
+ ctx.session.setPermissionMode(chosen.mode);
8841
+ console.log(`
8842
+ ${chosen.color}${chosen.glyph} Switched to ${chosen.title}\x1B[0m
8843
+ ${style.dim(chosen.desc)}
8844
+ `);
8845
+ }
8846
+ }
8847
+ }
8520
8848
 
8521
8849
  // src/cli/ui/markdown.ts
8522
8850
  class MarkdownHighlighter {
@@ -8788,9 +9116,9 @@ class MarkdownHighlighter {
8788
9116
  }
8789
9117
 
8790
9118
  // src/cli/update-checker.ts
8791
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
9119
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
8792
9120
  import { homedir as homedir9 } from "os";
8793
- import { join as join10 } from "path";
9121
+ import { join as join9 } from "path";
8794
9122
  var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
8795
9123
  function parseSemver(v) {
8796
9124
  const clean = v.replace(/^v/, "").trim();
@@ -8811,8 +9139,8 @@ function isNewerVersion(current, remote) {
8811
9139
  return remPatch > curPatch;
8812
9140
  }
8813
9141
  function getUpdateCachePath() {
8814
- const baseDir = process.env.PIKAA_HOME || process.env.GROUPY_HOME || join10(homedir9(), ".pikaa");
8815
- return join10(baseDir, "update-cache.json");
9142
+ const baseDir = process.env.PIKAA_HOME || process.env.GROUPY_HOME || join9(homedir9(), ".pikaa");
9143
+ return join9(baseDir, "update-cache.json");
8816
9144
  }
8817
9145
  async function fetchLatestNpmVersion(packageName, timeoutMs = 1500) {
8818
9146
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
@@ -8845,9 +9173,9 @@ async function checkForUpdates(options = {}) {
8845
9173
  const cachePath = options.cachePath || getUpdateCachePath();
8846
9174
  const now = Date.now();
8847
9175
  let cached = null;
8848
- if (!options.force && existsSync19(cachePath)) {
9176
+ if (!options.force && existsSync18(cachePath)) {
8849
9177
  try {
8850
- const raw = JSON.parse(readFileSync14(cachePath, "utf8"));
9178
+ const raw = JSON.parse(readFileSync13(cachePath, "utf8"));
8851
9179
  if (raw && typeof raw.lastChecked === "number" && typeof raw.latestVersion === "string") {
8852
9180
  cached = raw;
8853
9181
  if (now - cached.lastChecked < CHECK_INTERVAL_MS) {
@@ -8875,8 +9203,8 @@ async function checkForUpdates(options = {}) {
8875
9203
  return null;
8876
9204
  }
8877
9205
  try {
8878
- const parentDir = join10(cachePath, "..");
8879
- if (!existsSync19(parentDir)) {
9206
+ const parentDir = join9(cachePath, "..");
9207
+ if (!existsSync18(parentDir)) {
8880
9208
  mkdirSync10(parentDir, { recursive: true });
8881
9209
  }
8882
9210
  const cacheData = {
@@ -9049,9 +9377,9 @@ class CliRepl {
9049
9377
  filesModified: Array.from(this.turnFilesModified)
9050
9378
  });
9051
9379
  if (this.turnDoneResolver) {
9052
- const resolve22 = this.turnDoneResolver;
9380
+ const resolve21 = this.turnDoneResolver;
9053
9381
  this.turnDoneResolver = undefined;
9054
- resolve22();
9382
+ resolve21();
9055
9383
  }
9056
9384
  break;
9057
9385
  case "Error":
@@ -9067,9 +9395,9 @@ class CliRepl {
9067
9395
  console.error(style.red(`Error: ${msg.message}
9068
9396
  `));
9069
9397
  if (this.turnDoneResolver) {
9070
- const resolve22 = this.turnDoneResolver;
9398
+ const resolve21 = this.turnDoneResolver;
9071
9399
  this.turnDoneResolver = undefined;
9072
- resolve22();
9400
+ resolve21();
9073
9401
  }
9074
9402
  break;
9075
9403
  }
@@ -9124,6 +9452,10 @@ class CliRepl {
9124
9452
  }).catch(() => {});
9125
9453
  const editor = new InteractiveLineEditor({
9126
9454
  cwd: this.session.cwd,
9455
+ initialMode: this.session.permissionMode,
9456
+ onModeChange: (newMode) => {
9457
+ this.session.setPermissionMode(newMode);
9458
+ },
9127
9459
  onInterrupt: () => {
9128
9460
  if (this.isProcessing) {
9129
9461
  const activeTurn = this.session.getActiveTurn();
@@ -9135,9 +9467,9 @@ class CliRepl {
9135
9467
  `));
9136
9468
  this.isProcessing = false;
9137
9469
  if (this.turnDoneResolver) {
9138
- const resolve22 = this.turnDoneResolver;
9470
+ const resolve21 = this.turnDoneResolver;
9139
9471
  this.turnDoneResolver = undefined;
9140
- resolve22();
9472
+ resolve21();
9141
9473
  }
9142
9474
  }
9143
9475
  }
@@ -9169,8 +9501,8 @@ class CliRepl {
9169
9501
  }
9170
9502
  }
9171
9503
  try {
9172
- const turnPromise = new Promise((resolve22) => {
9173
- this.turnDoneResolver = resolve22;
9504
+ const turnPromise = new Promise((resolve21) => {
9505
+ this.turnDoneResolver = resolve21;
9174
9506
  });
9175
9507
  await this.session.submit({
9176
9508
  type: "TurnInput",
@@ -9258,7 +9590,7 @@ async function main() {
9258
9590
  explicitApiKey = args[++i];
9259
9591
  apiKey = explicitApiKey || apiKey;
9260
9592
  } else if (arg === "--cwd" || arg === "-C") {
9261
- cwd = resolve22(args[++i] || cwd);
9593
+ cwd = resolve21(args[++i] || cwd);
9262
9594
  } else if (arg === "--role" || arg === "-r") {
9263
9595
  role = args[++i] || role;
9264
9596
  } else if (arg === "--mcp") {
@@ -9284,11 +9616,11 @@ async function main() {
9284
9616
  const mcpManager = new McpManager;
9285
9617
  const candidateConfigs = [
9286
9618
  mcpConfigFile,
9287
- resolve22(cwd, ".mcp.json"),
9288
- resolve22(cwd, "mcp_config.json")
9619
+ resolve21(cwd, ".mcp.json"),
9620
+ resolve21(cwd, "mcp_config.json")
9289
9621
  ].filter(Boolean);
9290
9622
  for (const cfg of candidateConfigs) {
9291
- if (existsSync20(cfg)) {
9623
+ if (existsSync19(cfg)) {
9292
9624
  try {
9293
9625
  await mcpManager.loadConfigFile(cfg);
9294
9626
  mcpManager.registerToolsIntoRouter(tools4);
package/dist/index.js CHANGED
@@ -1648,6 +1648,22 @@ var applyPatchTool = {
1648
1648
  const filePath = resolve2(ctx.cwd, rawPath);
1649
1649
  const targetContent = typeof args.targetContent === "string" ? args.targetContent : "";
1650
1650
  const replacementContent = String(args.replacementContent ?? "");
1651
+ if (ctx.execPolicy) {
1652
+ const evalResult = ctx.execPolicy.shouldPromptFileEdit(rawPath);
1653
+ if (evalResult.isPlanBlocked || ctx.mode === "plan") {
1654
+ return {
1655
+ output: "Error: Cannot mutate files while in Plan Mode. Please present the implementation plan first.",
1656
+ isError: true
1657
+ };
1658
+ }
1659
+ if (evalResult.prompt && ctx.requestApproval) {
1660
+ const approval = await ctx.requestApproval(`Apply patch to: ${rawPath}`, `apply_patch ${rawPath}`);
1661
+ const allowed = typeof approval === "object" ? approval.allowed : Boolean(approval);
1662
+ if (!allowed) {
1663
+ return { output: `Action rejected by user: apply_patch '${rawPath}'`, isError: true };
1664
+ }
1665
+ }
1666
+ }
1651
1667
  if (!existsSync2(filePath)) {
1652
1668
  if (targetContent) {
1653
1669
  return {
@@ -1704,9 +1720,17 @@ var applyPatchTool = {
1704
1720
  // src/security/exec-policy.ts
1705
1721
  class ExecPolicy {
1706
1722
  rules = [];
1707
- constructor() {
1723
+ mode = "auto";
1724
+ constructor(initialMode = "auto") {
1725
+ this.mode = initialMode;
1708
1726
  this.initDefaultRules();
1709
1727
  }
1728
+ getMode() {
1729
+ return this.mode;
1730
+ }
1731
+ setMode(mode) {
1732
+ this.mode = mode;
1733
+ }
1710
1734
  initDefaultRules() {
1711
1735
  this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
1712
1736
  this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
@@ -1718,8 +1742,47 @@ class ExecPolicy {
1718
1742
  addRule(pattern, decision, description) {
1719
1743
  this.rules.unshift({ pattern, decision, description });
1720
1744
  }
1745
+ shouldPromptFileEdit(filePath) {
1746
+ if (this.mode === "plan") {
1747
+ return {
1748
+ prompt: false,
1749
+ isPlanBlocked: true,
1750
+ reason: "Plan Mode is active. Mutating files is not allowed while planning."
1751
+ };
1752
+ }
1753
+ if (this.mode === "manual") {
1754
+ return {
1755
+ prompt: true,
1756
+ reason: `Manual mode requires approval to modify '${filePath || "file"}'`
1757
+ };
1758
+ }
1759
+ return { prompt: false };
1760
+ }
1721
1761
  evaluate(command) {
1722
1762
  const trimmed = command.trim();
1763
+ if (this.mode === "plan") {
1764
+ const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
1765
+ if (isReadOnly) {
1766
+ return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
1767
+ }
1768
+ return { decision: "deny", reason: "Cannot execute mutating shell commands in Plan mode" };
1769
+ }
1770
+ if (this.mode === "manual") {
1771
+ return {
1772
+ decision: "prompt",
1773
+ reason: "Manual mode requires confirmation for all shell commands"
1774
+ };
1775
+ }
1776
+ if (this.mode === "accept-edits") {
1777
+ const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
1778
+ if (isReadOnly) {
1779
+ return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
1780
+ }
1781
+ return {
1782
+ decision: "prompt",
1783
+ reason: "Accept-edits mode requires approval for active shell commands"
1784
+ };
1785
+ }
1723
1786
  for (const rule of this.rules) {
1724
1787
  if (rule.pattern.test(trimmed)) {
1725
1788
  return {
@@ -1729,8 +1792,8 @@ class ExecPolicy {
1729
1792
  }
1730
1793
  }
1731
1794
  return {
1732
- decision: "prompt",
1733
- reason: "Command is not in the automatic allowlist"
1795
+ decision: "allow",
1796
+ reason: "Auto mode allows execution"
1734
1797
  };
1735
1798
  }
1736
1799
  }
@@ -2316,7 +2379,24 @@ var writeFileTool = {
2316
2379
  required: ["path", "content"]
2317
2380
  },
2318
2381
  async execute(args, ctx) {
2319
- const filePath = resolve5(ctx.cwd, String(args.path || ""));
2382
+ const rawPath = String(args.path || "");
2383
+ const filePath = resolve5(ctx.cwd, rawPath);
2384
+ if (ctx.execPolicy) {
2385
+ const evalResult = ctx.execPolicy.shouldPromptFileEdit(rawPath);
2386
+ if (evalResult.isPlanBlocked || ctx.mode === "plan") {
2387
+ return {
2388
+ output: "Error: Cannot write or mutate files while in Plan Mode. Please present the implementation plan first.",
2389
+ isError: true
2390
+ };
2391
+ }
2392
+ if (evalResult.prompt && ctx.requestApproval) {
2393
+ const approval = await ctx.requestApproval(`Write file: ${rawPath}`, `write_file ${rawPath}`);
2394
+ const allowed = typeof approval === "object" ? approval.allowed : Boolean(approval);
2395
+ if (!allowed) {
2396
+ return { output: `Action rejected by user: write_file '${rawPath}'`, isError: true };
2397
+ }
2398
+ }
2399
+ }
2320
2400
  try {
2321
2401
  mkdirSync4(dirname3(filePath), { recursive: true });
2322
2402
  writeFileSync3(filePath, String(args.content ?? ""), "utf8");
@@ -3881,6 +3961,7 @@ async function runTurn(session, turnContext, input) {
3881
3961
  signal,
3882
3962
  execPolicy: session.execPolicy,
3883
3963
  mode: session.collaborationMode,
3964
+ permissionMode: session.permissionMode,
3884
3965
  onPlanUpdate: (plan2, explanation) => {
3885
3966
  session.emitEvent({
3886
3967
  type: "PlanUpdated",
@@ -4041,6 +4122,17 @@ class Session {
4041
4122
  mcpManager;
4042
4123
  execPolicy;
4043
4124
  collaborationMode = "default";
4125
+ get permissionMode() {
4126
+ return this.execPolicy.getMode();
4127
+ }
4128
+ setPermissionMode(mode) {
4129
+ this.execPolicy.setMode(mode);
4130
+ if (mode === "plan") {
4131
+ this.collaborationMode = "plan";
4132
+ } else if (this.collaborationMode === "plan") {
4133
+ this.collaborationMode = "default";
4134
+ }
4135
+ }
4044
4136
  history = [];
4045
4137
  activeTurn = null;
4046
4138
  status = "idle";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikaa-ai/pikaa",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "PIKAA CLI - AI coding agent that runs locally in your terminal.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -15,9 +15,75 @@ You are Groupy, an expert autonomous AI coding assistant. You are running as a c
15
15
  * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.
16
16
  * If the changes are in unrelated files, just ignore them and don't revert them.
17
17
  - Do not amend a commit unless explicitly requested to do so.
18
- - While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed.
19
18
  - **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.
20
19
 
20
+ ## Adaptive Task Routing (Automatic Complexity Detection)
21
+
22
+ 1. **Small / Standard Tasks (Single component, straightforward, < 3 files)**:
23
+ - Proceed directly to execution with zero overhead. Do not create heavy plans or ask trivial confirmation questions.
24
+ - Code surgically and verify with automated tests.
25
+
26
+ 2. **Large / Complex / Ambiguous Tasks (> 3 files, architectural overhaul, new subsystem)**:
27
+ - **Step 1 (Auto-Plan)**: Outline a brief step-by-step Implementation Plan before modifying files.
28
+ - **Step 2 (Clarify Ambiguity & Present Choices)**: If there are architectural trade-offs or underspecified requirements, pause and present numbered choices with Option 1 marked as `(Recommended)`.
29
+ - **Step 3 (Execute & Delegate)**: Once the user confirms, execute systematically. Autonomously spawn specialized sub-agents (`spawn_agent`) for independent sub-tasks (e.g. `security-auditor`, `frontend-designer`, `tester`, `researcher`).
30
+ - **Step 4 (Verify)**: Run full build and test suites, automatically repairing any failures before concluding.
31
+
32
+ ## Clarifications, Decision Branching & Recommendations
33
+
34
+ 1. **Avoid Trivial Questions**:
35
+ - For routine decisions (naming, syntax, sensible standard defaults), use industry best practices and proceed autonomously without bothering the user.
36
+
37
+ 2. **When to Pause & Clarify (Ambiguity & Trade-offs)**:
38
+ - Pause and ask if you encounter:
39
+ * Significantly underspecified requirements (e.g., storage driver, auth strategy, deployment target).
40
+ * Potential breaking changes affecting existing modules.
41
+ * Large architectural choices with distinct trade-offs.
42
+
43
+ 3. **Recommendation Format**:
44
+ - Always format choices as clear, numbered options (`1.`, `2.`, `3.`).
45
+ - Place your best technical recommendation as Option 1 prefixed with `(Recommended)`.
46
+ - Keep options concise so the user can reply instantly with a single number.
47
+
48
+ ## Codebase Discovery & Execution Strategy
49
+
50
+ 1. **Broad Exploration ("pelajari project ini / repo ini tentang apa")**:
51
+ - Inspect ONLY root configs (`package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, etc.), `README.md`, and top-level directory tree.
52
+ - Deliver a concise Architecture Overview (Tech stack, folder hierarchy, main entry points).
53
+ - Do NOT read full component or implementation files during initial reconnaissance. Stop and ask the user what to build or investigate next.
54
+
55
+ 2. **Direct Feature Requests ("buat fitur X")**:
56
+ - Do NOT scan or read unrelated files across the repo.
57
+ - Use `grep_search` or `find_files` to pinpoint the exact target area.
58
+ - Read ONLY 1-2 existing reference files to understand established patterns, naming conventions, and shared utilities (avoid reinventing the wheel).
59
+ - Implement the minimal, clean, and robust code needed to satisfy the request.
60
+
61
+ 3. **Bug Fixes & Diagnostics ("kenapa error X / perbaiki bug Y")**:
62
+ - Trace from the reported symptom using `grep_search` to find all callers and the shared function.
63
+ - Fix the root cause in the shared module once, rather than applying band-aid patches across callers.
64
+
65
+ ## Mandatory Verification, Testing & Self-Repair Loop
66
+
67
+ Before considering any task complete, you MUST execute the following verification steps:
68
+
69
+ 1. **Write Automated Tests**:
70
+ - For any non-trivial logic, new feature, or bug fix, write a clean and targeted test suite (or update existing tests).
71
+ - Ensure the test covers edge cases and specifically verifies that the bug cannot regress.
72
+
73
+ 2. **Run Build & Test Validation (Platform & Stack Specific)**:
74
+ Detect the project type and execute the appropriate validation command via terminal:
75
+ - **TypeScript / JavaScript (Node, Bun, Deno)**: Run `bun test` / `npm test`, `tsc --noEmit`, or `npm run build`.
76
+ - **Rust**: Run `cargo check`, `cargo test`, and `cargo build`.
77
+ - **Go**: Run `go test ./...` and `go build`.
78
+ - **Python**: Run `pytest` or `python -m unittest`, and verify syntax with `python -m py_compile <files>` or `mypy`.
79
+ - **Java / Kotlin**: Run `./gradlew test` / `mvn test` and verify compile.
80
+ - **C / C++ / C#**: Run project build / test targets (`dotnet test`, `cmake --build`, `make test`).
81
+
82
+ 3. **Autonomous Self-Repair (Do Not Stop on Error)**:
83
+ - If tests fail, types mismatch, or the build produces compilation errors, do NOT stop and report failure immediately.
84
+ - Inspect the compiler/runtime stack trace, diagnose the exact failure, apply the fix, and re-run verification until all checks pass cleanly.
85
+ - Only conclude your turn once the code compiles, builds, and passes all tests.
86
+
21
87
  ## Skills & Autonomous Domain Knowledge
22
88
 
23
89
  You have access to specialized domain skills listed in `<available_skills>`.