@cydm/pie 1.0.37 → 1.0.38

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/README.md CHANGED
@@ -309,6 +309,8 @@ Pie exposes one default model-visible web tool: `web_research`. Use it for publi
309
309
 
310
310
  Pie's default tools operate from the current workspace. File tools are workspace-scoped, `~/.pie` is allowlisted for Pie configuration/session support, and shell commands return structured timeout/truncation/error details. The canonical shell tool is still named `bash` for compatibility; on Windows `shell:auto` uses PowerShell, and callers can explicitly request `bash`, `powershell`, or `cmd`. High-risk shell commands require confirmation outside YOLO mode. `/settings/yolo` relaxes filesystem sandbox directory restrictions and should only be used in trusted workspaces. `pie permissions` prints the current policy-derived safety summary.
311
311
 
312
+ In interactive Plan mode (`/tools/plan`), workspace editing tools are unavailable. The `bash` guard accepts simple read commands and read-only pipelines such as `rg 'TODO' . | head -20`; it blocks redirects, command substitution, and write-capable subcommands. Use `plan_scratch` to write, read, or list temporary text drafts in a directory dedicated to the current session. Shell redirection to `/tmp` remains blocked in Plan mode. Leave Plan mode before changing workspace files.
313
+
312
314
  ## Code Intelligence
313
315
 
314
316
  `code_intel` provides read-only JS/TS diagnostics, definition, references, symbols, and hover. CLI uses an isolated TypeScript worker by default and falls back to bounded lightweight text analysis on worker startup/OOM/crash/timeout failures; aborts, permission errors, and path escapes do not fallback. Unity uses a Node sidecar contract for TypeScript intelligence so the PuerTS bundle does not import Node-only parser dependencies.
@@ -1,9 +1,9 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  createAskUserCapability
4
- } from "../../../chunks/chunk-MGZHZSHB.js";
5
- import "../../../chunks/chunk-4B42ZU4B.js";
6
- import "../../../chunks/chunk-64XJIKVK.js";
4
+ } from "../../../chunks/chunk-2PLCSYFF.js";
5
+ import "../../../chunks/chunk-NTFKTUOW.js";
6
+ import "../../../chunks/chunk-2WHIT2UB.js";
7
7
  import "../../../chunks/chunk-TG2EQLX2.js";
8
8
 
9
9
  // builtin/extensions/ask-user/index.ts
@@ -7,14 +7,23 @@ import {
7
7
  isPlanModeSafeCommand,
8
8
  markCompletedPlanSteps,
9
9
  restoreExecutionState
10
- } from "../../../chunks/chunk-MGZHZSHB.js";
11
- import "../../../chunks/chunk-4B42ZU4B.js";
12
- import "../../../chunks/chunk-64XJIKVK.js";
10
+ } from "../../../chunks/chunk-2PLCSYFF.js";
11
+ import "../../../chunks/chunk-NTFKTUOW.js";
12
+ import {
13
+ Type
14
+ } from "../../../chunks/chunk-2WHIT2UB.js";
13
15
  import "../../../chunks/chunk-TG2EQLX2.js";
14
16
 
15
17
  // builtin/extensions/plan-mode/index.ts
18
+ import { randomUUID } from "node:crypto";
19
+ import { promises as fs, constants as fsConstants } from "node:fs";
20
+ import os from "node:os";
21
+ import path from "node:path";
16
22
  var SESSION_METADATA_KEY = "planModeEnabled";
17
23
  var EXECUTION_METADATA_KEY = "executionState";
24
+ var SCRATCH_METADATA_KEY = "planScratchId";
25
+ var SCRATCH_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
26
+ var MAX_SCRATCH_CHARS = 64e3;
18
27
  var planModeEnabled = false;
19
28
  var executionMode = false;
