@pikaa-ai/pikaa 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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");
@@ -5949,8 +6041,8 @@ function getPackageMetadata() {
5949
6041
  if (cachedMetadata) {
5950
6042
  return cachedMetadata;
5951
6043
  }
5952
- const envVersion = process.env.PIKAA_VERSION || process.env.GROUPY_VERSION || process.env.npm_package_version;
5953
- const envName = process.env.PIKAA_NAME || process.env.npm_package_name;
6044
+ const envVersion = process.env.PIKAA_VERSION || process.env.GROUPY_VERSION;
6045
+ const envName = process.env.PIKAA_NAME || process.env.GROUPY_NAME;
5954
6046
  if (envVersion) {
5955
6047
  cachedMetadata = {
5956
6048
  name: envName || "pikaa",
@@ -6003,7 +6095,7 @@ function getPackageMetadata() {
6003
6095
  } catch {}
6004
6096
  cachedMetadata = {
6005
6097
  name: "pikaa",
6006
- version: "0.3.2"
6098
+ version: "0.3.6"
6007
6099
  };
6008
6100
  return cachedMetadata;
6009
6101
  }
@@ -6445,10 +6537,41 @@ ${preview}${more}`);
6445
6537
  console.log();
6446
6538
  }
6447
6539
  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`;
6540
+ const cols = typeof process.stdout?.columns === "number" && process.stdout.columns > 0 ? process.stdout.columns : 80;
6541
+ const boxWidth = Math.max(20, cols - 2);
6542
+ const contentWidth = Math.max(10, boxWidth - 3);
6543
+ const BG = "\x1B[48;2;43;43;45m";
6544
+ const CHEVRON = "\x1B[38;2;128;128;133m";
6545
+ const TEXT = "\x1B[38;2;240;240;242m";
6546
+ const RESET2 = "\x1B[0m";
6547
+ const rawLines = text.split(`
6548
+ `);
6549
+ const wrappedLines = [];
6550
+ for (const rawLine of rawLines) {
6551
+ if (rawLine.length === 0) {
6552
+ wrappedLines.push("");
6553
+ continue;
6554
+ }
6555
+ let current = rawLine;
6556
+ while (current.length > contentWidth) {
6557
+ let breakIdx = current.lastIndexOf(" ", contentWidth);
6558
+ if (breakIdx <= 0) {
6559
+ breakIdx = contentWidth;
6560
+ }
6561
+ wrappedLines.push(current.slice(0, breakIdx));
6562
+ current = current.slice(breakIdx).trimStart();
6563
+ }
6564
+ if (current.length > 0) {
6565
+ wrappedLines.push(current);
6566
+ }
6567
+ }
6568
+ const formatted = wrappedLines.map((line, idx) => {
6569
+ const prefix = idx === 0 ? `${CHEVRON} \u276F ` : `${CHEVRON} `;
6570
+ const padLen = Math.max(0, contentWidth - line.length);
6571
+ return ` ${BG}${prefix}${TEXT}${line}${" ".repeat(padLen)}${RESET2}`;
6572
+ });
6573
+ return formatted.join(`
6574
+ `);
6452
6575
  }
6453
6576
  static formatClaudeAssistantResponse(text) {
6454
6577
  return ` \x1B[38;2;192;202;245m${text}\x1B[0m`;
@@ -6481,12 +6604,49 @@ function ensureKeypressInitialized() {
6481
6604
  class InteractiveLineEditor {
6482
6605
  promptSymbol;
6483
6606
  cwd;
6607
+ mode;
6484
6608
  onInterrupt;
6609
+ onModeChange;
6485
6610
  searchEngine = new FileSearchEngine;
6486
6611
  constructor(options = {}) {
6487
6612
  this.promptSymbol = options.promptSymbol || " \x1B[38;2;192;202;245m\u276F\x1B[0m ";
6488
6613
  this.cwd = options.cwd || process.cwd();
6614
+ this.mode = options.initialMode || "auto";
6489
6615
  this.onInterrupt = options.onInterrupt;
6616
+ this.onModeChange = options.onModeChange;
6617
+ }
6618
+ getMode() {
6619
+ return this.mode;
6620
+ }
6621
+ setMode(mode) {
6622
+ this.mode = mode;
6623
+ this.onModeChange?.(mode);
6624
+ }
6625
+ cycleMode() {
6626
+ const modes = ["auto", "manual", "accept-edits", "plan"];
6627
+ const nextIdx = (modes.indexOf(this.mode) + 1) % modes.length;
6628
+ const next = modes[nextIdx];
6629
+ this.setMode(next);
6630
+ return next;
6631
+ }
6632
+ cycleModeReverse() {
6633
+ const modes = ["auto", "manual", "accept-edits", "plan"];
6634
+ const nextIdx = (modes.indexOf(this.mode) - 1 + modes.length) % modes.length;
6635
+ const next = modes[nextIdx];
6636
+ this.setMode(next);
6637
+ return next;
6638
+ }
6639
+ getModeLine() {
6640
+ switch (this.mode) {
6641
+ case "auto":
6642
+ 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`;
6643
+ case "manual":
6644
+ return ` \x1B[38;2;148;148;148m\u23F8 manual mode on \xB7 ? for shortcuts \xB7 \u21E0 for agents\x1B[0m`;
6645
+ case "accept-edits":
6646
+ 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`;
6647
+ case "plan":
6648
+ 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`;
6649
+ }
6490
6650
  }
6491
6651
  async readLine() {
6492
6652
  if (!process.stdin.isTTY) {
@@ -6540,6 +6700,14 @@ class InteractiveLineEditor {
6540
6700
  return [];
6541
6701
  }
6542
6702
  };
6703
+ const getTerminalCols = () => {
6704
+ return typeof process.stdout?.columns === "number" && process.stdout.columns > 0 ? process.stdout.columns : 80;
6705
+ };
6706
+ const getRule = () => {
6707
+ const cols = getTerminalCols();
6708
+ const ruleLen = Math.max(10, cols - 4);
6709
+ return "\u2500".repeat(ruleLen);
6710
+ };
6543
6711
  const ensureVisible = (totalItems, visibleRows) => {
6544
6712
  if (totalItems === 0 || visibleRows === 0) {
6545
6713
  scrollTop = 0;
@@ -6567,37 +6735,39 @@ class InteractiveLineEditor {
6567
6735
  }
6568
6736
  };
6569
6737
  const redraw = () => {
6570
- clearMenu();
6738
+ process.stdout.write("\x1B[J");
6571
6739
  process.stdout.write(`\r\x1B[2K${this.promptSymbol}${buffer}`);
6572
6740
  const slashMatches = getMatchingCommands();
6573
6741
  const activeFile = getActiveFileQuery();
6574
6742
  const fileMatches = activeFile ? getMatchingFiles(activeFile.query) : [];
6575
6743
  if (buffer.startsWith("/") && !popupDismissed && slashMatches.length > 0) {
6576
- const BOX_WIDTH = 70;
6744
+ const termCols = getTerminalCols();
6745
+ const BOX_WIDTH = Math.max(20, Math.min(termCols - 4, 120));
6577
6746
  const maxVisible = Math.min(slashMatches.length, 7);
6578
6747
  ensureVisible(slashMatches.length, maxVisible);
6579
6748
  const visibleMatches = slashMatches.slice(scrollTop, scrollTop + maxVisible);
6580
6749
  const menuLines = [];
6581
- const rule = "\u2500".repeat(Math.min(BOX_WIDTH, 68));
6582
- const RULE_COLOR = "\x1B[38;2;80;80;88m";
6750
+ const rule = "\u2500".repeat(BOX_WIDTH);
6751
+ const RULE_COLOR2 = "\x1B[38;2;80;80;88m";
6583
6752
  const ACTIVE_COLOR = "\x1B[38;2;225;225;225m";
6584
6753
  const INACTIVE_COLOR = "\x1B[38;2;139;139;144m";
6585
6754
  const RESET2 = "\x1B[0m";
6586
- menuLines.push(` ${RULE_COLOR}${rule}${RESET2}`);
6755
+ menuLines.push(` ${RULE_COLOR2}${rule}${RESET2}`);
6756
+ const maxDescLen = Math.max(10, BOX_WIDTH - 22);
6587
6757
  for (let i = 0;i < visibleMatches.length; i++) {
6588
6758
  const cmd = visibleMatches[i];
6589
6759
  const actualIdx = scrollTop + i;
6590
6760
  const isSelected = actualIdx === selectedIndex;
6591
6761
  const marker = isSelected ? `${ACTIVE_COLOR}\u276F${RESET2}` : " ";
6592
6762
  const rawName = cmd.name.padEnd(16).slice(0, 16);
6593
- const rawDesc = cmd.description.length > 48 ? cmd.description.slice(0, 45) + "..." : cmd.description;
6763
+ const rawDesc = cmd.description.length > maxDescLen ? cmd.description.slice(0, maxDescLen - 3) + "..." : cmd.description;
6594
6764
  if (isSelected) {
6595
6765
  menuLines.push(` ${marker} \x1B[1m${ACTIVE_COLOR}${rawName}${RESET2} \x1B[1m${ACTIVE_COLOR}${rawDesc}${RESET2}`);
6596
6766
  } else {
6597
6767
  menuLines.push(` ${marker} ${INACTIVE_COLOR}${rawName}${RESET2} ${INACTIVE_COLOR}${rawDesc}${RESET2}`);
6598
6768
  }
6599
6769
  }
6600
- menuLines.push(` ${RULE_COLOR}${rule}${RESET2}`);
6770
+ menuLines.push(` ${RULE_COLOR2}${rule}${RESET2}`);
6601
6771
  for (const line of menuLines) {
6602
6772
  process.stdout.write(`
6603
6773
  \x1B[2K${line}`);
@@ -6605,18 +6775,20 @@ class InteractiveLineEditor {
6605
6775
  renderedMenuLines = menuLines.length;
6606
6776
  process.stdout.write(`\x1B[${renderedMenuLines}A`);
6607
6777
  } else if (activeFile && !popupDismissed && fileMatches.length > 0) {
6608
- const BOX_WIDTH = 70;
6778
+ const termCols = getTerminalCols();
6779
+ const BOX_WIDTH = Math.max(20, Math.min(termCols - 4, 120));
6609
6780
  const maxVisible = Math.min(fileMatches.length, 7);
6610
6781
  ensureVisible(fileMatches.length, maxVisible);
6611
6782
  const visibleMatches = fileMatches.slice(scrollTop, scrollTop + maxVisible);
6612
6783
  const menuLines = [];
6613
6784
  menuLines.push(` ${style.dim("\u250C\u2500\u2500")} ${style.brandBold("Files")} ${style.dim("\u2500".repeat(Math.max(10, BOX_WIDTH - 9)) + "\u2510")}`);
6785
+ const maxPathLen = Math.max(10, BOX_WIDTH - 8);
6614
6786
  for (let i = 0;i < visibleMatches.length; i++) {
6615
6787
  const filePath = visibleMatches[i];
6616
6788
  const actualIdx = scrollTop + i;
6617
6789
  const isSelected = actualIdx === selectedIndex;
6618
6790
  const marker = isSelected ? style.brand("\u276F") : " ";
6619
- const rawPath = filePath.length > 62 ? "..." + filePath.slice(filePath.length - 59) : filePath.padEnd(62);
6791
+ const rawPath = filePath.length > maxPathLen ? "..." + filePath.slice(filePath.length - (maxPathLen - 3)) : filePath.padEnd(maxPathLen);
6620
6792
  const coloredPath = isSelected ? style.brandBold(rawPath) : style.cyan(rawPath);
6621
6793
  menuLines.push(` ${style.dim("\u2502")} ${marker} ${coloredPath} ${style.dim("\u2502")}`);
6622
6794
  }
@@ -6641,6 +6813,15 @@ class InteractiveLineEditor {
6641
6813
  }
6642
6814
  renderedMenuLines = menuLines.length;
6643
6815
  process.stdout.write(`\x1B[${renderedMenuLines}A`);
6816
+ } else {
6817
+ const RULE_COLOR2 = "\x1B[38;2;60;60;68m";
6818
+ const bottomRule = ` ${RULE_COLOR2}${getRule()}\x1B[0m`;
6819
+ const modeLine = this.getModeLine();
6820
+ process.stdout.write(`
6821
+ \x1B[2K${bottomRule}
6822
+ \x1B[2K${modeLine}`);
6823
+ renderedMenuLines = 2;
6824
+ process.stdout.write(`\x1B[2A`);
6644
6825
  }
6645
6826
  const visiblePromptLength = this.promptSymbol.replace(/\x1b\[[0-9;]*m/g, "").length;
6646
6827
  const cursorCol = visiblePromptLength + cursor;
@@ -6650,7 +6831,16 @@ class InteractiveLineEditor {
6650
6831
  process.stdout.write("\r");
6651
6832
  }
6652
6833
  };
6834
+ const onResize = () => {
6835
+ redraw();
6836
+ };
6837
+ if (process.stdout && typeof process.stdout.on === "function") {
6838
+ process.stdout.on("resize", onResize);
6839
+ }
6653
6840
  const cleanupAndResolve = (result) => {
6841
+ if (process.stdout && typeof process.stdout.removeListener === "function") {
6842
+ process.stdout.removeListener("resize", onResize);
6843
+ }
6654
6844
  clearMenu();
6655
6845
  process.stdin.removeListener("keypress", onKeypress);
6656
6846
  if (process.stdin.isTTY) {
@@ -6659,11 +6849,11 @@ class InteractiveLineEditor {
6659
6849
  } catch {}
6660
6850
  }
6661
6851
  if (result.trim().length > 0 && !result.startsWith("/")) {
6662
- process.stdout.write(`\r\x1B[2K${CliFormatter.formatClaudeUserPrompt(result)}
6852
+ process.stdout.write(`\x1B[1A\r\x1B[2K${CliFormatter.formatClaudeUserPrompt(result)}
6663
6853
 
6664
6854
  `);
6665
6855
  } else {
6666
- process.stdout.write(`
6856
+ process.stdout.write(`\x1B[1A\r\x1B[2K
6667
6857
  `);
6668
6858
  }
6669
6859
  resolve18(result);
@@ -6681,6 +6871,9 @@ class InteractiveLineEditor {
6681
6871
  redraw();
6682
6872
  return;
6683
6873
  }
6874
+ if (process.stdout && typeof process.stdout.removeListener === "function") {
6875
+ process.stdout.removeListener("resize", onResize);
6876
+ }
6684
6877
  clearMenu();
6685
6878
  if (process.stdin.isTTY) {
6686
6879
  try {
@@ -6731,6 +6924,12 @@ class InteractiveLineEditor {
6731
6924
  cleanupAndResolve(buffer);
6732
6925
  return;
6733
6926
  }
6927
+ const isShiftTab = key.name === "tab" && Boolean(key.shift) || key.name === "backtab" || key.sequence === "\x1B[Z" || _str === "\x1B[Z";
6928
+ if (isShiftTab) {
6929
+ this.cycleMode();
6930
+ redraw();
6931
+ return;
6932
+ }
6734
6933
  if (key.name === "tab") {
6735
6934
  if (activeFile && !popupDismissed && fileMatches.length > 0) {
6736
6935
  const chosen = fileMatches[selectedIndex] || fileMatches[0];
@@ -6753,8 +6952,14 @@ class InteractiveLineEditor {
6753
6952
  scrollTop = 0;
6754
6953
  popupDismissed = false;
6755
6954
  redraw();
6955
+ return;
6756
6956
  }
6757
6957
  }
6958
+ if (buffer.trim().length === 0) {
6959
+ this.cycleMode();
6960
+ redraw();
6961
+ return;
6962
+ }
6758
6963
  return;
6759
6964
  }
6760
6965
  if (key.name === "up") {
@@ -6818,7 +7023,11 @@ class InteractiveLineEditor {
6818
7023
  }
6819
7024
  };
6820
7025
  process.stdin.on("keypress", onKeypress);
6821
- process.stdout.write(this.promptSymbol);
7026
+ const RULE_COLOR = "\x1B[38;2;60;60;68m";
7027
+ process.stdout.write(`
7028
+ ${RULE_COLOR}${getRule()}\x1B[0m
7029
+ `);
7030
+ redraw();
6822
7031
  });
6823
7032
  }
6824
7033
  }
@@ -7550,6 +7759,11 @@ var AVAILABLE_SLASH_COMMANDS = [
7550
7759
  { name: "/reasoning", description: "Toggle internal reasoning chain visibility" },
7551
7760
  { name: "/login", description: "Authenticate with backend provider" },
7552
7761
  { name: "/whoami", description: "Check backend authentication status" },
7762
+ { name: "/mode", description: "Cycle or set execution permission mode (auto, manual, accept-edits, plan)" },
7763
+ { name: "/auto", description: "Switch to Auto Mode (tools execute automatically)" },
7764
+ { name: "/manual", description: "Switch to Manual Mode (all tools require user confirmation)" },
7765
+ { name: "/accept-edits", description: "Switch to Accept Edits Mode (file edits auto-approved, shell prompts)" },
7766
+ { name: "/plan", description: "Switch to Plan Mode (read-only planning, mutations blocked)" },
7553
7767
  { name: "/skills", description: "List domain skills in workspace & global" },
7554
7768
  { name: "/memories", description: "View learned preferences & memories" },
7555
7769
  { name: "/worktrees", description: "List active isolated Git Worktrees" },
@@ -7601,6 +7815,37 @@ async function handleSlashCommand(input, ctx) {
7601
7815
  case "/roles":
7602
7816
  await handleRolesCommand(ctx, args[0]);
7603
7817
  return true;
7818
+ case "/mode":
7819
+ case "/modes":
7820
+ case "/permission":
7821
+ case "/permissions":
7822
+ await handleModeCommand(ctx, args[0]);
7823
+ return true;
7824
+ case "/auto":
7825
+ ctx.session.setPermissionMode("auto");
7826
+ console.log(`
7827
+ \x1B[38;2;255;215;0m\u23F5\u23F5 Switched to Auto Mode (tools execute automatically)\x1B[0m
7828
+ `);
7829
+ return true;
7830
+ case "/manual":
7831
+ ctx.session.setPermissionMode("manual");
7832
+ console.log(`
7833
+ \x1B[38;2;148;148;148m\u23F8 Switched to Manual Mode (all tools require user confirmation)\x1B[0m
7834
+ `);
7835
+ return true;
7836
+ case "/accept-edits":
7837
+ case "/acceptedits":
7838
+ ctx.session.setPermissionMode("accept-edits");
7839
+ console.log(`
7840
+ \x1B[38;2;175;175;215m\u23F5\u23F5 Switched to Accept Edits Mode (file edits auto-approved, shell prompts)\x1B[0m
7841
+ `);
7842
+ return true;
7843
+ case "/plan":
7844
+ ctx.session.setPermissionMode("plan");
7845
+ console.log(`
7846
+ \x1B[38;2;95;175;175m\u23F8 Switched to Plan Mode (read-only planning, mutations blocked)\x1B[0m
7847
+ `);
7848
+ return true;
7604
7849
  case "/agents":
7605
7850
  printAgents(ctx);
7606
7851
  return true;
@@ -8517,6 +8762,84 @@ function printReleaseNotes() {
8517
8762
  console.log(" " + ROSE2 + "\u2514" + "\u2500".repeat(totalInnerWidth) + "\u2518" + RESET2);
8518
8763
  console.log("");
8519
8764
  }
8765
+ async function handleModeCommand(ctx, arg) {
8766
+ const validModes = [
8767
+ {
8768
+ mode: "auto",
8769
+ title: "Auto Mode",
8770
+ desc: "Tools run automatically without confirmation (fastest autonomous workflow)",
8771
+ glyph: "\u23F5\u23F5",
8772
+ color: "\x1B[38;2;255;215;0m"
8773
+ },
8774
+ {
8775
+ mode: "manual",
8776
+ title: "Manual Mode",
8777
+ desc: "Every file edit and shell command asks for user confirmation",
8778
+ glyph: "\u23F8",
8779
+ color: "\x1B[38;2;148;148;148m"
8780
+ },
8781
+ {
8782
+ mode: "accept-edits",
8783
+ title: "Accept Edits Mode",
8784
+ desc: "File edits are auto-approved, but shell commands require approval",
8785
+ glyph: "\u23F5\u23F5",
8786
+ color: "\x1B[38;2;175;175;215m"
8787
+ },
8788
+ {
8789
+ mode: "plan",
8790
+ title: "Plan Mode",
8791
+ desc: "Read-only mode. Blocks all mutations and focuses strictly on planning",
8792
+ glyph: "\u23F8",
8793
+ color: "\x1B[38;2;95;175;175m"
8794
+ }
8795
+ ];
8796
+ const currentMode = ctx.session.permissionMode;
8797
+ if (arg) {
8798
+ const normalized = arg.trim().toLowerCase();
8799
+ const match = validModes.find((m) => m.mode === normalized || m.mode.replace("-", "") === normalized);
8800
+ if (match) {
8801
+ ctx.session.setPermissionMode(match.mode);
8802
+ console.log(`
8803
+ ${match.color}${match.glyph} Switched to ${match.title}\x1B[0m
8804
+ ${style.dim(match.desc)}
8805
+ `);
8806
+ return;
8807
+ }
8808
+ }
8809
+ if (!process.stdin.isTTY || false || !process.stdin.readable) {
8810
+ console.log(`
8811
+ Current Permission Mode: ${style.bold(currentMode)}`);
8812
+ for (const m of validModes) {
8813
+ const active = m.mode === currentMode ? " (active)" : "";
8814
+ console.log(` \u2022 ${m.title}${active}: ${m.desc}`);
8815
+ }
8816
+ console.log();
8817
+ return;
8818
+ }
8819
+ const items = validModes.map((m) => ({
8820
+ id: m.mode,
8821
+ label: `${m.glyph} ${m.title}`,
8822
+ badge: m.mode === currentMode ? "ACTIVE" : undefined,
8823
+ description: m.desc,
8824
+ checked: m.mode === currentMode
8825
+ }));
8826
+ const res = await promptInteractiveList({
8827
+ title: "\uD83C\uDF9B\uFE0F Select Execution Permission Mode (Shift+Tab in prompt to cycle)",
8828
+ items,
8829
+ mode: "select",
8830
+ customKeyHints: "\u2191/\u2193: navigate \xB7 Enter: switch mode \xB7 Esc: cancel"
8831
+ });
8832
+ if (res.action === "select" && res.selectedItem) {
8833
+ const chosen = validModes.find((m) => m.mode === res.selectedItem?.id);
8834
+ if (chosen) {
8835
+ ctx.session.setPermissionMode(chosen.mode);
8836
+ console.log(`
8837
+ ${chosen.color}${chosen.glyph} Switched to ${chosen.title}\x1B[0m
8838
+ ${style.dim(chosen.desc)}
8839
+ `);
8840
+ }
8841
+ }
8842
+ }
8520
8843
 
8521
8844
  // src/cli/ui/markdown.ts
8522
8845
  class MarkdownHighlighter {
@@ -9124,6 +9447,10 @@ class CliRepl {
9124
9447
  }).catch(() => {});
9125
9448
  const editor = new InteractiveLineEditor({
9126
9449
  cwd: this.session.cwd,
9450
+ initialMode: this.session.permissionMode,
9451
+ onModeChange: (newMode) => {
9452
+ this.session.setPermissionMode(newMode);
9453
+ },
9127
9454
  onInterrupt: () => {
9128
9455
  if (this.isProcessing) {
9129
9456
  const activeTurn = this.session.getActiveTurn();
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.5",
3
+ "version": "0.3.7",
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>`.