jinzd-ai-cli 0.4.216 → 0.4.217

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +3 -2
  2. package/dist/{batch-C6HCXZIG.js → batch-YCOVKTXD.js} +2 -2
  3. package/dist/{chat-index-R2E27VXN.js → chat-index-PS274XM7.js} +1 -1
  4. package/dist/{chunk-6HLKCSB3.js → chunk-524WZOKS.js} +1 -1
  5. package/dist/{chunk-GY3C3C2Y.js → chunk-KOPUCJXM.js} +3 -3
  6. package/dist/{chunk-TCOC4AUI.js → chunk-MGBMNCHG.js} +279 -54
  7. package/dist/{chunk-PNTLA3MN.js → chunk-OUAZOE5U.js} +2 -2
  8. package/dist/{chunk-OLN7VUZA.js → chunk-QAYOI57M.js} +3 -1
  9. package/dist/{chunk-77HDCGTN.js → chunk-SNJAOXFT.js} +1 -1
  10. package/dist/{chunk-OQGVGPEK.js → chunk-VHY6NVMQ.js} +2 -0
  11. package/dist/{chunk-VW7Y27WW.js → chunk-VTH7BLXK.js} +1 -1
  12. package/dist/{chunk-WAI3WPV2.js → chunk-WKOQ5CYC.js} +1 -1
  13. package/dist/{chunk-TJ6GGN4B.js → chunk-WZ3VKLF3.js} +1 -1
  14. package/dist/{chunk-7YWS3JLU.js → chunk-XJGEQIYS.js} +3 -1
  15. package/dist/{ci-6ZTFO3LX.js → ci-52RZIYWB.js} +4 -4
  16. package/dist/{ci-format-CLZ6QJRL.js → ci-format-73UXKE65.js} +2 -2
  17. package/dist/{constants-APSORFOH.js → constants-DIXAD35W.js} +3 -1
  18. package/dist/{doctor-cli-GUIX4X5F.js → doctor-cli-7GOQPULZ.js} +4 -4
  19. package/dist/electron-server.js +562 -318
  20. package/dist/{hub-ZGHQWNWE.js → hub-GIGBITZN.js} +1 -1
  21. package/dist/index.js +129 -69
  22. package/dist/{pr-JOL3IAGV.js → pr-KPQ5RPKB.js} +4 -4
  23. package/dist/{run-tests-4PKSIVK5.js → run-tests-K7QR5QN4.js} +1 -1
  24. package/dist/{run-tests-ZRK4TQUN.js → run-tests-ZDSA3QES.js} +2 -2
  25. package/dist/{server-O3XHT56X.js → server-22YF3U34.js} +5 -4
  26. package/dist/{server-FXUF5P64.js → server-QT3SC2KI.js} +129 -86
  27. package/dist/{task-orchestrator-TUMDTOAX.js → task-orchestrator-BHQQCVTY.js} +5 -4
  28. package/dist/{usage-X7MJX4YD.js → usage-WZZFSFLM.js} +2 -2
  29. package/dist/web/client/app.js +53 -2
  30. package/dist/web/client/index.html +11 -1
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -33,7 +33,8 @@
33
33
  - **Semantic Code Search** *(v0.4.77+)* — `search_code` tool finds code by meaning, not name. Local sentence embeddings (multilingual MiniLM, 117 MB one-time download) score symbols by cosine similarity against natural-language queries in English or Chinese ("where are users authenticated", "哪里做了速率限制"). No API key, runs on CPU. Manage with `/index semantic-rebuild|semantic-clear`
34
34
  - **MCP Server Mode** *(v0.4.84+)* — `aicli mcp-serve` reverses ai-cli into an MCP server (JSON-RPC 2.0 over stdio), exposing its 28 built-in tools (incl. `find_symbol` / `search_code` / `run_tests`) to Claude Desktop / Cursor / any MCP client. Opt-in destructive-tool allow, `--tools` whitelist, `--cwd` override
35
35
  - **Session Sensitive-Data Redaction** *(v0.4.88+)* — unified redactor scrubs `password=` / `api_key` / bearer tokens / OpenAI-style keys from every message **before it hits disk**. Query text is redacted too, so secrets never reach embeddings or logs. `/security status` + `/security scan` to audit
36
- - **Human-like Long-Term Memory** *(v0.4.89+, B4)* — semantic index over every past chat session + `recall_memory` AI tool + `/memory rebuild|refresh|status|recall` commands. AI is prompted to auto-recall when it sees "last time" / "之前" / ambiguous references. Reuses the same MiniLM embedder as semantic code search
36
+ - **Human-like Long-Term Memory** *(v0.4.89+, B4)* — semantic index over every past chat session + `recall_memory` AI tool + `/memory rebuild|refresh|status|recall` commands. AI is prompted to auto-recall when it sees "last time" / "之前" / ambiguous references. Reuses the same MiniLM embedder as semantic code search
37
+ - **Governed Persistent Memory** *(v0.4.217+)* — `save_memory` and `/memory add` write auditable `memory.jsonl` entries with id/scope/source/sensitivity/approval/expiry; low-risk entries auto-approve, sensitive entries stay pending until `/memory approve <id>`, and project-scoped memories only inject inside the same project
37
38
  - **Web UI Memory Panel** *(v0.4.90+, B4)* — new 🧠 Memory sidebar tab with semantic search across past chats; each hit has **➕ Inject** (quotes the snippet into the chat input as a markdown blockquote so you can review/edit before sending — no silent context injection) and **↗ Load** (jumps to source session). Bulk "Inject top 3" for recall bundles
