flavor-code 1.2.4 → 1.2.6

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.
@@ -104,10 +104,12 @@ function isDestructiveTool(name) {
104
104
  return DESTRUCTIVE_TOOLS.has(name);
105
105
  }
106
106
  var PermissionEngine = class {
107
+ #lexicalWorkspace;
107
108
  #workspace;
108
109
  #mode;
109
110
  constructor(options) {
110
111
  const root = resolve(options.workspace);
112
+ this.#lexicalWorkspace = root;
111
113
  this.#workspace = existsSync(root) ? realpathSync.native(root) : root;
112
114
  this.#mode = canonicalMode(options.mode ?? "default");
113
115
  }
@@ -128,13 +130,13 @@ var PermissionEngine = class {
128
130
  return { decision: "deny", reason: `${request.tool} requires source and destination paths` };
129
131
  }
130
132
  for (const path of paths) {
131
- const classification = classifyPath(this.#workspace, path);
133
+ const classification = classifyPath(this.#lexicalWorkspace, this.#workspace, path);
132
134
  if (classification.escape) return { decision: "deny", reason: classification.reason ?? "Path escapes the workspace" };
133
135
  if (request.agent === "subagent" && !classification.inside) {
134
136
  return { decision: "deny", reason: "Subagents are restricted to the workspace" };
135
137
  }
136
138
  }
137
- const inside = paths.every((path) => classifyPath(this.#workspace, path).inside);
139
+ const inside = paths.every((path) => classifyPath(this.#lexicalWorkspace, this.#workspace, path).inside);
138
140
  if (this.#mode === "plan") {
139
141
  return READ_TOOLS.has(request.tool) ? { decision: "allow" } : { decision: "deny", reason: "Plan mode is read-only" };
140
142
  }
@@ -161,11 +163,11 @@ var PermissionEngine = class {
161
163
  const analysis = request.args === void 0 ? analyzeCommand(request.command ?? "") : analyzeArgumentCommand(request.command ?? "", request.args);
162
164
  if (request.agent === "subagent") {
163
165
  if (request.cwd === void 0) return { decision: "ask", reason: "Subagent shell commands require an explicit workspace cwd" };
164
- const cwd = classifyPath(this.#workspace, request.cwd);
166
+ const cwd = classifyPath(this.#lexicalWorkspace, this.#workspace, request.cwd);
165
167
  if (cwd.escape || !cwd.inside) return { decision: "deny", reason: "Subagent shell cwd must remain in the workspace" };
166
168
  if (analysis.destructive) return { decision: "deny", reason: "Destructive commands are forbidden for subagents" };
167
169
  if (analysis.wrapped || analysis.opaque || !isRoutineCommand(analysis.command)) return { decision: "ask", reason: "Subagent shell command requires main-Agent approval" };
168
- const argumentDecision = assessRoutineArguments(analysis.command, request.cwd, this.#workspace);
170
+ const argumentDecision = assessRoutineArguments(analysis.command, request.cwd, this.#lexicalWorkspace, this.#workspace);
169
171
  if (argumentDecision !== "allow") {
170
172
  return argumentDecision === "deny" ? { decision: "deny", reason: "Subagent command arguments escape the workspace" } : { decision: "ask", reason: "Ambiguous subagent command arguments require main-Agent approval" };
171
173
  }
@@ -177,12 +179,12 @@ var PermissionEngine = class {
177
179
  if (analysis.wrapped || analysis.opaque) return ask(this.#mode, "Shell wrapper requires approval");
178
180
  if (this.#mode === "bypassPermissions") return { decision: "allow" };
179
181
  if ((this.#mode === "acceptEdits" || this.#mode === "auto") && isRoutineCommand(analysis.command)) {
180
- const cwd = request.cwd ?? this.#workspace;
181
- const cwdClassification = classifyPath(this.#workspace, cwd);
182
+ const cwd = request.cwd ?? this.#lexicalWorkspace;
183
+ const cwdClassification = classifyPath(this.#lexicalWorkspace, this.#workspace, cwd);
182
184
  if (cwdClassification.escape || !cwdClassification.inside) {
183
185
  return { decision: "deny", reason: "Routine command cwd must remain in the workspace" };
184
186
  }
185
- const argumentDecision = assessRoutineArguments(analysis.command, cwd, this.#workspace);
187
+ const argumentDecision = assessRoutineArguments(analysis.command, cwd, this.#lexicalWorkspace, this.#workspace);
186
188
  if (argumentDecision === "deny") return { decision: "deny", reason: "Routine command arguments escape the workspace" };
187
189
  if (argumentDecision === "ask") return ask(this.#mode, "Ambiguous routine command arguments require approval");
188
190
  return { decision: "allow" };
@@ -219,16 +221,16 @@ function analyzeArgumentCommand(executable, args) {
219
221
  }
220
222
  return { command, destructive: isForbiddenCommand(command), opaque: false, wrapped: false };
221
223
  }
222
- function classifyPath(workspace, input) {
223
- const candidate = resolve(workspace, input);
224
- const lexicalInside = isWithin(workspace, candidate);
224
+ function classifyPath(lexicalWorkspace, physicalWorkspace, input) {
225
+ const candidate = resolve(lexicalWorkspace, input);
226
+ const lexicalInside = isWithin(lexicalWorkspace, candidate);
225
227
  const traversal = input.split(/[\\/]+/).includes("..");
226
- const startsInWorkspace = pathStartsWith(workspace, input);
228
+ const startsInWorkspace = pathStartsWith(lexicalWorkspace, input);
227
229
  if ((!isAbsolute(input) || startsInWorkspace) && traversal && !lexicalInside) {
228
230
  return { inside: false, escape: true, reason: "Path traversal escapes the workspace" };
229
231
  }
230
232
  const physical = resolvePhysical(candidate);
231
- const physicalInside = isWithin(workspace, physical);
233
+ const physicalInside = isWithin(physicalWorkspace, physical);
232
234
  if (lexicalInside && !physicalInside) return { inside: false, escape: true, reason: "Symlink escapes the workspace" };
233
235
  return { inside: physicalInside, escape: false };
234
236
  }
@@ -318,7 +320,7 @@ function isRoutineCommand(command) {
318
320
  if (command.executable === "go") return first === "test";
319
321
  return ["pytest", "vitest", "jest", "eslint", "tsc"].includes(command.executable);
320
322
  }
321
- function assessRoutineArguments(command, cwd, workspace) {
323
+ function assessRoutineArguments(command, cwd, lexicalWorkspace, physicalWorkspace) {
322
324
  const args = routineArguments(command);
323
325
  if (args === void 0) return "ask";
324
326
  const pathOptions = /* @__PURE__ */ new Set([
@@ -343,7 +345,7 @@ function assessRoutineArguments(command, cwd, workspace) {
343
345
  const option2 = equals[1]?.toLowerCase() ?? "";
344
346
  const value = equals[2] ?? "";
345
347
  if (!pathOptions.has(option2)) return "ask";
346
- const decision = assessArgumentPath(value, cwd, workspace);
348
+ const decision = assessArgumentPath(value, cwd, lexicalWorkspace, physicalWorkspace);
347
349
  if (decision !== "allow") return decision;
348
350
  continue;
349
351
  }
@@ -351,14 +353,14 @@ function assessRoutineArguments(command, cwd, workspace) {
351
353
  if (option !== void 0 && pathOptions.has(option)) {
352
354
  const value = args[index + 1];
353
355
  if (value === void 0 || value.startsWith("-")) return "ask";
354
- const decision = assessArgumentPath(value, cwd, workspace);
356
+ const decision = assessArgumentPath(value, cwd, lexicalWorkspace, physicalWorkspace);
355
357
  if (decision !== "allow") return decision;
356
358
  index += 1;
357
359
  continue;
358
360
  }
359
361
  if (arg.startsWith("-")) continue;
360
362
  if (looksLikePath(arg)) {
361
- const decision = assessArgumentPath(arg, cwd, workspace);
363
+ const decision = assessArgumentPath(arg, cwd, lexicalWorkspace, physicalWorkspace);
362
364
  if (decision !== "allow") return decision;
363
365
  continue;
364
366
  }
@@ -376,10 +378,10 @@ function routineArguments(command) {
376
378
  if (["pytest", "vitest", "jest", "eslint", "tsc"].includes(command.executable)) return command.args;
377
379
  return void 0;
378
380
  }
379
- function assessArgumentPath(value, cwd, workspace) {
381
+ function assessArgumentPath(value, cwd, lexicalWorkspace, physicalWorkspace) {
380
382
  if (value.length === 0) return "ask";
381
383
  const resolved = resolve(cwd, value);
382
- const classification = classifyPath(workspace, resolved);
384
+ const classification = classifyPath(lexicalWorkspace, physicalWorkspace, resolved);
383
385
  return classification.escape || !classification.inside ? "deny" : "allow";
384
386
  }
385
387
  function looksLikePath(value) {
@@ -2326,6 +2328,7 @@ var MVP_COMMANDS = [
2326
2328
  "memory",
2327
2329
  "remember",
2328
2330
  "forget",
2331
+ "forget-cold",
2329
2332
  "checkpoint",
2330
2333
  "tree",
2331
2334
  "rewind",
@@ -2356,6 +2359,7 @@ var COMMAND_DESCRIPTIONS = {
2356
2359
  memory: "Show long-term project memory",
2357
2360
  remember: "Add a long-term memory",
2358
2361
  forget: "Remove matching long-term memories",
2362
+ "forget-cold": "Remove all cold long-term memories and their files",
2359
2363
  checkpoint: "Create a workspace and context checkpoint",
2360
2364
  tree: "Show the session history tree",
2361
2365
  rewind: "Restore a prior session node",
@@ -2502,7 +2506,7 @@ var HELP = [
2502
2506
  "/permissions <default|acceptEdits|plan|bypassPermissions|auto|bubble>",
2503
2507
  "/login authenticate via OAuth PKCE",
2504
2508
  "/init /config /skills /plugins /hooks /tasks",
2505
- "/memory /remember [type] <text> /forget <text-or-id> /finish",
2509
+ "/memory /remember [type] <text> /forget <text-or-id> /forget-cold /finish",
2506
2510
  "/checkpoint [label] /tree /rewind <node> /unrevert /fork <node>",
2507
2511
  "/compact /clear /help /exit",
2508
2512
  "/loop <goal> run a verified autonomous loop",
@@ -2698,6 +2702,8 @@ var FlavorSession = class {
2698
2702
  this.#notice(await this.#services.remember(command.type, command.text));
2699
2703
  } else if (command.name === "forget") {
2700
2704
  this.#notice(await this.#services.forget(command.query));
2705
+ } else if (command.name === "forget-cold") {
2706
+ this.#notice(await this.#services.forgetCold());
2701
2707
  } else if (command.name === "finish") {
2702
2708
  this.#notice(await this.#services.finishTask());
2703
2709
  } else if (command.name === "checkpoint") {
@@ -5042,7 +5048,8 @@ var ToolRuntime = class {
5042
5048
  } catch {
5043
5049
  }
5044
5050
  }
5045
- if (pre.decision === "ask" || permission.decision === "ask") {
5051
+ const sessionAllowed = this.#alwaysAllowed.has(getToolCategory(tool.name));
5052
+ if (!sessionAllowed && (pre.decision === "ask" || permission.decision === "ask")) {
5046
5053
  const reason = [pre.reason, permission.reason].filter((value) => value !== void 0).join("\n") || "Approval required";
5047
5054
  const requestDecision = await this.#hooks.emit({
5048
5055
  version: 1,
@@ -5067,8 +5074,7 @@ var ToolRuntime = class {
5067
5074
  throw signal.reason;
5068
5075
  } else {
5069
5076
  const category = getToolCategory(tool.name);
5070
- if (this.#alwaysAllowed.has(category)) {
5071
- } else if (this.#approve === void 0) {
5077
+ if (this.#approve === void 0) {
5072
5078
  return this.#fail(tool.name, input, context.agent, "permission_denied", reason);
5073
5079
  } else {
5074
5080
  const decision = await this.#approve({ ...request, reason }, signal);
@@ -9220,9 +9226,11 @@ var SkillRegistry = class {
9220
9226
  for (const configuredRoot of roots) {
9221
9227
  const root = resolve13(configuredRoot);
9222
9228
  let entries;
9229
+ let physicalRoot = root;
9223
9230
  try {
9224
9231
  const rootInfo = await lstat6(root);
9225
9232
  if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) throw new Error("Skill root must be a real directory");
9233
+ physicalRoot = await realpath4(root);
9226
9234
  entries = await readdir5(root, { withFileTypes: true });
9227
9235
  } catch (error) {
9228
9236
  if (isMissing2(error)) continue;
@@ -9235,12 +9243,12 @@ var SkillRegistry = class {
9235
9243
  try {
9236
9244
  if (entry2.isSymbolicLink()) throw new Error("Symlinked skill directories are not allowed");
9237
9245
  if (!entry2.isDirectory()) continue;
9238
- const physicalRoot = await realpath4(skillRoot);
9239
- if (!isWithin3(root, physicalRoot)) throw new Error("Skill directory escapes its registry root");
9246
+ const physicalSkillRoot = await realpath4(skillRoot);
9247
+ if (!isWithin3(physicalRoot, physicalSkillRoot)) throw new Error("Skill directory escapes its registry root");
9240
9248
  const skillFile = resolve13(skillRoot, "SKILL.md");
9241
9249
  const parsed = await readFrontmatterFile(
9242
9250
  skillFile,
9243
- physicalRoot,
9251
+ physicalSkillRoot,
9244
9252
  this.#options.maxMetadataBytes,
9245
9253
  this.#options.openFile
9246
9254
  );
@@ -9251,7 +9259,7 @@ var SkillRegistry = class {
9251
9259
  name: parsed.name,
9252
9260
  description: parsed.description,
9253
9261
  source,
9254
- root: physicalRoot,
9262
+ root: physicalSkillRoot,
9255
9263
  disableModelInvocation: parsed.disableModelInvocation
9256
9264
  }),
9257
9265
  skillFile
@@ -10747,6 +10755,7 @@ function createEditTool(workspace, options = {}) {
10747
10755
  paths: (input) => [guard.lexical(input.path)],
10748
10756
  execute: async (input, signal) => {
10749
10757
  abortIfNeeded(signal);
10758
+ const displayPath = guard.lexical(input.path);
10750
10759
  const path = await guard.existing(input.path);
10751
10760
  const contents = await readText(path);
10752
10761
  const hasCRLF = contents.includes("\r\n");
@@ -10762,11 +10771,11 @@ function createEditTool(workspace, options = {}) {
10762
10771
  }
10763
10772
  const updatedLF = contentsLF.slice(0, first) + newTextLF + contentsLF.slice(first + oldTextLF.length);
10764
10773
  const updated = hasCRLF ? updatedLF.replace(/\n/g, "\r\n") : updatedLF;
10765
- await options.beforeCommit?.({ path, before: contents, after: updated, kind: "update" }, signal);
10774
+ await options.beforeCommit?.({ path: displayPath, before: contents, after: updated, kind: "update" }, signal);
10766
10775
  await atomicWrite(path, updated, signal);
10767
10776
  return withToolPresentation(
10768
- { path, replacements: 1 },
10769
- buildFileChangePresentation(path, contents, updated, "update")
10777
+ { path: displayPath, replacements: 1 },
10778
+ buildFileChangePresentation(displayPath, contents, updated, "update")
10770
10779
  );
10771
10780
  }
10772
10781
  };
@@ -12830,7 +12839,7 @@ ${total}`;
12830
12839
 
12831
12840
  // src/memory/store.ts
12832
12841
  import { createHash as createHash7 } from "crypto";
12833
- import { readFile as readFile14 } from "fs/promises";
12842
+ import { readFile as readFile14, rm as rm7 } from "fs/promises";
12834
12843
  import { resolve as resolve19, join as join15, relative as relative16, sep as sep15 } from "path";
12835
12844
 
12836
12845
  // src/memory/similarity.ts
@@ -13057,6 +13066,50 @@ var MemoryStore = class {
13057
13066
  }) }));
13058
13067
  return removed;
13059
13068
  }
13069
+ /**
13070
+ * Removes every cold memory entry and its backing content. A task file is
13071
+ * deleted only when all of its entries were cold; otherwise only the cold
13072
+ * entries are dropped from the shared file.
13073
+ */
13074
+ async forgetCold(now = /* @__PURE__ */ new Date()) {
13075
+ let removedReferences = [];
13076
+ await this.#updateIndex((index) => {
13077
+ removedReferences = index.references.filter((reference) => classifyMemoryHeat(reference, now) === "cold");
13078
+ const removedIds = new Set(removedReferences.map((reference) => reference.id));
13079
+ return { ...index, references: index.references.filter((reference) => !removedIds.has(reference.id)) };
13080
+ });
13081
+ if (removedReferences.length === 0) return { removed: 0, filesRemoved: 0 };
13082
+ const byPath = /* @__PURE__ */ new Map();
13083
+ for (const reference of removedReferences) {
13084
+ const group = byPath.get(reference.contentPath) ?? [];
13085
+ group.push(reference);
13086
+ byPath.set(reference.contentPath, group);
13087
+ }
13088
+ let filesRemoved = 0;
13089
+ for (const [contentPath, references] of byPath) {
13090
+ const removedIds = new Set(references.map((reference) => reference.id));
13091
+ const path = this.#resolveContentPath(contentPath);
13092
+ const recovered = await readRecoverableFile(path, decodeTask);
13093
+ if (recovered === void 0) continue;
13094
+ const remaining = recovered.value.items.filter((item) => !removedIds.has(item.id));
13095
+ if (remaining.length === 0) {
13096
+ await rm7(path, { force: true });
13097
+ await rm7(`${path}.bak`, { force: true });
13098
+ filesRemoved += 1;
13099
+ } else {
13100
+ await updateProtectedFile({
13101
+ path,
13102
+ decode: decodeTask,
13103
+ encode: encodeTask,
13104
+ update: (current) => {
13105
+ const document = current ?? recovered.value;
13106
+ return { ...document, items: document.items.filter((item) => !removedIds.has(item.id)) };
13107
+ }
13108
+ });
13109
+ }
13110
+ }
13111
+ return { removed: removedReferences.length, filesRemoved };
13112
+ }
13060
13113
  async recall(query, options) {
13061
13114
  assertTaskId(options.taskId);
13062
13115
  const now = options.now ?? /* @__PURE__ */ new Date();
@@ -14957,6 +15010,12 @@ ${renderMemoryDocument(entries)}`;
14957
15010
  if (removed > 0) await refreshMemoryState();
14958
15011
  return removed === 0 ? "No matching memory found." : `Forgot ${removed} memory ${removed === 1 ? "entry" : "entries"}.`;
14959
15012
  },
15013
+ forgetCold: async () => {
15014
+ if (memoryStore === void 0) return "Long-term memory is disabled.";
15015
+ const { removed, filesRemoved } = await memoryStore.forgetCold();
15016
+ if (removed > 0) await refreshMemoryState();
15017
+ return removed === 0 ? "No cold long-term memories to remove." : `Forgot ${removed} cold memory ${removed === 1 ? "entry" : "entries"} and removed ${filesRemoved} task ${filesRemoved === 1 ? "file" : "files"}.`;
15018
+ },
14960
15019
  finishTask: () => finalizeMemoryTask(true),
14961
15020
  pluginCommands: () => [...pluginCommands.keys()].sort(),
14962
15021
  runPluginCommand: async (name, args, signal) => {
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module"; const require = createRequire(impor
3
3
  import {
4
4
  FlavorRpcServer,
5
5
  TraceRecorder
6
- } from "./chunk-L5A2NDMT.js";
6
+ } from "./chunk-CXSXBPD6.js";
7
7
  import {
8
8
  staticTaskLines
9
9
  } from "./chunk-KKIBYDJI.js";
@@ -20,7 +20,7 @@ import {
20
20
  message,
21
21
  packageVersion,
22
22
  redactErrorText
23
- } from "./chunk-MRISY6KK.js";
23
+ } from "./chunk-RYDCJHXW.js";
24
24
  import {
25
25
  McpServerConfigSchema,
26
26
  McpServerNameSchema,
@@ -771,7 +771,7 @@ function createProgram() {
771
771
  const [{ render, AlternateScreen }, { createElement }, { App }] = await Promise.all([
772
772
  import("./claude-ink-WPWPEL5A.js"),
773
773
  import("react"),
774
- import("./app-N3655H2E.js")
774
+ import("./app-WRTEXGA6.js")
775
775
  ]);
776
776
  const instance = await render(createElement(
777
777
  AlternateScreen,