jinzd-ai-cli 0.4.240 → 0.4.242
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 +4 -4
- package/README.zh-CN.md +6 -6
- package/dist/{batch-QVWTKZF4.js → batch-D3UHL432.js} +2 -2
- package/dist/{chat-index-7HXBWQFH.js → chat-index-ALUHFJKI.js} +3 -2
- package/dist/{chat-index-WYXYD7YP.js → chat-index-ML5FF5L6.js} +2 -1
- package/dist/{chunk-SCE4HTKR.js → chunk-6ZWNXHNH.js} +1 -1
- package/dist/chunk-B5TYJO7V.js +123 -0
- package/dist/chunk-D27TMEOV.js +327 -0
- package/dist/{chunk-2QQKS56X.js → chunk-DT77O33T.js} +1 -1
- package/dist/{chunk-3RAG6N4A.js → chunk-FEMPCNX3.js} +79 -252
- package/dist/{chunk-4MBOMN32.js → chunk-FRNWQIG6.js} +5 -5
- package/dist/{chunk-5MYPIQ3Y.js → chunk-IGPFYNZA.js} +4 -2
- package/dist/{chunk-D3636NWD.js → chunk-J363BGOH.js} +1 -1
- package/dist/{chunk-JMP3LJC2.js → chunk-KWQKQVPH.js} +4 -2
- package/dist/{chunk-4URJHGUY.js → chunk-LHINXQZF.js} +1 -1
- package/dist/{chunk-ATUYVDK7.js → chunk-MC2ZNPBC.js} +4 -163
- package/dist/chunk-MKLPAQT6.js +164 -0
- package/dist/{chunk-UO3XWZFE.js → chunk-NV63EYFL.js} +3 -119
- package/dist/{chunk-MJJDVV7I.js → chunk-OO2PHOXD.js} +3 -3
- package/dist/{chunk-KCEO2XJ4.js → chunk-PJ3ONBWZ.js} +0 -119
- package/dist/{chunk-O5DZLCMC.js → chunk-PSP6UIKL.js} +2 -2
- package/dist/chunk-UWV2NBCO.js +418 -0
- package/dist/{chunk-UKIDVNA5.js → chunk-WMAXYDCB.js} +7 -1
- package/dist/{chunk-FK3WUEP6.js → chunk-Y4CMNXZH.js} +1 -1
- package/dist/chunk-YVTASHS5.js +121 -0
- package/dist/{ci-TVGNRBCM.js → ci-WSED3Z6V.js} +4 -4
- package/dist/{ci-format-MYXIMFBH.js → ci-format-6AYL2X2J.js} +2 -2
- package/dist/{constants-CGQSCJPK.js → constants-FJ3L5GYO.js} +1 -1
- package/dist/{doctor-cli-7EVHTO7C.js → doctor-cli-HCP56MKH.js} +4 -4
- package/dist/electron-server.js +423 -572
- package/dist/{hub-JAHPVHAL.js → hub-K5MYSVEF.js} +2 -2
- package/dist/index.js +203 -58
- package/dist/{persist-AMH5SQ4W.js → persist-WGIJ6RXY.js} +4 -3
- package/dist/persistent-memory-EYAKL2XA.js +55 -0
- package/dist/persistent-memory-HWFNDQFQ.js +53 -0
- package/dist/{pr-D22NW3CL.js → pr-OICMUN6R.js} +5 -5
- package/dist/{run-tests-PRGRUUKE.js → run-tests-B3FSE5NV.js} +2 -1
- package/dist/{run-tests-L4UAFYAB.js → run-tests-DSEW5HED.js} +2 -2
- package/dist/{server-CXF4XPEZ.js → server-2FS65CQZ.js} +130 -47
- package/dist/{server-ACSCQHUV.js → server-EDCGPEGC.js} +8 -6
- package/dist/{task-orchestrator-SFGLDJEX.js → task-orchestrator-WCZYV3TE.js} +8 -6
- package/dist/{usage-MCATLIB3.js → usage-WVUYNLOD.js} +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
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 30 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
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;
|
|
37
|
+
- **Governed Persistent Memory** *(v0.4.217+)* — `save_memory` and `/memory add` write auditable `memory.jsonl` entries with id/scope/source/sensitivity/approval/expiry; AI-written entries (`save_memory`) always stay pending until `/memory approve <id>` (v0.4.242+), low-risk manual entries auto-approve, project-scoped memories only inject inside the same project, and `/memory clear` confirms + keeps a timestamped backup
|
|
38
38
|
- **Package Plugin Ecosystem** *(v0.4.218+)* — install shareable .aicli-plugin/plugin.json packages with skills, hooks, commands, MCP servers, agents, and permission hints; only trusted+enabled plugins load, hooks still require hook trust, and MCP/tools remain under permission profiles/network policy
|
|
39
39
|
- **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
|
|
40
40
|
- **Streaming Tool Use** — Real-time streaming of AI reasoning and tool calls as they happen
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
- **PWA Support** — Install Web UI as a desktop/mobile app, accessible over LAN
|
|
48
48
|
- **Hierarchical Context** — 3-layer context files (global / project / subdirectory) auto-injected; supports native AICLI.md, Claude-compatible CLAUDE.md, and Codex-compatible AGENTS.md with override files
|
|
49
49
|
- **Headless Mode** — `ai-cli -p "prompt"` for CI/CD pipelines and scripting
|
|
50
|
-
- **
|
|
50
|
+
- **48 REPL Commands** — Session management, checkpointing, code review, security review/scan, rewind, scaffolding, cross-session history search, chat-memory recall, smart model routing (`/route`), and more
|
|
51
51
|
- **GitHub Actions CI/CD** — Automated testing on Node 20/22 + npm publish on release tags
|
|
52
52
|
- **PR Review CLI** *(v0.4.216+)* — `aicli pr review|security-review|summarize` reviews `main...HEAD`, custom `--base/--head`, or GitHub PR URLs; optional `--agents security,bugs,tests,maintainability`; never posts comments unless `--post` is explicit
|
|
53
53
|
- **CI Review Artifacts** *(v0.4.216+)* — `aicli ci` emits Markdown, JSON, or SARIF and gates on security-high, test-failure, and lint-failure findings in read-only review mode
|
|
@@ -173,7 +173,7 @@ AI autonomously invokes these 30 tools during conversations:
|
|
|
173
173
|
| `run_tests` | safe | Auto-detect and run project tests (JUnit XML parsing) |
|
|
174
174
|
| `spawn_agent` | safe | Delegate subtasks to named isolated agents (`agent`: explorer/worker/reviewer/security/tester) |
|
|
175
175
|
| `ask_user` | safe | Pause and ask the user a question |
|
|
176
|
-
| `save_memory` | safe | Persist governed memory across sessions;
|
|
176
|
+
| `save_memory` | safe | Persist governed memory across sessions; AI-written entries stay pending until `/memory approve <id>` |
|
|
177
177
|
| `write_todos` | safe | Task breakdown with live progress rendering |
|
|
178
178
|
| `save_last_response` | write | Save AI response to file |
|
|
179
179
|
| `task_create` | write | Start a command running in the background |
|
|
@@ -456,7 +456,7 @@ Environment variables take precedence over config file values:
|
|
|
456
456
|
| `AICLI_API_KEY_OPENAI` | OpenAI API Key |
|
|
457
457
|
| `AICLI_API_KEY_OPENROUTER` | OpenRouter API Key |
|
|
458
458
|
| `AICLI_API_KEY_ZHIPU` | Zhipu API Key |
|
|
459
|
-
| `AICLI_API_KEY_KIMI` | Kimi API Key |
|
|
459
|
+
| `AICLI_API_KEY_KIMI` | Kimi API Key |
|
|
460
460
|
| `AICLI_API_KEY_QWEN` | Qwen / Alibaba Cloud API Key |
|
|
461
461
|
| `AICLI_PROVIDER` | Default provider ID |
|
|
462
462
|
| `AICLI_NO_STREAM` | Set to `1` to disable streaming |
|
package/README.zh-CN.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
- **对话分支**(v0.4.74+)— REPL 内 `/branch list/new/switch/delete/rename/diff/cherry-pick`,Web UI 回放面板每条消息旁 🌿 "fork here" 按钮,任意位置开辟新分支探索不同方向,原对话保持不变;v0.4.81 起所有子命令支持 id / title / 唯一前缀
|
|
24
24
|
- **符号索引**(v0.4.76+,C1;v0.4.143 起多语言)— tree-sitter 持久化索引(TS / JS / TSX / JSX / Python / Go / Rust / Java / C/C++)+ 3 个只读 AI 工具(`find_symbol` / `get_outline` / `find_references`)+ `/index status/rebuild/clear`;启动后台增量刷新,`write_file` 后自动 upsert
|
|
25
25
|
- **语义代码搜索**(v0.4.77+,C2)— `search_code` AI 工具 + 本地 sentence embedding(`paraphrase-multilingual-MiniLM-L12-v2`,117 MB 一次性下载,384 维,CPU 运行);支持中英文自然语言查询代码;`/index semantic-rebuild/semantic-clear` 管理
|
|
26
|
-
- **MCP Server 模式**(v0.4.84+,E1)— `aicli mcp-serve` 把 aicli 反转为 MCP 服务器(JSON-RPC 2.0 over stdio),把
|
|
26
|
+
- **MCP Server 模式**(v0.4.84+,E1)— `aicli mcp-serve` 把 aicli 反转为 MCP 服务器(JSON-RPC 2.0 over stdio),把 30 个内置工具(含 `find_symbol` / `search_code` / `run_tests`)输出给 Claude Desktop / Cursor / 任何 MCP 客户端;支持 `--tools` 白名单、`--cwd` 覆盖、`--allow-destructive`
|
|
27
27
|
- **Session 敏感数据脱敏**(v0.4.88+)— 统一 redactor 在 session **落盘前**自动替换 `password=` / `api_key` / bearer token / OpenAI key 为 `[REDACTED:*]`;查询文本也走脱敏,secret 不会进 embedding 或日志;`/security status` + `/security scan` 审计
|
|
28
28
|
- **类人长期记忆**(v0.4.89+,B4)— 聊天记录语义索引跨 session 可召回 + `recall_memory` AI 工具 + `/memory rebuild/refresh/status/recall/index-clear`;AI 看到"上次"/"之前"/指代不明时自动回忆。复用 C2 的 MiniLM embedder
|
|
29
29
|
- **Web UI Memory 面板**(v0.4.90+,B4)— sidebar 新增 🧠 Memory 标签页,跨 session 语义搜索;每条 hit 带 **➕ Inject**(把片段作为 markdown 引用块塞进聊天输入框,用户可在上面继续打字编辑——不是静默注入上下文)和 **↗ Load**(跳转对应 session);顶部"➕ Inject top 3"一键批量
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
- **PWA 支持** — Web UI 可安装为桌面/移动应用,支持局域网访问
|
|
38
38
|
- **三层级上下文** — 全局 / 项目 / 子目录上下文文件自动注入;支持原生 `AICLI.md`、Claude 兼容 `CLAUDE.md`、Codex 兼容 `AGENTS.md` 及 override 文件
|
|
39
39
|
- **无头模式** — `aicli -p "提示词"` 用于 CI/CD 管道和脚本
|
|
40
|
-
- **
|
|
40
|
+
- **48 个 REPL 命令** — 会话管理、检查点、代码审查、安全审查/扫描、对话回退、脚手架、聊天记忆召回、智能模型路由(`/route`)等
|
|
41
41
|
- **GitHub Actions CI/CD** — Node 20/22 自动测试 + Release tag 自动发布 npm
|
|
42
42
|
- **跨平台** — Windows、macOS、Linux
|
|
43
43
|
|
|
@@ -92,7 +92,7 @@ aicli
|
|
|
92
92
|
### Web UI
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
aicli web # 在 localhost:
|
|
95
|
+
aicli web # 在 localhost:3000 启动
|
|
96
96
|
aicli web --port 8080 # 自定义端口
|
|
97
97
|
aicli web --host 0.0.0.0 # 局域网访问(手机/平板)
|
|
98
98
|
```
|
|
@@ -144,7 +144,7 @@ aicli
|
|
|
144
144
|
|
|
145
145
|
## 内置工具(Agentic 能力)
|
|
146
146
|
|
|
147
|
-
AI 在对话中可自主调用
|
|
147
|
+
AI 在对话中可自主调用 30 个工具:
|
|
148
148
|
|
|
149
149
|
| 工具 | 安全级别 | 说明 |
|
|
150
150
|
|------|---------|------|
|
|
@@ -220,7 +220,7 @@ AI 在对话中可自主调用 28 个工具:
|
|
|
220
220
|
|
|
221
221
|
**多行输入**:行末加 `\` 续行,或直接粘贴多行内容(自动检测合并)。
|
|
222
222
|
|
|
223
|
-
在 REPL 中输入 `/help` 查看全部
|
|
223
|
+
在 REPL 中输入 `/help` 查看全部 48 个命令。
|
|
224
224
|
|
|
225
225
|
## CLI 参数
|
|
226
226
|
|
|
@@ -414,7 +414,7 @@ Auto Mode 不会被项目配置静默开启。用 `/permissions recently-denied`
|
|
|
414
414
|
| `AICLI_API_KEY_OPENAI` | OpenAI API Key |
|
|
415
415
|
| `AICLI_API_KEY_OPENROUTER` | OpenRouter API Key |
|
|
416
416
|
| `AICLI_API_KEY_ZHIPU` | 智谱 API Key |
|
|
417
|
-
| `AICLI_API_KEY_KIMI` | Kimi API Key |
|
|
417
|
+
| `AICLI_API_KEY_KIMI` | Kimi API Key |
|
|
418
418
|
| `AICLI_API_KEY_QWEN` | Qwen / Alibaba Cloud API Key |
|
|
419
419
|
| `AICLI_PROVIDER` | 默认 Provider |
|
|
420
420
|
| `AICLI_NO_STREAM` | 设为 `1` 禁用流式输出 |
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConfigManager
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WMAXYDCB.js";
|
|
5
5
|
import "./chunk-5ULLIOVC.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-Y4CMNXZH.js";
|
|
7
7
|
import {
|
|
8
8
|
atomicWriteFileSync
|
|
9
9
|
} from "./chunk-IW3Q7AE5.js";
|
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
getChatIndexStatus,
|
|
7
7
|
loadChatIndex,
|
|
8
8
|
searchChatMemory
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IGPFYNZA.js";
|
|
10
10
|
import "./chunk-KHYD3WXE.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-PJ3ONBWZ.js";
|
|
12
|
+
import "./chunk-B5TYJO7V.js";
|
|
12
13
|
import "./chunk-IW3Q7AE5.js";
|
|
13
14
|
export {
|
|
14
15
|
buildChatIndex,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/security/redactor.ts
|
|
4
|
+
var DEFAULT_PATTERNS = [
|
|
5
|
+
// password: xxx / password = xxx / password="xxx"
|
|
6
|
+
// Covers YAML / JSON / shell-ish / env-file forms.
|
|
7
|
+
{ kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
|
|
8
|
+
// PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
|
|
9
|
+
{ kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
|
|
10
|
+
// JDBC/PG/MySQL/Mongo connection strings with inline credentials
|
|
11
|
+
// postgresql://user:pass@host/db → redact pass
|
|
12
|
+
{ kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
|
|
13
|
+
// Anthropic API keys
|
|
14
|
+
{ kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
|
|
15
|
+
// L6 (v0.4.108): Zhipu / GLM API keys — `<24+ hex/base64-ish>.<32+>`
|
|
16
|
+
// Two segments separated by a dot, each safely identifiable by length
|
|
17
|
+
// and char class. Conservative on the lower bound so we don't eat
|
|
18
|
+
// version strings like `1.0.0` or filenames.
|
|
19
|
+
{ kind: "zhipu-key", regex: /\b([a-zA-Z0-9]{24,}\.[a-zA-Z0-9]{32,})\b/g },
|
|
20
|
+
// OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
|
|
21
|
+
{ kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
|
|
22
|
+
// GitHub personal access tokens
|
|
23
|
+
{ kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
|
|
24
|
+
{ kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
|
|
25
|
+
{ kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
|
|
26
|
+
// Slack tokens
|
|
27
|
+
{ kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
|
|
28
|
+
{ kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
|
|
29
|
+
// AWS access key IDs (AKIA...) and secret access keys are context-dependent;
|
|
30
|
+
// we only catch the ID because secret key alone is indistinguishable from random base64.
|
|
31
|
+
{ kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
|
|
32
|
+
// Google API keys
|
|
33
|
+
{ kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
|
|
34
|
+
// Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
|
|
35
|
+
{ kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
|
|
36
|
+
// Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
|
|
37
|
+
{ kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
|
|
38
|
+
// Bearer <token> in Authorization headers
|
|
39
|
+
{ kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
|
|
40
|
+
// Private key PEM blocks — catch the header+footer together
|
|
41
|
+
// P2-PERF-02: PEM bodies use strict Base64 + =\n — narrowed char class avoids backtracking
|
|
42
|
+
{ kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[ \t\n\r]*[A-Za-z0-9+/=\n\r-]*[ \t\n\r]*-----END [A-Z ]*PRIVATE KEY-----/g }
|
|
43
|
+
];
|
|
44
|
+
var MAX_CUSTOM = 32;
|
|
45
|
+
var MAX_PATTERN_LEN = 500;
|
|
46
|
+
var SUSPICIOUS_REDOS = /\([^)]*[+*][^)]*\)\s*[+*{]/;
|
|
47
|
+
var MAX_REDACT_INPUT = 512e3;
|
|
48
|
+
function render(placeholder, kind) {
|
|
49
|
+
return placeholder.replace("{kind}", kind);
|
|
50
|
+
}
|
|
51
|
+
function redactString(input, options) {
|
|
52
|
+
if (!options.enabled || !input) return { redacted: input, hits: [] };
|
|
53
|
+
if (input.length > MAX_REDACT_INPUT) return { redacted: input, hits: [] };
|
|
54
|
+
const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
|
|
55
|
+
const customSrcs = (options.customRegexes ?? []).slice(0, MAX_CUSTOM);
|
|
56
|
+
const patterns = [
|
|
57
|
+
...options.patterns ?? DEFAULT_PATTERNS,
|
|
58
|
+
...customSrcs.flatMap((src, i) => {
|
|
59
|
+
if (typeof src !== "string" || src.length === 0 || src.length > MAX_PATTERN_LEN) return [];
|
|
60
|
+
try {
|
|
61
|
+
const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
|
|
62
|
+
const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
|
|
63
|
+
if (SUSPICIOUS_REDOS.test(body)) return [];
|
|
64
|
+
const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
|
|
65
|
+
return [{ kind: `custom-${i}`, regex }];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
];
|
|
71
|
+
let redacted = input;
|
|
72
|
+
const hits = [];
|
|
73
|
+
for (const { kind, regex } of patterns) {
|
|
74
|
+
const rx = new RegExp(regex.source, regex.flags);
|
|
75
|
+
const captureCount = new RegExp(rx.source + "|").exec("").length - 1;
|
|
76
|
+
redacted = redacted.replace(rx, (...args) => {
|
|
77
|
+
const match = args[0];
|
|
78
|
+
const g1 = captureCount >= 1 ? args[1] : void 0;
|
|
79
|
+
const g2 = captureCount >= 2 ? args[2] : void 0;
|
|
80
|
+
const offset = args[1 + captureCount];
|
|
81
|
+
if (captureCount >= 2 && typeof g2 === "string") {
|
|
82
|
+
hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
|
|
83
|
+
return `${g1}${render(placeholder, kind)}`;
|
|
84
|
+
}
|
|
85
|
+
hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
|
|
86
|
+
return render(placeholder, kind);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { redacted, hits };
|
|
90
|
+
}
|
|
91
|
+
function redactJson(value, options) {
|
|
92
|
+
if (!options.enabled) return { value, hits: [] };
|
|
93
|
+
const allHits = [];
|
|
94
|
+
function walk(v) {
|
|
95
|
+
if (typeof v === "string") {
|
|
96
|
+
const r = redactString(v, options);
|
|
97
|
+
allHits.push(...r.hits);
|
|
98
|
+
return r.redacted;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
101
|
+
if (v && typeof v === "object") {
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const [k, vv] of Object.entries(v)) {
|
|
104
|
+
out[k] = walk(vv);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
return v;
|
|
109
|
+
}
|
|
110
|
+
const redacted = walk(value);
|
|
111
|
+
return { value: redacted, hits: allHits };
|
|
112
|
+
}
|
|
113
|
+
function scanString(input, options) {
|
|
114
|
+
const { hits } = redactString(input, { ...options, enabled: true });
|
|
115
|
+
return hits;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
DEFAULT_PATTERNS,
|
|
120
|
+
redactString,
|
|
121
|
+
redactJson,
|
|
122
|
+
scanString
|
|
123
|
+
};
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getGitRoot
|
|
4
|
+
} from "./chunk-HOSJZMQS.js";
|
|
5
|
+
import {
|
|
6
|
+
MEMORY_FILE_NAME,
|
|
7
|
+
MEMORY_MAX_CHARS,
|
|
8
|
+
MEMORY_STORE_FILE_NAME
|
|
9
|
+
} from "./chunk-Y4CMNXZH.js";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_PATTERNS,
|
|
12
|
+
redactString
|
|
13
|
+
} from "./chunk-B5TYJO7V.js";
|
|
14
|
+
import {
|
|
15
|
+
atomicWriteFileSync
|
|
16
|
+
} from "./chunk-IW3Q7AE5.js";
|
|
17
|
+
|
|
18
|
+
// src/memory/persistent-memory.ts
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from "fs";
|
|
20
|
+
import { randomUUID, createHash } from "crypto";
|
|
21
|
+
import { dirname, join, resolve } from "path";
|
|
22
|
+
function parseWritableScope(value) {
|
|
23
|
+
return value === "personal" || value === "project" ? value : void 0;
|
|
24
|
+
}
|
|
25
|
+
function memoryStorePath(configDir) {
|
|
26
|
+
return join(configDir, MEMORY_STORE_FILE_NAME);
|
|
27
|
+
}
|
|
28
|
+
function memoryMarkdownPath(configDir) {
|
|
29
|
+
return join(configDir, MEMORY_FILE_NAME);
|
|
30
|
+
}
|
|
31
|
+
function getProjectKey(cwd = process.cwd()) {
|
|
32
|
+
return resolve(getGitRoot(cwd) ?? cwd);
|
|
33
|
+
}
|
|
34
|
+
function nowIso() {
|
|
35
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
36
|
+
}
|
|
37
|
+
function isValidScope(value) {
|
|
38
|
+
return value === "personal" || value === "project" || value === "session" || value === "team";
|
|
39
|
+
}
|
|
40
|
+
function isValidSensitivity(value) {
|
|
41
|
+
return value === "low" || value === "medium" || value === "high";
|
|
42
|
+
}
|
|
43
|
+
function isValidSource(value) {
|
|
44
|
+
return value === "tool" || value === "manual";
|
|
45
|
+
}
|
|
46
|
+
function normalizeEntry(value) {
|
|
47
|
+
if (!value || typeof value !== "object") return null;
|
|
48
|
+
const raw = value;
|
|
49
|
+
const content = typeof raw.content === "string" ? raw.content.trim() : "";
|
|
50
|
+
if (!content) return null;
|
|
51
|
+
const createdAt = typeof raw.createdAt === "string" ? raw.createdAt : nowIso();
|
|
52
|
+
return {
|
|
53
|
+
id: typeof raw.id === "string" && raw.id ? raw.id : randomUUID(),
|
|
54
|
+
content,
|
|
55
|
+
scope: isValidScope(raw.scope) ? raw.scope : "personal",
|
|
56
|
+
source: isValidSource(raw.source) ? raw.source : void 0,
|
|
57
|
+
sourceSession: typeof raw.sourceSession === "string" && raw.sourceSession ? raw.sourceSession : void 0,
|
|
58
|
+
sourceProject: typeof raw.sourceProject === "string" && raw.sourceProject ? raw.sourceProject : void 0,
|
|
59
|
+
createdAt,
|
|
60
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : createdAt,
|
|
61
|
+
expiresAt: typeof raw.expiresAt === "string" && raw.expiresAt ? raw.expiresAt : void 0,
|
|
62
|
+
sensitivity: isValidSensitivity(raw.sensitivity) ? raw.sensitivity : "low",
|
|
63
|
+
approved: raw.approved === true,
|
|
64
|
+
redactedKinds: Array.isArray(raw.redactedKinds) ? raw.redactedKinds.filter((x) => typeof x === "string") : void 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function legacyEntriesFromMarkdown(configDir) {
|
|
68
|
+
const file = memoryMarkdownPath(configDir);
|
|
69
|
+
if (!existsSync(file)) return [];
|
|
70
|
+
const content = readFileSync(file, "utf-8").trim();
|
|
71
|
+
if (!content) return [];
|
|
72
|
+
const parts = content.split(/\n(?=##\s+\d{4}-\d{2}-\d{2})/g);
|
|
73
|
+
const chunks = parts.length > 1 ? parts : [content];
|
|
74
|
+
return chunks.map((chunk, index) => {
|
|
75
|
+
const match = chunk.match(/^##\s+([^\n]+)\n([\s\S]*)$/);
|
|
76
|
+
const body = (match ? match[2] : chunk).trim();
|
|
77
|
+
const parsed = match ? Date.parse(match[1].replace(" ", "T")) : NaN;
|
|
78
|
+
const createdAt = Number.isFinite(parsed) ? new Date(parsed).toISOString() : nowIso();
|
|
79
|
+
const id = "legacy-" + createHash("sha1").update(`${index}
|
|
80
|
+
${chunk}`).digest("hex").slice(0, 12);
|
|
81
|
+
return normalizeEntry({ id, content: body, scope: "personal", createdAt, updatedAt: createdAt, sensitivity: "low", approved: true });
|
|
82
|
+
}).filter((entry) => Boolean(entry));
|
|
83
|
+
}
|
|
84
|
+
function loadMemoryEntries(configDir) {
|
|
85
|
+
const file = memoryStorePath(configDir);
|
|
86
|
+
if (!existsSync(file)) return legacyEntriesFromMarkdown(configDir);
|
|
87
|
+
const text = readFileSync(file, "utf-8").trim();
|
|
88
|
+
if (!text) return [];
|
|
89
|
+
const entries = [];
|
|
90
|
+
for (const line of text.split("\n")) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (!trimmed) continue;
|
|
93
|
+
try {
|
|
94
|
+
const entry = normalizeEntry(JSON.parse(trimmed));
|
|
95
|
+
if (entry) entries.push(entry);
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
function saveMemoryEntries(configDir, entries) {
|
|
102
|
+
mkdirSync(configDir, { recursive: true });
|
|
103
|
+
const jsonl = entries.map((entry) => JSON.stringify(entry)).join("\n");
|
|
104
|
+
atomicWriteFileSync(memoryStorePath(configDir), jsonl ? jsonl + "\n" : "");
|
|
105
|
+
}
|
|
106
|
+
function isMemoryExpired(entry, at = /* @__PURE__ */ new Date()) {
|
|
107
|
+
return Boolean(entry.expiresAt && Date.parse(entry.expiresAt) <= at.getTime());
|
|
108
|
+
}
|
|
109
|
+
function isMemoryVisibleInProject(entry, cwd = process.cwd()) {
|
|
110
|
+
if (entry.scope !== "project") return true;
|
|
111
|
+
return entry.sourceProject === getProjectKey(cwd);
|
|
112
|
+
}
|
|
113
|
+
function listMemoryEntries(configDir, options = {}) {
|
|
114
|
+
const cwd = options.cwd ?? process.cwd();
|
|
115
|
+
return loadMemoryEntries(configDir).filter((entry) => options.includeRejected || entry.approved).filter((entry) => options.includeExpired || !isMemoryExpired(entry)).filter((entry) => isMemoryVisibleInProject(entry, cwd));
|
|
116
|
+
}
|
|
117
|
+
function addMemoryEntry(configDir, content, options = {}) {
|
|
118
|
+
const original = content.trim();
|
|
119
|
+
if (!original) throw new Error("memory content is required");
|
|
120
|
+
const redact = options.redact ?? true;
|
|
121
|
+
const redacted = redact ? redactString(original, { enabled: true, patterns: options.patterns ?? DEFAULT_PATTERNS, customRegexes: options.customPatterns }) : { redacted: original, hits: [] };
|
|
122
|
+
const sensitivity = options.sensitivity ?? (redacted.hits.length > 0 ? "high" : "low");
|
|
123
|
+
const approved = options.approved ?? (sensitivity === "low" && redacted.hits.length === 0 && options.source !== "tool");
|
|
124
|
+
const scope = options.scope ?? "personal";
|
|
125
|
+
const timestamp = nowIso();
|
|
126
|
+
const entry = {
|
|
127
|
+
id: randomUUID(),
|
|
128
|
+
content: redacted.redacted.trim(),
|
|
129
|
+
scope,
|
|
130
|
+
source: options.source,
|
|
131
|
+
sourceSession: options.sourceSession,
|
|
132
|
+
sourceProject: scope === "project" ? getProjectKey(options.cwd) : void 0,
|
|
133
|
+
createdAt: timestamp,
|
|
134
|
+
updatedAt: timestamp,
|
|
135
|
+
expiresAt: options.expiresAt,
|
|
136
|
+
sensitivity,
|
|
137
|
+
approved,
|
|
138
|
+
redactedKinds: [...new Set(redacted.hits.map((hit) => hit.kind))]
|
|
139
|
+
};
|
|
140
|
+
const entries = loadMemoryEntries(configDir);
|
|
141
|
+
entries.push(entry);
|
|
142
|
+
saveMemoryEntries(configDir, entries);
|
|
143
|
+
return entry;
|
|
144
|
+
}
|
|
145
|
+
function updateMemoryEntry(configDir, idPrefix, newContent, options = {}) {
|
|
146
|
+
const content = newContent.trim();
|
|
147
|
+
if (!content) throw new Error("memory content is required");
|
|
148
|
+
const entries = loadMemoryEntries(configDir);
|
|
149
|
+
const entry = findUnique(entries, idPrefix);
|
|
150
|
+
const redact = options.redact ?? true;
|
|
151
|
+
const redacted = redact ? redactString(content, { enabled: true, patterns: options.patterns ?? DEFAULT_PATTERNS, customRegexes: options.customPatterns }) : { redacted: content, hits: [] };
|
|
152
|
+
entry.content = redacted.redacted.trim();
|
|
153
|
+
entry.updatedAt = nowIso();
|
|
154
|
+
entry.redactedKinds = [...new Set(redacted.hits.map((hit) => hit.kind))];
|
|
155
|
+
if (redacted.hits.length > 0) {
|
|
156
|
+
entry.sensitivity = "high";
|
|
157
|
+
entry.approved = false;
|
|
158
|
+
} else if (options.source === "tool") {
|
|
159
|
+
entry.approved = false;
|
|
160
|
+
}
|
|
161
|
+
saveMemoryEntries(configDir, entries);
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
function updateMemoryApproval(configDir, idPrefix, approved) {
|
|
165
|
+
const entries = loadMemoryEntries(configDir);
|
|
166
|
+
const entry = findUnique(entries, idPrefix);
|
|
167
|
+
entry.approved = approved;
|
|
168
|
+
entry.updatedAt = nowIso();
|
|
169
|
+
saveMemoryEntries(configDir, entries);
|
|
170
|
+
return entry;
|
|
171
|
+
}
|
|
172
|
+
function deleteMemoryEntry(configDir, idPrefix) {
|
|
173
|
+
const entries = loadMemoryEntries(configDir);
|
|
174
|
+
const entry = findUnique(entries, idPrefix);
|
|
175
|
+
saveMemoryEntries(configDir, entries.filter((candidate) => candidate.id !== entry.id));
|
|
176
|
+
return entry;
|
|
177
|
+
}
|
|
178
|
+
function expireMemoryEntry(configDir, idPrefix, expiresAt = nowIso()) {
|
|
179
|
+
const entries = loadMemoryEntries(configDir);
|
|
180
|
+
const entry = findUnique(entries, idPrefix);
|
|
181
|
+
entry.expiresAt = expiresAt;
|
|
182
|
+
entry.updatedAt = nowIso();
|
|
183
|
+
saveMemoryEntries(configDir, entries);
|
|
184
|
+
return entry;
|
|
185
|
+
}
|
|
186
|
+
function countPendingMemories(configDir) {
|
|
187
|
+
return loadMemoryEntries(configDir).filter((entry) => !entry.approved && !isMemoryExpired(entry)).length;
|
|
188
|
+
}
|
|
189
|
+
function clearMemoryEntries(configDir) {
|
|
190
|
+
const entries = loadMemoryEntries(configDir);
|
|
191
|
+
const storePath = memoryStorePath(configDir);
|
|
192
|
+
let backupPath = null;
|
|
193
|
+
if (entries.length > 0) {
|
|
194
|
+
const sourceFile = existsSync(storePath) ? storePath : memoryMarkdownPath(configDir);
|
|
195
|
+
if (existsSync(sourceFile)) {
|
|
196
|
+
backupPath = `${sourceFile}.bak.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
197
|
+
copyFileSync(sourceFile, backupPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
mkdirSync(configDir, { recursive: true });
|
|
201
|
+
atomicWriteFileSync(storePath, "");
|
|
202
|
+
atomicWriteFileSync(memoryMarkdownPath(configDir), "");
|
|
203
|
+
return { backupPath, cleared: entries.length };
|
|
204
|
+
}
|
|
205
|
+
function exportMemoryEntries(configDir, cwd = process.cwd(), format = "json") {
|
|
206
|
+
if (format === "md") {
|
|
207
|
+
const active = listMemoryEntries(configDir, { cwd }).filter((entry) => entry.approved && !isMemoryExpired(entry));
|
|
208
|
+
return renderMemoryMarkdown(active);
|
|
209
|
+
}
|
|
210
|
+
return JSON.stringify(listMemoryEntries(configDir, { cwd, includeExpired: true, includeRejected: true }), null, 2);
|
|
211
|
+
}
|
|
212
|
+
function findUnique(entries, idPrefix) {
|
|
213
|
+
const matches = entries.filter((entry) => entry.id.startsWith(idPrefix));
|
|
214
|
+
if (matches.length === 0) throw new Error(`memory id not found: ${idPrefix}`);
|
|
215
|
+
if (matches.length > 1) throw new Error(`memory id is ambiguous: ${idPrefix}`);
|
|
216
|
+
return matches[0];
|
|
217
|
+
}
|
|
218
|
+
function renderMemoryMarkdown(entries) {
|
|
219
|
+
return entries.map((entry) => {
|
|
220
|
+
const date = entry.createdAt.replace("T", " ").slice(0, 19);
|
|
221
|
+
const source = [
|
|
222
|
+
`id:${entry.id.slice(0, 8)}`,
|
|
223
|
+
`scope:${entry.scope}`,
|
|
224
|
+
entry.source ? `source:${entry.source}` : "",
|
|
225
|
+
entry.sourceSession ? `session:${entry.sourceSession.slice(0, 8)}` : "",
|
|
226
|
+
entry.sourceProject ? `project:${entry.sourceProject}` : "",
|
|
227
|
+
`sensitivity:${entry.sensitivity}`
|
|
228
|
+
].filter(Boolean).join(" \xB7 ");
|
|
229
|
+
return `## ${date}
|
|
230
|
+
<!-- ${source} -->
|
|
231
|
+
${entry.content}
|
|
232
|
+
`;
|
|
233
|
+
}).join("\n");
|
|
234
|
+
}
|
|
235
|
+
function syncLegacyMarkdown(configDir, entries = loadMemoryEntries(configDir)) {
|
|
236
|
+
mkdirSync(dirname(memoryMarkdownPath(configDir)), { recursive: true });
|
|
237
|
+
const active = entries.filter((entry) => entry.approved && !isMemoryExpired(entry));
|
|
238
|
+
atomicWriteFileSync(memoryMarkdownPath(configDir), renderMemoryMarkdown(active));
|
|
239
|
+
}
|
|
240
|
+
function formatMemoryForPrompt(configDir, cwd = process.cwd(), maxChars = MEMORY_MAX_CHARS) {
|
|
241
|
+
const entries = listMemoryEntries(configDir, { cwd }).filter((entry) => entry.approved && !isMemoryExpired(entry));
|
|
242
|
+
if (entries.length === 0) return null;
|
|
243
|
+
let content = entries.map((entry) => {
|
|
244
|
+
const meta = [
|
|
245
|
+
`id ${entry.id.slice(0, 8)}`,
|
|
246
|
+
entry.scope,
|
|
247
|
+
entry.source ?? "unknown",
|
|
248
|
+
entry.createdAt.slice(0, 10)
|
|
249
|
+
].join(" \xB7 ");
|
|
250
|
+
return `## ${meta}
|
|
251
|
+
${entry.content}`;
|
|
252
|
+
}).join("\n\n");
|
|
253
|
+
let entryCount = entries.length;
|
|
254
|
+
let truncated = false;
|
|
255
|
+
if (content.length > maxChars) {
|
|
256
|
+
truncated = true;
|
|
257
|
+
content = content.slice(-maxChars);
|
|
258
|
+
const firstEntry = content.indexOf("\n## ");
|
|
259
|
+
if (firstEntry !== -1) content = content.slice(firstEntry + 1);
|
|
260
|
+
entryCount = Math.max(1, (content.match(/^## /gm) ?? []).length);
|
|
261
|
+
content = `(Note: persistent memory exceeded the ${maxChars}-char budget \u2014 showing the newest ${entryCount} of ${entries.length} entries; older entries were dropped. The user can prune with /memory show and /memory delete <id>.)
|
|
262
|
+
|
|
263
|
+
` + content;
|
|
264
|
+
}
|
|
265
|
+
return { content, entryCount, totalCount: entries.length, truncated };
|
|
266
|
+
}
|
|
267
|
+
function findSimilarMemory(entries, content) {
|
|
268
|
+
const normalize = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
269
|
+
const target = normalize(content);
|
|
270
|
+
if (target.length < 8) return null;
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
const existing = normalize(entry.content);
|
|
273
|
+
if (existing === target || existing.includes(target) || target.includes(existing)) return entry;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function listOrphanedProjectMemories(configDir) {
|
|
278
|
+
return loadMemoryEntries(configDir).filter((entry) => entry.scope === "project" && entry.sourceProject !== void 0 && !existsSync(entry.sourceProject));
|
|
279
|
+
}
|
|
280
|
+
function rebindMemoryProject(configDir, idPrefix, newProjectPath) {
|
|
281
|
+
const entries = loadMemoryEntries(configDir);
|
|
282
|
+
const entry = findUnique(entries, idPrefix);
|
|
283
|
+
if (entry.scope !== "project") throw new Error(`memory ${entry.id.slice(0, 8)} is not project-scoped (scope: ${entry.scope})`);
|
|
284
|
+
entry.sourceProject = getProjectKey(resolve(newProjectPath));
|
|
285
|
+
entry.updatedAt = nowIso();
|
|
286
|
+
saveMemoryEntries(configDir, entries);
|
|
287
|
+
return entry;
|
|
288
|
+
}
|
|
289
|
+
function detectStaleLegacyMarkdownEdit(configDir) {
|
|
290
|
+
const jsonlPath = memoryStorePath(configDir);
|
|
291
|
+
const mdPath = memoryMarkdownPath(configDir);
|
|
292
|
+
if (!existsSync(jsonlPath) || !existsSync(mdPath)) return false;
|
|
293
|
+
try {
|
|
294
|
+
if (!readFileSync(mdPath, "utf-8").trim()) return false;
|
|
295
|
+
const jsonlMtime = statSync(jsonlPath).mtimeMs;
|
|
296
|
+
const mdMtime = statSync(mdPath).mtimeMs;
|
|
297
|
+
return mdMtime > jsonlMtime + 2e3;
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export {
|
|
304
|
+
parseWritableScope,
|
|
305
|
+
memoryStorePath,
|
|
306
|
+
memoryMarkdownPath,
|
|
307
|
+
getProjectKey,
|
|
308
|
+
loadMemoryEntries,
|
|
309
|
+
isMemoryExpired,
|
|
310
|
+
isMemoryVisibleInProject,
|
|
311
|
+
listMemoryEntries,
|
|
312
|
+
addMemoryEntry,
|
|
313
|
+
updateMemoryEntry,
|
|
314
|
+
updateMemoryApproval,
|
|
315
|
+
deleteMemoryEntry,
|
|
316
|
+
expireMemoryEntry,
|
|
317
|
+
countPendingMemories,
|
|
318
|
+
clearMemoryEntries,
|
|
319
|
+
exportMemoryEntries,
|
|
320
|
+
renderMemoryMarkdown,
|
|
321
|
+
syncLegacyMarkdown,
|
|
322
|
+
formatMemoryForPrompt,
|
|
323
|
+
findSimilarMemory,
|
|
324
|
+
listOrphanedProjectMemories,
|
|
325
|
+
rebindMemoryProject,
|
|
326
|
+
detectStaleLegacyMarkdownEdit
|
|
327
|
+
};
|