38
39
  - **Streaming Tool Use** — Real-time streaming of AI reasoning and tool calls as they happen
39
40
  - **Sub-Agents** — Delegate complex subtasks to isolated child agents with independent tool loops
@@ -170,7 +171,7 @@ AI autonomously invokes these 27 tools during conversations:
170
171
  | `run_tests` | safe | Auto-detect and run project tests (JUnit XML parsing) |
171
172
  | `spawn_agent` | safe | Delegate subtasks to named isolated agents (`agent`: explorer/worker/reviewer/security/tester) |
172
173
  | `ask_user` | safe | Pause and ask the user a question |
173
- | `save_memory` | safe | Persist important info across sessions |
174
+ | `save_memory` | safe | Persist governed memory across sessions; sensitive entries require `/memory approve <id>` |
174
175
  | `write_todos` | safe | Task breakdown with live progress rendering |
175
176
  | `save_last_response` | write | Save AI response to file |
176
177
  | `task_create` | write | Start a command running in the background |
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigManager
4
- } from "./chunk-WAI3WPV2.js";
4
+ } from "./chunk-WKOQ5CYC.js";
5
5
  import "./chunk-TZQHYZKT.js";
6
- import "./chunk-7YWS3JLU.js";
6
+ import "./chunk-XJGEQIYS.js";
7
7
  import {
8
8
  atomicWriteFileSync
9
9
  } from "./chunk-IW3Q7AE5.js";
@@ -5,7 +5,7 @@ import {
5
5
  getChatIndexStatus,
6
6
  loadChatIndex,
7
7
  searchChatMemory
8
- } from "./chunk-OQGVGPEK.js";
8
+ } from "./chunk-VHY6NVMQ.js";
9
9
  import "./chunk-JV5N65KN.js";
10
10
  import "./chunk-6BUTA5VW.js";
11
11
  export {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  CONFIG_DIR_NAME,
4
4
  VERSION
5
- } from "./chunk-7YWS3JLU.js";
5
+ } from "./chunk-XJGEQIYS.js";
6
6
 
7
7
  // src/diagnostics/crash-log.ts
8
8
  import {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  evaluateCiGate,
4
4
  normalizeCiThresholds
5
- } from "./chunk-TJ6GGN4B.js";
5
+ } from "./chunk-WZ3VKLF3.js";
6
6
  import {
7
7
  buildReviewPrompt,
8
8
  buildSecurityReviewPrompt,
@@ -13,10 +13,10 @@ import {
13
13
  } from "./chunk-QMXC327F.js";
14
14
  import {
15
15
  ConfigManager
16
- } from "./chunk-WAI3WPV2.js";
16
+ } from "./chunk-WKOQ5CYC.js";
17
17
  import {
18
18
  VERSION
19
- } from "./chunk-7YWS3JLU.js";
19
+ } from "./chunk-XJGEQIYS.js";
20
20
 
21
21
  // src/cli/ci.ts
22
22
  import { execFileSync } from "child_process";
@@ -5,10 +5,10 @@ import {
5
5
  } from "./chunk-T2NL5ZIA.js";
6
6
  import {
7
7
  runTestsTool
8
- } from "./chunk-VW7Y27WW.js";
8
+ } from "./chunk-VTH7BLXK.js";
9
9
  import {
10
10
  runTool
11
- } from "./chunk-77HDCGTN.js";
11
+ } from "./chunk-SNJAOXFT.js";
12
12
  import {
13
13
  getDangerLevel,
14
14
  isFileWriteTool,
@@ -23,10 +23,15 @@ import {
23
23
  CONFIG_DIR_NAME,
24
24
  DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
25
25
  MEMORY_FILE_NAME,
26
+ MEMORY_MAX_CHARS,
27
+ MEMORY_STORE_FILE_NAME,
26
28
  SUBAGENT_ALLOWED_TOOLS,
27
29
  SUBAGENT_DEFAULT_MAX_ROUNDS,
28
30
  SUBAGENT_MAX_ROUNDS_LIMIT
29
- } from "./chunk-7YWS3JLU.js";
31
+ } from "./chunk-XJGEQIYS.js";
32
+ import {
33
+ getGitRoot
34
+ } from "./chunk-HOSJZMQS.js";
30
35
  import {
31
36
  fileCheckpoints
32
37
  } from "./chunk-4BKXL7SM.js";
@@ -34,6 +39,10 @@ import {
34
39
  loadChatIndex,
35
40
  searchChatMemory
36
41
  } from "./chunk-TB4W4Y4T.js";