20
29
  function toPlanTodoItems(state) {
@@ -26,6 +35,62 @@ function toPlanTodoItems(state) {
26
35
  }
27
36
  function planModeExtension(ctx) {
28
37
  const capability = createPlanCapability();
38
+ function scratchDirectory() {
39
+ const metadata = ctx.getSessionMetadata();
40
+ let id = metadata?.[SCRATCH_METADATA_KEY];
41
+ if (typeof id !== "string" || !/^[0-9a-f-]{36}$/.test(id)) {
42
+ id = randomUUID();
43
+ ctx.setSessionMetadata(SCRATCH_METADATA_KEY, id);
44
+ }
45
+ return path.join(os.tmpdir(), `pie-plan-${id}`);
46
+ }
47
+ ctx.registerTool({
48
+ name: "plan_scratch",
49
+ label: "plan_scratch",
50
+ description: "Read, write, or list session-local temporary text drafts while planning. Use a simple filename; files stay outside the workspace. Shell output redirection is blocked in Plan mode.",
51
+ parameters: Type.Object({
52
+ action: Type.Union([Type.Literal("write"), Type.Literal("read"), Type.Literal("list")]),
53
+ name: Type.Optional(Type.String({ description: "Simple filename within this session's scratch directory" })),
54
+ content: Type.Optional(Type.String({ description: "Text to write (maximum 64,000 characters)" }))
55
+ }),
56
+ risk: { riskClass: "session_mutation", permissionScope: "session", mutatesSession: true, availableInPlanMode: true },
57
+ async execute(args) {
58
+ const reply = (text, isError = false) => ({ content: [{ type: "text", text }], isError });
59
+ try {
60
+ const directory = scratchDirectory();
61
+ await fs.mkdir(directory, { recursive: true, mode: 448 });
62
+ if (!(await fs.lstat(directory)).isDirectory()) return reply("Temporary draft directory is invalid.", true);
63
+ if (args.action === "list") {
64
+ const names = (await fs.readdir(directory)).filter((name) => SCRATCH_NAME.test(name)).sort();
65
+ return reply(names.length ? names.join("\n") : "No temporary drafts in this session.");
66
+ }
67
+ if (!args.name || !SCRATCH_NAME.test(args.name) || args.name === "." || args.name === "..") {
68
+ return reply("Invalid draft filename. Use letters, digits, dots, underscores, or hyphens without directories.", true);
69
+ }
70
+ const filename = path.join(directory, args.name);
71
+ if (args.action === "read") {
72
+ const handle2 = await fs.open(filename, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
73
+ try {
74
+ return reply(await handle2.readFile("utf8"));
75
+ } finally {
76
+ await handle2.close();
77
+ }
78
+ }
79
+ if (typeof args.content !== "string" || args.content.length > MAX_SCRATCH_CHARS) {
80
+ return reply(`Draft content must be text of at most ${MAX_SCRATCH_CHARS} characters.`, true);
81
+ }
82
+ const handle = await fs.open(filename, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | (fsConstants.O_NOFOLLOW || 0), 384);
83
+ try {
84
+ await handle.writeFile(args.content, "utf8");
85
+ } finally {
86
+ await handle.close();
87
+ }
88
+ return reply(`Saved ${args.name} (${args.content.length} characters).`);
89
+ } catch (error) {
90
+ return reply(`Temporary draft failed: ${error instanceof Error ? error.message : String(error)}`, true);
91
+ }
92
+ }
93
+ });
29
94
  function readPersistedPlanMode() {
30
95
  return ctx.getSessionMetadata()?.[SESSION_METADATA_KEY] === true;
31
96
  }
@@ -45,7 +110,7 @@ function planModeExtension(ctx) {
45
110
  const completed = state.steps.filter((step) => step.status === "completed").length;
46
111
  ctx.ui.notify(`\u{1F4CB} Plan: ${completed}/${state.steps.length} completed`, "info");
47
112
  } else if (planModeEnabled) {
48
- ctx.ui.notify("\u23F8\uFE0F Plan mode active (read-only)", "info");
113
+ ctx.ui.notify("\u23F8\uFE0F Plan mode active (workspace read-only; session drafts available)", "info");
49
114
  }
50
115
  }
51
116
  function applyPlanModeState(enabled, forcePromptInjection = false) {
@@ -84,11 +149,11 @@ function planModeExtension(ctx) {
84
149
  const command = event.args?.command;
85
150
  if (command && !isPlanModeSafeCommand(command)) {
86
151
  ctx.ui.notify(
87
- `Plan mode: Command blocked (not allowlisted). Use /plan to disable plan mode first.
152
+ `Plan mode: Command blocked. Use read-only commands, or plan_scratch for temporary drafts. Use /plan to leave Plan mode.
88
153
  Command: ${command}`,
89
154
  "error"
90
155
  );
91
- return { block: true, reason: `Plan mode: command "${command}" is not in the allowlist. Disable plan mode with /plan to run this command.` };
156
+ return { block: true, reason: `Plan mode: command "${command}" is not a read-only shell command. Use plan_scratch for temporary drafts, or /plan to leave Plan mode.` };
92
157
  }
93
158
  });
94
159
  ctx.on("agent:start", async () => {
@@ -1,13 +1,13 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  createCliHostCapabilities
4
- } from "../../../chunks/chunk-QXTAEPLR.js";
4
+ } from "../../../chunks/chunk-IKIEQTVJ.js";
5
5
  import {
6
6
  createSharedFileSystemTools,
7
7
  createSubagentCapability
8
- } from "../../../chunks/chunk-MGZHZSHB.js";
9
- import "../../../chunks/chunk-4B42ZU4B.js";
10
- import "../../../chunks/chunk-64XJIKVK.js";
8
+ } from "../../../chunks/chunk-2PLCSYFF.js";
9
+ import "../../../chunks/chunk-NTFKTUOW.js";
10
+ import "../../../chunks/chunk-2WHIT2UB.js";
11
11
  import "../../../chunks/chunk-TG2EQLX2.js";
12
12
 
13
13
  // builtin/extensions/subagent/index.ts
@@ -7,9 +7,9 @@ import {
7
7
  executeManageTodoList,
8
8
  executionStateToTodos,
9
9
  restoreExecutionState
10
- } from "../../../chunks/chunk-MGZHZSHB.js";
11
- import "../../../chunks/chunk-4B42ZU4B.js";
12
- import "../../../chunks/chunk-64XJIKVK.js";
10
+ } from "../../../chunks/chunk-2PLCSYFF.js";
11
+ import "../../../chunks/chunk-NTFKTUOW.js";
12
+ import "../../../chunks/chunk-2WHIT2UB.js";
13
13
  import "../../../chunks/chunk-TG2EQLX2.js";
14
14
 
15
15
  // builtin/extensions/todo/index.ts
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  Agent
4
- } from "./chunk-4B42ZU4B.js";
4
+ } from "./chunk-NTFKTUOW.js";
5
5
  import {
6
6
  FileSystemGateway,
7
7
  Type,
@@ -11,7 +11,241 @@ import {
11
11
  getPlatformConfig,
12
12
  hashText,
13
13
  streamSimple
14
- } from "./chunk-64XJIKVK.js";
14
+ } from "./chunk-2WHIT2UB.js";
15
+ import {
16
+ __commonJS,
17
+ __toESM
18
+ } from "./chunk-TG2EQLX2.js";
19
+
20
+ // ../../node_modules/shell-quote/quote.js
21
+ var require_quote = __commonJS({
22
+ "../../node_modules/shell-quote/quote.js"(exports, module) {
23
+ "use strict";
24
+ module.exports = function quote(xs) {
25
+ return xs.map(function(s) {
26
+ if (s === "") {
27
+ return "''";
28
+ }
29
+ if (s && typeof s === "object") {
30
+ return s.op.replace(/(.)/g, "\\$1");
31
+ }
32
+ if (/["\s\\]/.test(s) && !/'/.test(s)) {
33
+ return "'" + s.replace(/(['])/g, "\\$1") + "'";
34
+ }
35
+ if (/["'\s]/.test(s)) {
36
+ return '"' + s.replace(/(["\\$`!])/g, "\\$1") + '"';
37
+ }
38
+ return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2");
39
+ }).join(" ");
40
+ };
41
+ }
42
+ });
43
+
44
+ // ../../node_modules/shell-quote/parse.js
45
+ var require_parse = __commonJS({
46
+ "../../node_modules/shell-quote/parse.js"(exports, module) {
47
+ "use strict";
48
+ var CONTROL = "(?:" + [
49
+ "\\|\\|",
50
+ "\\&\\&",
51
+ ";;",
52
+ "\\|\\&",
53
+ "\\<\\(",
54
+ "\\<\\<\\<",
55
+ ">>",
56
+ ">\\&",
57
+ "<\\&",
58
+ "[&;()|<>]"
59
+ ].join("|") + ")";
60
+ var controlRE = new RegExp("^" + CONTROL + "$");
61
+ var META = "|&;()<> \\t";
62
+ var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"';
63
+ var DOUBLE_QUOTE = "'((\\\\'|[^'])*?)'";
64
+ var hash = /^#$/;
65
+ var SQ = "'";
66
+ var DQ = '"';
67
+ var DS = "$";
68
+ var TOKEN = "";
69
+ var mult = 4294967296;
70
+ for (i = 0; i < 4; i++) {
71
+ TOKEN += (mult * Math.random()).toString(16);
72
+ }
73
+ var i;
74
+ var startsWithToken = new RegExp("^" + TOKEN);
75
+ function matchAll(s, r) {
76
+ var origIndex = r.lastIndex;
77
+ var matches = [];
78
+ var matchObj;
79
+ while (matchObj = r.exec(s)) {
80
+ matches.push(matchObj);
81
+ if (r.lastIndex === matchObj.index) {
82
+ r.lastIndex += 1;
83
+ }
84
+ }
85
+ r.lastIndex = origIndex;
86
+ return matches;
87
+ }
88
+ function getVar(env, pre, key) {
89
+ var r = typeof env === "function" ? env(key) : env[key];
90
+ if (typeof r === "undefined" && key != "") {
91
+ r = "";
92
+ } else if (typeof r === "undefined") {
93
+ r = "$";
94
+ }
95
+ if (typeof r === "object") {
96
+ return pre + TOKEN + JSON.stringify(r) + TOKEN;
97
+ }
98
+ return pre + r;
99
+ }
100
+ function parseInternal(string, env, opts) {
101
+ if (!opts) {
102
+ opts = {};
103
+ }
104
+ var BS = opts.escape || "\\";
105
+ var BAREWORD = "(\\" + BS + `['"` + META + `]|[^\\s'"` + META + "])+";
106
+ var chunker = new RegExp([
107
+ "(" + CONTROL + ")",
108
+ // control chars
109
+ "(" + BAREWORD + "|" + SINGLE_QUOTE + "|" + DOUBLE_QUOTE + ")+"
110
+ ].join("|"), "g");
111
+ var matches = matchAll(string, chunker);
112
+ if (matches.length === 0) {
113
+ return [];
114
+ }
115
+ if (!env) {
116
+ env = {};
117
+ }
118
+ var commented = false;
119
+ return matches.map(function(match2) {
120
+ var s = match2[0];
121
+ if (!s || commented) {
122
+ return void 0;
123
+ }
124
+ if (controlRE.test(s)) {
125
+ return { op: s };
126
+ }
127
+ var quote = false;
128
+ var esc = false;
129
+ var out = "";
130
+ var isGlob = false;
131
+ var i2;
132
+ function parseEnvVar() {
133
+ i2 += 1;
134
+ var varend;
135
+ var varname;
136
+ var char = s.charAt(i2);
137
+ if (char === "{") {
138
+ i2 += 1;
139
+ if (s.charAt(i2) === "}") {
140
+ throw new Error("Bad substitution: " + s.slice(i2 - 2, i2 + 1));
141
+ }
142
+ varend = s.indexOf("}", i2);
143
+ if (varend < 0) {
144
+ throw new Error("Bad substitution: " + s.slice(i2));
145
+ }
146
+ varname = s.slice(i2, varend);
147
+ i2 = varend;
148
+ } else if (/[*@#?$!_-]/.test(char)) {
149
+ varname = char;
150
+ i2 += 1;
151
+ } else {
152
+ var slicedFromI = s.slice(i2);
153
+ varend = slicedFromI.match(/[^\w\d_]/);
154
+ if (!varend) {
155
+ varname = slicedFromI;
156
+ i2 = s.length;
157
+ } else {
158
+ varname = slicedFromI.slice(0, varend.index);
159
+ i2 += varend.index - 1;
160
+ }
161
+ }
162
+ return getVar(env, "", varname);
163
+ }
164
+ for (i2 = 0; i2 < s.length; i2++) {
165
+ var c = s.charAt(i2);
166
+ isGlob = isGlob || !quote && (c === "*" || c === "?");
167
+ if (esc) {
168
+ out += c;
169
+ esc = false;
170
+ } else if (quote) {
171
+ if (c === quote) {
172
+ quote = false;
173
+ } else if (quote == SQ) {
174
+ out += c;
175
+ } else {
176
+ if (c === BS) {
177
+ i2 += 1;
178
+ c = s.charAt(i2);
179
+ if (c === DQ || c === BS || c === DS) {
180
+ out += c;
181
+ } else {
182
+ out += BS + c;
183
+ }
184
+ } else if (c === DS) {
185
+ out += parseEnvVar();
186
+ } else {
187
+ out += c;
188
+ }
189
+ }
190
+ } else if (c === DQ || c === SQ) {
191
+ quote = c;
192
+ } else if (controlRE.test(c)) {
193
+ return { op: s };
194
+ } else if (hash.test(c)) {
195
+ commented = true;
196
+ var commentObj = { comment: string.slice(match2.index + i2 + 1) };
197
+ if (out.length) {
198
+ return [out, commentObj];
199
+ }
200
+ return [commentObj];
201
+ } else if (c === BS) {
202
+ esc = true;
203
+ } else if (c === DS) {
204
+ out += parseEnvVar();
205
+ } else {
206
+ out += c;
207
+ }
208
+ }
209
+ if (isGlob) {
210
+ return { op: "glob", pattern: out };
211
+ }
212
+ return out;
213
+ }).reduce(function(prev, arg) {
214
+ return typeof arg === "undefined" ? prev : prev.concat(arg);
215
+ }, []);
216
+ }
217
+ module.exports = function parse2(s, env, opts) {
218
+ var mapped = parseInternal(s, env, opts);
219
+ if (typeof env !== "function") {
220
+ return mapped;
221
+ }
222
+ return mapped.reduce(function(acc, s2) {
223
+ if (typeof s2 === "object") {
224
+ return acc.concat(s2);
225
+ }
226
+ var xs = s2.split(RegExp("(" + TOKEN + ".*?" + TOKEN + ")", "g"));
227
+ if (xs.length === 1) {
228
+ return acc.concat(xs[0]);
229
+ }
230
+ return acc.concat(xs.filter(Boolean).map(function(x) {
231
+ if (startsWithToken.test(x)) {
232
+ return JSON.parse(x.split(TOKEN)[1]);
233
+ }
234
+ return x;
235
+ }));
236
+ }, []);
237
+ };
238
+ }
239
+ });
240
+
241
+ // ../../node_modules/shell-quote/index.js
242
+ var require_shell_quote = __commonJS({
243
+ "../../node_modules/shell-quote/index.js"(exports) {
244
+ "use strict";
245
+ exports.quote = require_quote();
246
+ exports.parse = require_parse();
247
+ }
248
+ });
15
249
 
16
250
  // ../../packages/shared-headless-capabilities/src/loop.ts
17
251
  function nowIso() {
@@ -445,6 +679,7 @@ var BUILTIN_TOOL_CAPABILITY_METADATA = {
445
679
  unity_log_read: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true, maxOutputChars: 1e5 },
446
680
  unity_scene_object_edit: { riskClass: "external_side_effect", concurrencySafe: false, requiresPermission: true, permissionScope: "unity", externalSideEffect: true, highRisk: true },
447
681
  unity_refresh: { riskClass: "external_side_effect", concurrencySafe: false, permissionScope: "unity", externalSideEffect: true },
682
+ unity_tool_call: { riskClass: "external_side_effect", concurrencySafe: false, requiresPermission: true, permissionScope: "unity", externalSideEffect: true, highRisk: true },
448
683
  unity_script_run: { riskClass: "external_side_effect", concurrencySafe: false, requiresPermission: true, permissionScope: "unity", externalSideEffect: true, highRisk: true }
449
684
  };
450
685
  function getToolCapabilityMetadata(tool) {
@@ -493,39 +728,27 @@ function selectToolsForRuntimePolicy(tools, mode) {
493
728
  var DEFAULT_PLAN_MODE_TOOL_NAMES = Object.entries(BUILTIN_TOOL_CAPABILITY_METADATA).filter(([, metadata]) => metadata.availableInPlanMode).map(([name]) => name);
494
729
 
495
730
  // ../../packages/shared-headless-capabilities/src/plan.ts
496
- var SAFE_PREFIXES = [
497
- "cat ",
498
- "ls ",
499
- "find_files ",
500
- "grep_text ",
501
- "head ",
502
- "tail ",
503
- "echo ",
504
- "pwd",
505
- "cd ",
506
- "which ",
507
- "git status",
508
- "git log",
509
- "git diff",
510
- "git show",
511
- "git branch -a",
512
- "git remote -v",
513
- "git config --list",
514
- "npm list",
515
- "npm view"
516
- ];
517
- var DANGEROUS_PATTERNS = [
518
- /\brm\s+-[rf]*/i,
519
- />[\s]*\/dev\//,
520
- /\|.*\brm\b/,
521
- /git\s+push/i,
522
- /git\s+reset/i,
523
- /git\s+checkout\s+-/,
524
- /git\s+revert/i,
525
- /npm\s+publish/i,
526
- /npm\s+uninstall/i,
527
- /npm\s+rm/i
528
- ];
731
+ var import_shell_quote = __toESM(require_shell_quote(), 1);
732
+ var SIMPLE_READ_COMMANDS = /* @__PURE__ */ new Set(["pwd", "ls", "cat", "head", "tail", "wc", "grep", "rg", "which", "type", "echo", "printf", "cd", "true", "false"]);
733
+ var GIT_READ_SUBCOMMANDS = /* @__PURE__ */ new Set(["status", "log", "diff", "show", "rev-parse", "ls-files", "ls-tree"]);
734
+ var UNSAFE_GIT_FLAGS = /^(?:-c|--config-env|--exec|--output|--ext-diff|--textconv)(?:=|$)/;
735
+ var UNSAFE_RG_FLAGS = /^(?:--pre|--pre-glob)(?:=|$)/;
736
+ function isReadOnlySegment(words) {
737
+ if (words.length === 0) return false;
738
+ const [program, ...args] = words;
739
+ if (program === "git") {
740
+ const subcommand = args[0] ?? "";
741
+ const isReadSubcommand = GIT_READ_SUBCOMMANDS.has(subcommand) || subcommand === "branch" && args.slice(1).every((arg) => ["-a", "--all", "-r", "--remotes", "--list"].includes(arg)) || subcommand === "remote" && args.length === 2 && args[1] === "-v" || subcommand === "config" && args.length === 2 && args[1] === "--list";
742
+ return isReadSubcommand && !args.some((arg) => UNSAFE_GIT_FLAGS.test(arg) || arg === "--no-pager" || arg === "-c");
743
+ }
744
+ if (program === "npm") return ["list", "view"].includes(args[0] ?? "") && !args.some((arg) => arg === "--script-shell");
745
+ if (program === "find") {
746
+ return !args.some((arg) => /^-(?:exec|execdir|ok|okdir|delete|fprint|fls|fprintf)/.test(arg));
747
+ }
748
+ if (!SIMPLE_READ_COMMANDS.has(program)) return false;
749
+ if (program === "rg") return !args.some((arg) => UNSAFE_RG_FLAGS.test(arg) || arg === "--pre" || arg === "--pre-glob");
750
+ return true;
751
+ }
529
752
  function extractPlanTodoItems(text) {
530
753
  const items = [];
531
754
  const planMatch = text.match(/Plan:\s*\n?([\s\S]*?)(?=\n\n|\n[A-Z]|$)/i);
@@ -545,20 +768,23 @@ function extractPlanTodoItems(text) {
545
768
  }
546
769
  function isPlanModeSafeCommand(command) {
547
770
  const trimmed = command.trim();
548
- for (const pattern of DANGEROUS_PATTERNS) {
549
- if (pattern.test(trimmed)) {
550
- return false;
551
- }
771
+ if (!trimmed || /[$`\n\r\0]/.test(trimmed)) return false;
772
+ let tokens;
773
+ try {
774
+ tokens = (0, import_shell_quote.parse)(trimmed);
775
+ } catch {
776
+ return false;
552
777
  }
553
- for (const prefix of SAFE_PREFIXES) {
554
- if (trimmed.startsWith(prefix)) {
555
- return true;
778
+ const segment = [];
779
+ for (const token of tokens) {
780
+ if (typeof token === "string") {
781
+ segment.push(token);
782
+ continue;
556
783
  }
784
+ if (!("op" in token) || !["|", "&&", "||", ";"].includes(String(token.op)) || !isReadOnlySegment(segment)) return false;
785
+ segment.length = 0;
557
786
  }
558
- if (/^[a-z]+$/.test(trimmed)) {
559
- return ["pwd", "ls", "clear", "whoami"].includes(trimmed);
560
- }
561
- return false;
787
+ return isReadOnlySegment(segment);
562
788
  }
563
789
  function extractDoneSteps(text) {
564
790
  const steps = [];