42
+ import {
43
+ DEFAULT_PATTERNS,
44
+ redactString
45
+ } from "./chunk-SLSWPBK3.js";
37
46
  import {
38
47
  indexProject
39
48
  } from "./chunk-K3CF65QH.js";
@@ -2412,7 +2421,7 @@ var ToolExecutor = class {
2412
2421
  rl.resume();
2413
2422
  process.stdout.write(prompt);
2414
2423
  this.confirming = true;
2415
- return new Promise((resolve7) => {
2424
+ return new Promise((resolve8) => {
2416
2425
  let completed = false;
2417
2426
  const cleanup = (result) => {
2418
2427
  if (completed) return;
@@ -2422,7 +2431,7 @@ var ToolExecutor = class {
2422
2431
  rl.pause();
2423
2432
  rlAny.output = savedOutput;
2424
2433
  this.confirming = false;
2425
- resolve7(result);
2434
+ resolve8(result);
2426
2435
  };
2427
2436
  const onLine = (line) => {
2428
2437
  const trimmed = line.trim();
@@ -2596,7 +2605,7 @@ var ToolExecutor = class {
2596
2605
  rl.resume();
2597
2606
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
2598
2607
  this.confirming = true;
2599
- return new Promise((resolve7) => {
2608
+ return new Promise((resolve8) => {
2600
2609
  let completed = false;
2601
2610
  const cleanup = (answer) => {
2602
2611
  if (completed) return;
@@ -2606,7 +2615,7 @@ var ToolExecutor = class {
2606
2615
  rl.pause();
2607
2616
  rlAny.output = savedOutput;
2608
2617
  this.confirming = false;
2609
- resolve7(answer === "y");
2618
+ resolve8(answer === "y");
2610
2619
  };
2611
2620
  const onLine = (line) => {
2612
2621
  const trimmed = line.trim();
@@ -3855,7 +3864,7 @@ var runInteractiveTool = {
3855
3864
  PYTHONDONTWRITEBYTECODE: "1"
3856
3865
  };
3857
3866
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
3858
- return new Promise((resolve7) => {
3867
+ return new Promise((resolve8) => {
3859
3868
  const child = spawn2(executable, cmdArgs.map(String), {
3860
3869
  cwd: process.cwd(),
3861
3870
  env,
@@ -3888,22 +3897,22 @@ var runInteractiveTool = {
3888
3897
  setTimeout(writeNextLine, 400);
3889
3898
  const timer = setTimeout(() => {
3890
3899
  child.kill();
3891
- resolve7(`${prefixWarnings}[Timeout after ${timeout}ms]
3900
+ resolve8(`${prefixWarnings}[Timeout after ${timeout}ms]
3892
3901
  ${buildOutput(stdout, stderr)}`);
3893
3902
  }, timeout);
3894
3903
  child.on("close", (code) => {
3895
3904
  clearTimeout(timer);
3896
3905
  const output = buildOutput(stdout, stderr);
3897
3906
  if (code !== 0 && code !== null) {
3898
- resolve7(`${prefixWarnings}Exit code ${code}:
3907
+ resolve8(`${prefixWarnings}Exit code ${code}:
3899
3908
  ${output}`);
3900
3909
  } else {
3901
- resolve7(`${prefixWarnings}${output || "(no output)"}`);
3910
+ resolve8(`${prefixWarnings}${output || "(no output)"}`);
3902
3911
  }
3903
3912
  });
3904
3913
  child.on("error", (err) => {
3905
3914
  clearTimeout(timer);
3906
- resolve7(
3915
+ resolve8(
3907
3916
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
3908
3917
  Hint: On Windows, use the full path to the executable, e.g.:
3909
3918
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -4571,26 +4580,236 @@ Any of these triggers means use save_last_response, NOT write_file:
4571
4580
  };
4572
4581
 
4573
4582
  // src/tools/builtin/save-memory.ts
4574
- import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync6, mkdirSync as mkdirSync4 } from "fs";
4575
- import { join as join6 } from "path";
4583
+ import { join as join7 } from "path";
4576
4584
  import { homedir as homedir3 } from "os";
4577
- function getMemoryFilePath() {
4578
- return join6(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4585
+
4586
+ // src/memory/persistent-memory.ts
4587
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8 } from "fs";
4588
+ import { randomUUID, createHash as createHash2 } from "crypto";
4589
+ import { dirname as dirname4, join as join6, resolve as resolve6 } from "path";
4590
+ function memoryStorePath(configDir) {
4591
+ return join6(configDir, MEMORY_STORE_FILE_NAME);
4592
+ }
4593
+ function memoryMarkdownPath(configDir) {
4594
+ return join6(configDir, MEMORY_FILE_NAME);
4595
+ }
4596
+ function getProjectKey(cwd = process.cwd()) {
4597
+ return resolve6(getGitRoot(cwd) ?? cwd);
4598
+ }
4599
+ function nowIso() {
4600
+ return (/* @__PURE__ */ new Date()).toISOString();
4601
+ }
4602
+ function isValidScope(value) {
4603
+ return value === "personal" || value === "project" || value === "session" || value === "team";
4604
+ }
4605
+ function isValidSensitivity(value) {
4606
+ return value === "low" || value === "medium" || value === "high";
4579
4607
  }
4580
- function formatTimestamp() {
4581
- const now = /* @__PURE__ */ new Date();
4582
- const pad = (n) => String(n).padStart(2, "0");
4583
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
4608
+ function normalizeEntry(value) {
4609
+ if (!value || typeof value !== "object") return null;
4610
+ const raw = value;
4611
+ const content = typeof raw.content === "string" ? raw.content.trim() : "";
4612
+ if (!content) return null;
4613
+ const createdAt = typeof raw.createdAt === "string" ? raw.createdAt : nowIso();
4614
+ return {
4615
+ id: typeof raw.id === "string" && raw.id ? raw.id : randomUUID(),
4616
+ content,
4617
+ scope: isValidScope(raw.scope) ? raw.scope : "personal",
4618
+ sourceSession: typeof raw.sourceSession === "string" && raw.sourceSession ? raw.sourceSession : void 0,
4619
+ sourceProject: typeof raw.sourceProject === "string" && raw.sourceProject ? raw.sourceProject : void 0,
4620
+ createdAt,
4621
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : createdAt,
4622
+ expiresAt: typeof raw.expiresAt === "string" && raw.expiresAt ? raw.expiresAt : void 0,
4623
+ sensitivity: isValidSensitivity(raw.sensitivity) ? raw.sensitivity : "low",
4624
+ approved: raw.approved === true,
4625
+ redactedKinds: Array.isArray(raw.redactedKinds) ? raw.redactedKinds.filter((x) => typeof x === "string") : void 0
4626
+ };
4627
+ }
4628
+ function legacyEntriesFromMarkdown(configDir) {
4629
+ const file = memoryMarkdownPath(configDir);
4630
+ if (!existsSync11(file)) return [];
4631
+ const content = readFileSync8(file, "utf-8").trim();
4632
+ if (!content) return [];
4633
+ const parts = content.split(/\n(?=##\s+\d{4}-\d{2}-\d{2})/g);
4634
+ const chunks = parts.length > 1 ? parts : [content];
4635
+ return chunks.map((chunk, index) => {
4636
+ const match = chunk.match(/^##\s+([^\n]+)\n([\s\S]*)$/);
4637
+ const body = (match ? match[2] : chunk).trim();
4638
+ const parsed = match ? Date.parse(match[1].replace(" ", "T")) : NaN;
4639
+ const createdAt = Number.isFinite(parsed) ? new Date(parsed).toISOString() : nowIso();
4640
+ const id = "legacy-" + createHash2("sha1").update(`${index}
4641
+ ${chunk}`).digest("hex").slice(0, 12);
4642
+ return normalizeEntry({ id, content: body, scope: "personal", createdAt, updatedAt: createdAt, sensitivity: "low", approved: true });
4643
+ }).filter((entry) => Boolean(entry));
4644
+ }
4645
+ function loadMemoryEntries(configDir) {
4646
+ const file = memoryStorePath(configDir);
4647
+ if (!existsSync11(file)) return legacyEntriesFromMarkdown(configDir);
4648
+ const text = readFileSync8(file, "utf-8").trim();
4649
+ if (!text) return [];
4650
+ const entries = [];
4651
+ for (const line of text.split("\n")) {
4652
+ const trimmed = line.trim();
4653
+ if (!trimmed) continue;
4654
+ try {
4655
+ const entry = normalizeEntry(JSON.parse(trimmed));
4656
+ if (entry) entries.push(entry);
4657
+ } catch {
4658
+ }
4659
+ }
4660
+ return entries;
4661
+ }
4662
+ function saveMemoryEntries(configDir, entries) {
4663
+ mkdirSync4(configDir, { recursive: true });
4664
+ const jsonl = entries.map((entry) => JSON.stringify(entry)).join("\n");
4665
+ atomicWriteFileSync(memoryStorePath(configDir), jsonl ? jsonl + "\n" : "");
4666
+ syncLegacyMarkdown(configDir, entries);
4667
+ }
4668
+ function isMemoryExpired(entry, at = /* @__PURE__ */ new Date()) {
4669
+ return Boolean(entry.expiresAt && Date.parse(entry.expiresAt) <= at.getTime());
4670
+ }
4671
+ function isMemoryVisibleInProject(entry, cwd = process.cwd()) {
4672
+ if (entry.scope !== "project") return true;
4673
+ return entry.sourceProject === getProjectKey(cwd);
4674
+ }
4675
+ function listMemoryEntries(configDir, options = {}) {
4676
+ const cwd = options.cwd ?? process.cwd();
4677
+ return loadMemoryEntries(configDir).filter((entry) => options.includeRejected || entry.approved).filter((entry) => options.includeExpired || !isMemoryExpired(entry)).filter((entry) => isMemoryVisibleInProject(entry, cwd));
4678
+ }
4679
+ function addMemoryEntry(configDir, content, options = {}) {
4680
+ const original = content.trim();
4681
+ if (!original) throw new Error("memory content is required");
4682
+ const redact = options.redact ?? true;
4683
+ const redacted = redact ? redactString(original, { enabled: true, patterns: options.patterns ?? DEFAULT_PATTERNS, customRegexes: options.customPatterns }) : { redacted: original, hits: [] };
4684
+ const sensitivity = options.sensitivity ?? (redacted.hits.length > 0 ? "high" : "low");
4685
+ const approved = options.approved ?? (sensitivity === "low" && redacted.hits.length === 0);
4686
+ const scope = options.scope ?? "personal";
4687
+ const timestamp = nowIso();
4688
+ const entry = {
4689
+ id: randomUUID(),
4690
+ content: redacted.redacted.trim(),
4691
+ scope,
4692
+ sourceSession: options.sourceSession,
4693
+ sourceProject: scope === "project" ? getProjectKey(options.cwd) : void 0,
4694
+ createdAt: timestamp,
4695
+ updatedAt: timestamp,
4696
+ expiresAt: options.expiresAt,
4697
+ sensitivity,
4698
+ approved,
4699
+ redactedKinds: [...new Set(redacted.hits.map((hit) => hit.kind))]
4700
+ };
4701
+ const entries = loadMemoryEntries(configDir);
4702
+ entries.push(entry);
4703
+ saveMemoryEntries(configDir, entries);
4704
+ return entry;
4705
+ }
4706
+ function updateMemoryApproval(configDir, idPrefix, approved) {
4707
+ const entries = loadMemoryEntries(configDir);
4708
+ const entry = findUnique(entries, idPrefix);
4709
+ entry.approved = approved;
4710
+ entry.updatedAt = nowIso();
4711
+ saveMemoryEntries(configDir, entries);
4712
+ return entry;
4713
+ }
4714
+ function deleteMemoryEntry(configDir, idPrefix) {
4715
+ const entries = loadMemoryEntries(configDir);
4716
+ const entry = findUnique(entries, idPrefix);
4717
+ saveMemoryEntries(configDir, entries.filter((candidate) => candidate.id !== entry.id));
4718
+ return entry;
4719
+ }
4720
+ function expireMemoryEntry(configDir, idPrefix, expiresAt = nowIso()) {
4721
+ const entries = loadMemoryEntries(configDir);
4722
+ const entry = findUnique(entries, idPrefix);
4723
+ entry.expiresAt = expiresAt;
4724
+ entry.updatedAt = nowIso();
4725
+ saveMemoryEntries(configDir, entries);
4726
+ return entry;
4727
+ }
4728
+ function exportMemoryEntries(configDir, cwd = process.cwd()) {
4729
+ return JSON.stringify(listMemoryEntries(configDir, { cwd, includeExpired: true, includeRejected: true }), null, 2);
4730
+ }
4731
+ function findUnique(entries, idPrefix) {
4732
+ const matches = entries.filter((entry) => entry.id.startsWith(idPrefix));
4733
+ if (matches.length === 0) throw new Error(`memory id not found: ${idPrefix}`);
4734
+ if (matches.length > 1) throw new Error(`memory id is ambiguous: ${idPrefix}`);
4735
+ return matches[0];
4736
+ }
4737
+ function syncLegacyMarkdown(configDir, entries = loadMemoryEntries(configDir)) {
4738
+ mkdirSync4(dirname4(memoryMarkdownPath(configDir)), { recursive: true });
4739
+ const active = entries.filter((entry) => entry.approved && !isMemoryExpired(entry));
4740
+ const markdown = active.map((entry) => {
4741
+ const date = entry.createdAt.replace("T", " ").slice(0, 19);
4742
+ const source = [
4743
+ `id:${entry.id.slice(0, 8)}`,
4744
+ `scope:${entry.scope}`,
4745
+ entry.sourceSession ? `session:${entry.sourceSession.slice(0, 8)}` : "",
4746
+ entry.sourceProject ? `project:${entry.sourceProject}` : "",
4747
+ `sensitivity:${entry.sensitivity}`
4748
+ ].filter(Boolean).join(" \xB7 ");
4749
+ return `## ${date}
4750
+ <!-- ${source} -->
4751
+ ${entry.content}
4752
+ `;
4753
+ }).join("\n");
4754
+ atomicWriteFileSync(memoryMarkdownPath(configDir), markdown);
4755
+ }
4756
+ function formatMemoryForPrompt(configDir, cwd = process.cwd()) {
4757
+ const entries = listMemoryEntries(configDir, { cwd }).filter((entry) => entry.approved && !isMemoryExpired(entry));
4758
+ if (entries.length === 0) return null;
4759
+ let content = entries.map((entry) => {
4760
+ const source = [
4761
+ `id ${entry.id.slice(0, 8)}`,
4762
+ `scope ${entry.scope}`,
4763
+ entry.sourceSession ? `source session ${entry.sourceSession.slice(0, 8)}` : "source manual/tool",
4764
+ entry.sourceProject ? `project ${entry.sourceProject}` : "",
4765
+ `sensitivity ${entry.sensitivity}`
4766
+ ].filter(Boolean).join(" \xB7 ");
4767
+ return `## ${entry.createdAt.replace("T", " ").slice(0, 19)}
4768
+ Source: ${source}
4769
+ ${entry.content}`;
4770
+ }).join("\n\n");
4771
+ if (content.length > MEMORY_MAX_CHARS) {
4772
+ content = content.slice(-MEMORY_MAX_CHARS);
4773
+ const firstEntry = content.indexOf("\n## ");
4774
+ if (firstEntry !== -1) content = content.slice(firstEntry + 1);
4775
+ }
4776
+ return { content, entryCount: entries.length };
4777
+ }
4778
+
4779
+ // src/tools/builtin/save-memory.ts
4780
+ function getConfigDir() {
4781
+ return join7(homedir3(), CONFIG_DIR_NAME);
4782
+ }
4783
+ function parseScope(value) {
4784
+ return value === "personal" || value === "project" || value === "session" || value === "team" ? value : void 0;
4785
+ }
4786
+ function parseSensitivity(value) {
4787
+ return value === "low" || value === "medium" || value === "high" ? value : void 0;
4584
4788
  }
4585
4789
  var saveMemoryTool = {
4586
4790
  definition: {
4587
4791
  name: "save_memory",
4588
- description: "Save important information to persistent memory that survives across sessions. Use this to remember: user preferences, coding style, project architecture decisions, recurring patterns, key findings, or any knowledge worth preserving. Keep each memory entry concise (1-3 sentences). The content will be automatically timestamped.",
4792
+ description: "Save important information to governed persistent memory. Low-risk memories are approved automatically; sensitive entries are stored as pending and require /memory approve <id>. Use concise entries for user preferences, project decisions, recurring patterns, and key findings.",
4589
4793
  parameters: {
4590
4794
  content: {
4591
4795
  type: "string",
4592
4796
  description: "The information to save to persistent memory. Keep it concise and actionable.",
4593
4797
  required: true
4798
+ },
4799
+ scope: {
4800
+ type: "string",
4801
+ description: "Memory scope: personal, project, session, or team. Defaults to personal. Project memories are only injected in the same project.",
4802
+ required: false
4803
+ },
4804
+ sensitivity: {
4805
+ type: "string",
4806
+ description: "Sensitivity: low, medium, or high. Low-risk entries may be auto-approved; medium/high remain pending unless explicitly approved.",
4807
+ required: false
4808
+ },
4809
+ expiresAt: {
4810
+ type: "string",
4811
+ description: "Optional ISO timestamp after which this memory should no longer be injected.",
4812
+ required: false
4594
4813
  }
4595
4814
  },
4596
4815
  dangerous: false
@@ -4598,20 +4817,16 @@ var saveMemoryTool = {
4598
4817
  async execute(args) {
4599
4818
  const content = String(args["content"] ?? "").trim();
4600
4819
  if (!content) throw new ToolError("save_memory", "content is required");
4601
- const memoryPath = getMemoryFilePath();
4602
- const configDir = join6(homedir3(), CONFIG_DIR_NAME);
4603
- if (!existsSync11(configDir)) {
4604
- mkdirSync4(configDir, { recursive: true });
4605
- }
4606
- const timestamp = formatTimestamp();
4607
- const entry = `
4608
- ## ${timestamp}
4609
- ${content}
4610
- `;
4611
- const previous = existsSync11(memoryPath) ? readFileSync8(memoryPath, "utf-8") : "";
4612
- atomicWriteFileSync(memoryPath, previous + entry);
4613
- const byteSize = statSync6(memoryPath).size;
4614
- return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
4820
+ const entry = addMemoryEntry(getConfigDir(), content, {
4821
+ scope: parseScope(args["scope"]) ?? "personal",
4822
+ sensitivity: parseSensitivity(args["sensitivity"]),
4823
+ expiresAt: typeof args["expiresAt"] === "string" ? args["expiresAt"] : void 0,
4824
+ cwd: process.cwd(),
4825
+ redact: true
4826
+ });
4827
+ const status = entry.approved ? "approved and active" : "pending approval";
4828
+ const redacted = entry.redactedKinds?.length ? ` Redacted: ${entry.redactedKinds.join(", ")}.` : "";
4829
+ return `Memory saved (${status}). id=${entry.id.slice(0, 8)} scope=${entry.scope} sensitivity=${entry.sensitivity}.${redacted}`;
4615
4830
  }
4616
4831
  };
4617
4832
 
@@ -4655,7 +4870,7 @@ function promptUser(rl, question) {
4655
4870
  console.log();
4656
4871
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
4657
4872
  process.stdout.write(chalk4.cyan("> "));
4658
- return new Promise((resolve7) => {
4873
+ return new Promise((resolve8) => {
4659
4874
  let completed = false;
4660
4875
  const cleanup = (answer) => {
4661
4876
  if (completed) return;
@@ -4665,7 +4880,7 @@ function promptUser(rl, question) {
4665
4880
  rl.pause();
4666
4881
  rlAny.output = savedOutput;
4667
4882
  askUserContext.prompting = false;
4668
- resolve7(answer);
4883
+ resolve8(answer);
4669
4884
  };
4670
4885
  const onLine = (line) => {
4671
4886
  cleanup(line);
@@ -4898,7 +5113,7 @@ function formatResults2(query, data, _requested) {
4898
5113
 
4899
5114
  // src/agents/agent-config.ts
4900
5115
  import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync9 } from "fs";
4901
- import { join as join7 } from "path";
5116
+ import { join as join8 } from "path";
4902
5117
  import { homedir as homedir4 } from "os";
4903
5118
  var READ_ONLY_TOOLS2 = /* @__PURE__ */ new Set([
4904
5119
  "read_file",
@@ -4997,7 +5212,7 @@ function loadAgentDir(dir, source) {
4997
5212
  const out = [];
4998
5213
  for (const file of readdirSync6(dir)) {
4999
5214
  if (!file.endsWith(".json")) continue;
5000
- const path3 = join7(dir, file);
5215
+ const path3 = join8(dir, file);
5001
5216
  try {
5002
5217
  const parsed = JSON.parse(readFileSync9(path3, "utf-8"));
5003
5218
  const cfg = parseAgentConfig(parsed, source, path3);
@@ -5009,8 +5224,8 @@ function loadAgentDir(dir, source) {
5009
5224
  }
5010
5225
  function getAgentDirs(configDir, cwd = process.cwd()) {
5011
5226
  return {
5012
- user: join7(configDir ?? join7(homedir4(), CONFIG_DIR_NAME), "agents"),
5013
- project: join7(cwd, CONFIG_DIR_NAME, "agents")
5227
+ user: join8(configDir ?? join8(homedir4(), CONFIG_DIR_NAME), "agents"),
5228
+ project: join8(cwd, CONFIG_DIR_NAME, "agents")
5014
5229
  };
5015
5230
  }
5016
5231
  function listAgentConfigs(configDir, cwd = process.cwd()) {
@@ -5052,7 +5267,7 @@ var nextId = 1;
5052
5267
  var preferredAgentName = "worker";
5053
5268
  var runs = [];
5054
5269
  var MAX_RUNS = 50;
5055
- function nowIso() {
5270
+ function nowIso2() {
5056
5271
  return (/* @__PURE__ */ new Date()).toISOString();
5057
5272
  }
5058
5273
  function startAgentRun(input) {
@@ -5061,7 +5276,7 @@ function startAgentRun(input) {
5061
5276
  agentName: input.agentName,
5062
5277
  task: input.task,
5063
5278
  status: "running",
5064
- startedAt: nowIso(),
5279
+ startedAt: nowIso2(),
5065
5280
  toolNames: [],
5066
5281
  agentIndex: input.agentIndex,
5067
5282
  stopRequested: false
@@ -5079,7 +5294,7 @@ function finishAgentRun(runId, status, summary, error) {
5079
5294
  const run = runs.find((r) => r.id === runId);
5080
5295
  if (!run) return;
5081
5296
  run.status = status;
5082
- run.finishedAt = nowIso();
5297
+ run.finishedAt = nowIso2();
5083
5298
  if (summary) run.summary = summary;
5084
5299
  if (error) run.error = error;
5085
5300
  }
@@ -5505,7 +5720,7 @@ var spawnAgentTool = {
5505
5720
 
5506
5721
  // src/tools/builtin/task-manager.ts
5507
5722
  import { spawn as spawn3 } from "child_process";
5508
- import { randomUUID } from "crypto";
5723
+ import { randomUUID as randomUUID2 } from "crypto";
5509
5724
  import { platform as platform4 } from "os";
5510
5725
  var MAX_OUTPUT_CHARS = 1e4;
5511
5726
  var MAX_OUTPUT_BYTES = 64 * 1024;
@@ -5538,7 +5753,7 @@ function createTask(command, description) {
5538
5753
  }
5539
5754
  }
5540
5755
  }
5541
- const id = randomUUID().slice(0, 8);
5756
+ const id = randomUUID2().slice(0, 8);
5542
5757
  const { shell, args } = shellInvocation(command);
5543
5758
  const proc = spawn3(shell, args, {
5544
5759
  stdio: ["ignore", "pipe", "pipe"],
@@ -5721,13 +5936,13 @@ var taskStopTool = {
5721
5936
  // src/tools/builtin/git-tools.ts
5722
5937
  import { execFileSync as execFileSync2 } from "child_process";
5723
5938
  import { existsSync as existsSync13 } from "fs";
5724
- import { join as join8 } from "path";
5939
+ import { join as join9 } from "path";
5725
5940
  function assertGitRepo(cwd) {
5726
5941
  let dir = cwd;
5727
5942
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
5728
5943
  while (dir && dir !== root) {
5729
- if (existsSync13(join8(dir, ".git"))) return;
5730
- const parent = join8(dir, "..");
5944
+ if (existsSync13(join9(dir, ".git"))) return;
5945
+ const parent = join9(dir, "..");
5731
5946
  if (parent === dir) break;
5732
5947
  dir = parent;
5733
5948
  }
@@ -5994,7 +6209,7 @@ ${commitOutput.trim()}`;
5994
6209
  // src/tools/builtin/notebook-edit.ts
5995
6210
  import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
5996
6211
  import { writeFile } from "fs/promises";
5997
- import { resolve as resolve6, extname as extname2 } from "path";
6212
+ import { resolve as resolve7, extname as extname2 } from "path";
5998
6213
  var notebookEditTool = {
5999
6214
  definition: {
6000
6215
  name: "notebook_edit",
@@ -6043,7 +6258,7 @@ var notebookEditTool = {
6043
6258
  if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
6044
6259
  throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
6045
6260
  }
6046
- const absPath = resolve6(filePath);
6261
+ const absPath = resolve7(filePath);
6047
6262
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
6048
6263
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
6049
6264
  }
@@ -6468,7 +6683,7 @@ function estimateToolDefinitionTokens(def) {
6468
6683
  // src/tools/registry.ts
6469
6684
  import { pathToFileURL } from "url";
6470
6685
  import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync7 } from "fs";
6471
- import { join as join9 } from "path";
6686
+ import { join as join10 } from "path";
6472
6687
  var ToolRegistry = class {
6473
6688
  tools = /* @__PURE__ */ new Map();
6474
6689
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -6655,12 +6870,12 @@ var ToolRegistry = class {
6655
6870
  process.stderr.write(
6656
6871
  `
6657
6872
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
6658
- ` + files.map((f) => ` + ${join9(pluginsDir, f)}`).join("\n") + "\n\n"
6873
+ ` + files.map((f) => ` + ${join10(pluginsDir, f)}`).join("\n") + "\n\n"
6659
6874
  );
6660
6875
  let loaded = 0;
6661
6876
  for (const file of files) {
6662
6877
  try {
6663
- const fileUrl = pathToFileURL(join9(pluginsDir, file)).href;
6878
+ const fileUrl = pathToFileURL(join10(pluginsDir, file)).href;
6664
6879
  const mod = await import(fileUrl);
6665
6880
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
6666
6881
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -6702,6 +6917,16 @@ export {
6702
6917
  getRecentlyDeniedAutoActions,
6703
6918
  clearRecentlyDeniedAutoActions,
6704
6919
  defaultActionClassifier,
6920
+ memoryStorePath,
6921
+ memoryMarkdownPath,
6922
+ isMemoryExpired,
6923
+ listMemoryEntries,
6924
+ addMemoryEntry,
6925
+ updateMemoryApproval,
6926
+ deleteMemoryEntry,
6927
+ expireMemoryEntry,
6928
+ exportMemoryEntries,
6929
+ formatMemoryForPrompt,
6705
6930
  isInterrupted,
6706
6931
  requestInterrupt,
6707
6932
  resetInterrupt,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  truncateForPersist
4
- } from "./chunk-TCOC4AUI.js";
4
+ } from "./chunk-MGBMNCHG.js";
5
5
  import {
6
6
  APP_NAME,
7
7
  CONFIG_DIR_NAME,
@@ -13,7 +13,7 @@ import {
13
13
  MCP_PROTOCOL_VERSION,
14
14
  MCP_TOOL_PREFIX,
15
15
  VERSION
16
- } from "./chunk-7YWS3JLU.js";
16
+ } from "./chunk-XJGEQIYS.js";
17
17
  import {
18
18
  atomicWriteFileSync
19
19
  } from "./chunk-IW3Q7AE5.js";
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.216";
9
+ var VERSION = "0.4.217";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -17,6 +17,7 @@ var CUSTOM_COMMANDS_DIR_NAME = "commands";
17
17
  var CONTEXT_FILE_CANDIDATES = ["AICLI.override.md", "AGENTS.override.md", "AICLI.md", "CLAUDE.md", "AGENTS.md"];
18
18
  var CONTEXT_FILE_MAX_BYTES = 32 * 1024;
19
19
  var MEMORY_FILE_NAME = "memory.md";
20
+ var MEMORY_STORE_FILE_NAME = "memory.jsonl";
20
21
  var MEMORY_MAX_CHARS = 1e4;
21
22
  var DEV_STATE_FILE_NAME = "dev-state.md";
22
23
  var DEFAULT_MAX_TOKENS = 8192;
@@ -552,6 +553,7 @@ export {
552
553
  CONTEXT_FILE_CANDIDATES,
553
554
  CONTEXT_FILE_MAX_BYTES,
554
555
  MEMORY_FILE_NAME,
556
+ MEMORY_STORE_FILE_NAME,
555
557
  MEMORY_MAX_CHARS,
556
558
  DEV_STATE_FILE_NAME,
557
559
  DEFAULT_MAX_TOKENS,