open-memex 0.3.0 → 0.4.0-alpha.1
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/AGENTS.md +11 -1
- package/README.md +67 -6
- package/README.zh-CN.md +58 -6
- package/dist/cli.js +214 -11
- package/dist/config.js +10 -0
- package/dist/github.js +215 -0
- package/dist/index.js +6 -0
- package/dist/init.js +30 -18
- package/dist/mcp.js +44 -8
- package/dist/paths.js +31 -0
- package/dist/retrieve/inject.js +2 -2
- package/dist/retrieve/search.js +31 -6
- package/dist/review.js +437 -0
- package/dist/store/db.js +3 -2
- package/dist/store/lifecycle.js +28 -13
- package/dist/store/markdown.js +49 -8
- package/dist/store/sync.js +74 -6
- package/dist/submit.js +339 -0
- package/dist/tools/ops.js +163 -6
- package/docs/USER-GUIDE.md +195 -0
- package/docs/USER-GUIDE.zh-CN.md +160 -0
- package/docs/V2-DESIGN.md +108 -6
- package/package.json +1 -1
- package/scripts/smoke-mcp.ts +2 -2
- package/src/cli.ts +204 -12
- package/src/config.ts +16 -0
- package/src/github.ts +257 -0
- package/src/index.ts +6 -0
- package/src/init.ts +30 -17
- package/src/mcp.ts +86 -7
- package/src/paths.ts +32 -0
- package/src/retrieve/inject.ts +2 -2
- package/src/retrieve/search.ts +33 -6
- package/src/review.ts +504 -0
- package/src/store/db.ts +3 -2
- package/src/store/lifecycle.ts +30 -12
- package/src/store/markdown.ts +83 -8
- package/src/store/sync.ts +85 -4
- package/src/submit.ts +411 -0
- package/src/tools/ops.ts +192 -7
package/AGENTS.md
CHANGED
|
@@ -12,7 +12,7 @@ via `prepublishOnly` — Node refuses `--experimental-strip-types` for files und
|
|
|
12
12
|
|
|
13
13
|
- **opencode host** loads `src/index.ts` under embedded **Bun**. SQLite here is `bun:sqlite` (built-in).
|
|
14
14
|
- **CLI** (`src/cli.ts`) and smoke tests run under **Node 22+** with `--experimental-strip-types`. SQLite here is `better-sqlite3` (native module).
|
|
15
|
-
- **MCP server** (`src/mcp.ts`, stdio) runs under **Node 22+** with `--experimental-strip-types`. It exposes the same
|
|
15
|
+
- **MCP server** (`src/mcp.ts`, stdio) runs under **Node 22+** with `--experimental-strip-types`. It exposes the same ten memory tools to any MCP client (VS Code Copilot, Cursor, Claude Code). **stdout is the protocol channel — never log to stdout in `mcp.ts`; diagnostics go to stderr.**
|
|
16
16
|
|
|
17
17
|
`src/store/db.ts` picks the backend at runtime by sniffing `globalThis.Bun`. Both backends share the same surface (`new Database(path)`, `.exec`, `.prepare().run/all/get`, `.close`). Any DB code you write must stay on that common subset — do not import `better-sqlite3` or `bun:sqlite` directly outside `db.ts`.
|
|
18
18
|
|
|
@@ -29,6 +29,12 @@ Consequences:
|
|
|
29
29
|
npm install # once
|
|
30
30
|
npm run typecheck # tsc --noEmit — the only lint/type gate
|
|
31
31
|
npm run cli -- where | list | search "q" | add ... | forget <id> | reindex
|
|
32
|
+
npm run cli -- sync-status # last sync time/kind + outbox drafts + repo review states + uncommitted files
|
|
33
|
+
npm run cli -- submit <id...> [--onto <branch>] [--base <branch>] # drafts → .ai/open-memex/ (local branch+commit)
|
|
34
|
+
npm run cli -- propose <id...> --to project [--local-approve] # copy personal → project outbox (batch OK)
|
|
35
|
+
npm run cli -- promote <id> [--reject] [--resubmit] [--note "..."] # proposed → approved → published (audit trail appended)
|
|
36
|
+
npm run cli -- pr-status [--apply] # map branch PR's GitHub state onto review_state (report; apply = local only)
|
|
37
|
+
npm run cli -- resolve [id-or-path] # list / 3-way-merge conflicted memories
|
|
32
38
|
npm run mcp # start the stdio MCP server
|
|
33
39
|
node --experimental-strip-types scripts\smoke-pure.ts # runs pure-logic checks (no sqlite)
|
|
34
40
|
node --experimental-strip-types scripts\smoke-mcp.ts # MCP handshake + tool round-trip (temp dirs, no real data)
|
|
@@ -129,3 +135,7 @@ hold `V2` and `V2/…` simultaneously. Full rules: `CONTRIBUTING.md`.
|
|
|
129
135
|
and the frozen `docs/V2-DESIGN.md` (append a `D<n>` decision entry, never rewrite history);
|
|
130
136
|
new commands → README CLI sections + this file's Commands. A change without its docs
|
|
131
137
|
is not done.
|
|
138
|
+
- **Version bumps ship with features.** `package.json` + `package-lock.json` carry the
|
|
139
|
+
in-development version. New features on a dev branch bump the minor on the alpha
|
|
140
|
+
line (`0.3.0` → `0.4.0-alpha.1`); fixes bump the patch (`-alpha.1` → `-alpha.2`).
|
|
141
|
+
The bump goes in the same commit as the feature, never as an afterthought.
|
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ open-memex doctor
|
|
|
153
153
|
|
|
154
154
|
Checks: Node version, config source, scope resolution for the current directory,
|
|
155
155
|
storage writability, then boots a real MCP server and runs `initialize` +
|
|
156
|
-
`tools/list` against it — all
|
|
156
|
+
`tools/list` against it — all eleven tools must show up.
|
|
157
157
|
|
|
158
158
|
## Tools the agent gets
|
|
159
159
|
|
|
@@ -164,6 +164,12 @@ storage writability, then boots a real MCP server and runs `initialize` +
|
|
|
164
164
|
| `memory_list` | List memories in a scope, newest first |
|
|
165
165
|
| `memory_supersede` | Replace a memory with a newer version (keeps a supersede chain) |
|
|
166
166
|
| `memory_forget` | Delete a memory by id |
|
|
167
|
+
| `memory_status` | Show the sync queue: outbox drafts, repo review states, uncommitted files |
|
|
168
|
+
| `memory_submit` | Move named drafts into the repo memory dir (local branch + commit) |
|
|
169
|
+
| `memory_propose` | Copy personal memories into the project scope as review candidates |
|
|
170
|
+
| `memory_promote` | Advance `proposed → approved → published` (or reject / resubmit) |
|
|
171
|
+
| `memory_resolve` | List conflicted memory files / 3-way-merge one of them |
|
|
172
|
+
| `memory_pr_status` | Map the branch PR's GitHub state onto each memory's review state |
|
|
167
173
|
|
|
168
174
|
## Capture
|
|
169
175
|
|
|
@@ -222,10 +228,17 @@ Defaults:
|
|
|
222
228
|
"maxProfileItems": 5, // top-N personal items injected on first turn
|
|
223
229
|
"injectOnFirstTurn": true, // [OPEN-MEMEX] system-prompt block
|
|
224
230
|
"keywordCaptureEnabled": true,
|
|
225
|
-
"logLevel": "info" // info | debug
|
|
231
|
+
"logLevel": "info", // info | debug
|
|
232
|
+
"memoryDir": ".ai/open-memex" // in-repo project-memory dir, relative to repo root
|
|
226
233
|
}
|
|
227
234
|
```
|
|
228
235
|
|
|
236
|
+
Project-scope memories are stored as one Markdown file each under
|
|
237
|
+
`<repo>/<memoryDir>/` (default `.ai/open-memex/`) so they can be shared via git;
|
|
238
|
+
personal memories stay in local appdata and never leave the machine. Existing
|
|
239
|
+
project files from appdata are moved into the repo dir automatically on first
|
|
240
|
+
write/sync.
|
|
241
|
+
|
|
229
242
|
`open-memex config` prints the effective config (defaults + file).
|
|
230
243
|
Change a setting after install:
|
|
231
244
|
|
|
@@ -235,7 +248,7 @@ open-memex config set maxProjectMemories 12
|
|
|
235
248
|
```
|
|
236
249
|
|
|
237
250
|
Settable keys: `maxProjectMemories`, `maxProfileItems`, `injectOnFirstTurn`,
|
|
238
|
-
`keywordCaptureEnabled`, `logLevel`. Full design: [docs/V2-DESIGN.md](./docs/V2-DESIGN.md).
|
|
251
|
+
`keywordCaptureEnabled`, `logLevel`, `memoryDir`. Full design: [docs/V2-DESIGN.md](./docs/V2-DESIGN.md).
|
|
239
252
|
|
|
240
253
|
## CLI reference
|
|
241
254
|
|
|
@@ -263,6 +276,52 @@ open-memex status <id> deprecated
|
|
|
263
276
|
open-memex forget <id>
|
|
264
277
|
```
|
|
265
278
|
|
|
279
|
+
Team review workflow (Phase 2B — two homes, one per stage):
|
|
280
|
+
|
|
281
|
+
Project drafts live in the **appdata outbox** (git-invisible, branch-independent);
|
|
282
|
+
only user-approved drafts move into `<repo>/.ai/open-memex/`, where they follow
|
|
283
|
+
branches and PRs. Nothing moves without you naming it.
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
open-memex sync-status
|
|
287
|
+
# show when the index was last synced (and what triggered it), the outbox
|
|
288
|
+
# (pending sync), the repo review states
|
|
289
|
+
# (draft / proposed / approved / published / rejected),
|
|
290
|
+
# and any uncommitted repo memory files.
|
|
291
|
+
|
|
292
|
+
open-memex submit <id...> [--onto <branch>] [--base <branch>]
|
|
293
|
+
# move your named drafts into .ai/open-memex/ as "proposed":
|
|
294
|
+
# creates mem/sync-<timestamp> (or stays on --onto for a code+memory PR),
|
|
295
|
+
# copies, flips review_state, local git commit. All-or-nothing; conflicts
|
|
296
|
+
# (same id, different content) abort cleanly. Prints the push + gh pr
|
|
297
|
+
# commands; an agent holding your Yes carries through push/PR itself.
|
|
298
|
+
# Default PR base is the current branch; --base redirects to main or your
|
|
299
|
+
# integration branch.
|
|
300
|
+
|
|
301
|
+
open-memex pr-status [--apply]
|
|
302
|
+
# read the branch's GitHub PR and map its state onto each in-repo memory:
|
|
303
|
+
# merged PR → published, PR approval → approved (approved_by = reviewer),
|
|
304
|
+
# changes-requested → suggestion only. Report by default; --apply performs
|
|
305
|
+
# the mapped transitions locally (no push).
|
|
306
|
+
|
|
307
|
+
open-memex propose <id...> --to project [--local-approve]
|
|
308
|
+
# propose one or several personal memories at once (one branch, one PR);
|
|
309
|
+
# each is copied with its own new id. All-or-nothing: a bad id aborts the
|
|
310
|
+
# whole batch, never a half-proposed one.
|
|
311
|
+
# copy a personal memory into the project scope as a review candidate
|
|
312
|
+
# (never moves — the personal original stays). Result lands in the outbox;
|
|
313
|
+
# run sync-status / submit when you're ready to put it in the repo.
|
|
314
|
+
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
315
|
+
# advance one step: proposed → approved → published (or reject with a note).
|
|
316
|
+
# Every transition is appended to the memory's review_history (who/when/why).
|
|
317
|
+
# A rejection never deletes the file — your call: accept it (close the PR,
|
|
318
|
+
# delete the branch), revise + --resubmit for another round, or keep it as
|
|
319
|
+
# a [rejected] record.
|
|
320
|
+
open-memex resolve [id-or-path]
|
|
321
|
+
# list conflicted memory files, or field-level 3-way merge one of them.
|
|
322
|
+
# Semantic conflicts are reported, never auto-resolved.
|
|
323
|
+
```
|
|
324
|
+
|
|
266
325
|
Maintenance:
|
|
267
326
|
|
|
268
327
|
```sh
|
|
@@ -280,7 +339,7 @@ simple cases — npm swallows unknown `--flag` args, so prefer direct `node`).
|
|
|
280
339
|
|
|
281
340
|
## MCP server
|
|
282
341
|
|
|
283
|
-
The same
|
|
342
|
+
The same eleven memory tools over the Model Context Protocol via a stdio server —
|
|
284
343
|
no host-specific plugin needed. Any MCP client can use open-memex.
|
|
285
344
|
|
|
286
345
|
```sh
|
|
@@ -302,8 +361,10 @@ server with cwd set to your project root (`init` handles this for you).
|
|
|
302
361
|
`init` setup, Chinese keyword capture with personal/project routing, `config` /
|
|
303
362
|
`capture --dry-run` / `doctor` helpers, Visual Studio support.
|
|
304
363
|
|
|
305
|
-
**
|
|
306
|
-
`
|
|
364
|
+
**In progress — `0.4.0`:** team sync — shared memory via git: appdata draft
|
|
365
|
+
outbox → `sync-status` → `submit` (local branch+commit, push/PR on your Yes)
|
|
366
|
+
→ `promote` / `resolve` review workflow, in-repo `.ai/open-memex/` dir, 1–2
|
|
367
|
+
colleague pilot.
|
|
307
368
|
|
|
308
369
|
**Coming — `0.3.0` (stable):** org layer — org memory repo, curator convention,
|
|
309
370
|
distill-to-AGENTS.md assist.
|
package/README.zh-CN.md
CHANGED
|
@@ -149,7 +149,7 @@ open-memex doctor
|
|
|
149
149
|
|
|
150
150
|
检查:Node 版本、配置来源、当前目录的 scope 解析、存储可写性,
|
|
151
151
|
然后启动一个真实的 MCP server 做 `initialize` + `tools/list`——
|
|
152
|
-
|
|
152
|
+
十一个 tools 都必须出现。
|
|
153
153
|
|
|
154
154
|
## Agent 可用的 tools
|
|
155
155
|
|
|
@@ -160,6 +160,12 @@ open-memex doctor
|
|
|
160
160
|
| `memory_list` | 按 scope 列出记忆,最新的在前 |
|
|
161
161
|
| `memory_supersede` | 用新版本替换一条记忆(保留替换链) |
|
|
162
162
|
| `memory_forget` | 按 id 删除一条记忆 |
|
|
163
|
+
| `memory_status` | 显示同步队列:outbox 草稿、repo 评审状态、未提交文件 |
|
|
164
|
+
| `memory_submit` | 把点名的草稿移入 repo memory 目录(建本地分支 + commit) |
|
|
165
|
+
| `memory_propose` | 把 personal 记忆复制到 project scope 作为评审候选 |
|
|
166
|
+
| `memory_promote` | 推进 `proposed → approved → published`(或 reject / resubmit) |
|
|
167
|
+
| `memory_resolve` | 列出冲突的记忆文件 / 对单个做三路合并 |
|
|
168
|
+
| `memory_pr_status` | 把分支 PR 的 GitHub 状态映射到每条记忆的评审状态 |
|
|
163
169
|
|
|
164
170
|
## 捕获(Capture)
|
|
165
171
|
|
|
@@ -220,10 +226,16 @@ Markdown 是 source of truth,SQLite 索引是派生的、可重建的
|
|
|
220
226
|
"maxProfileItems": 5, // 首轮注入的个人偏好条数
|
|
221
227
|
"injectOnFirstTurn": true, // [OPEN-MEMEX] system-prompt 块
|
|
222
228
|
"keywordCaptureEnabled": true,
|
|
223
|
-
"logLevel": "info" // info | debug
|
|
229
|
+
"logLevel": "info", // info | debug
|
|
230
|
+
"memoryDir": ".ai/open-memex" // 仓库内项目记忆目录,相对于仓库根目录
|
|
224
231
|
}
|
|
225
232
|
```
|
|
226
233
|
|
|
234
|
+
project scope 的记忆以"一个记忆一个 Markdown 文件"的形式存放在
|
|
235
|
+
`<仓库>/<memoryDir>/`(默认 `.ai/open-memex/`)下,可经 git 共享;
|
|
236
|
+
personal 记忆只存本地 appdata,永不离开本机。已有的 appdata 项目文件会在
|
|
237
|
+
首次写入/同步时自动搬进仓库目录。
|
|
238
|
+
|
|
227
239
|
`open-memex config` 打印生效配置(默认值 + 文件)。
|
|
228
240
|
安装后改设置:
|
|
229
241
|
|
|
@@ -233,7 +245,7 @@ open-memex config set maxProjectMemories 12
|
|
|
233
245
|
```
|
|
234
246
|
|
|
235
247
|
可设置的 key:`maxProjectMemories`、`maxProfileItems`、`injectOnFirstTurn`、
|
|
236
|
-
`keywordCaptureEnabled`、`logLevel`。完整设计见
|
|
248
|
+
`keywordCaptureEnabled`、`logLevel`、`memoryDir`。完整设计见
|
|
237
249
|
[docs/V2-DESIGN.md](./docs/V2-DESIGN.md)。
|
|
238
250
|
|
|
239
251
|
## CLI 参考
|
|
@@ -262,6 +274,45 @@ open-memex status <id> deprecated
|
|
|
262
274
|
open-memex forget <id>
|
|
263
275
|
```
|
|
264
276
|
|
|
277
|
+
团队评审工作流(Phase 2B —— 两个家,各管一段):
|
|
278
|
+
|
|
279
|
+
project 草稿先住在 **appdata outbox**(git 看不见、跟分支无关);只有你
|
|
280
|
+
点名批准的草稿,才会被移入 `<repo>/.ai/open-memex/`,之后随分支和 PR 走。
|
|
281
|
+
没经过你点名,什么都不会动。
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
open-memex sync-status
|
|
285
|
+
# 看索引上次同步的时间和触发方、outbox(待同步)、repo 里的评审状态
|
|
286
|
+
# (draft / proposed / approved / published / rejected),
|
|
287
|
+
# 以及 repo 里还没 commit 的记忆文件。
|
|
288
|
+
|
|
289
|
+
open-memex submit <id...> [--onto <branch>] [--base <branch>]
|
|
290
|
+
# 把你点名的草稿移入 .ai/open-memex/,状态变为 proposed:
|
|
291
|
+
# 建 mem/sync-<timestamp> 分支(或 --onto 当前分支,跟代码走同一个 PR),
|
|
292
|
+
# 复制、改 review_state、本地 git commit。全有或全无;冲突(同 id 不同内容)
|
|
293
|
+
# 干净回滚。打印 push + gh pr 命令;Agent 拿到你的 Yes 后会自己走完 push/PR。
|
|
294
|
+
# PR 默认 base 是当前分支;--base 可改到 main 或集成支。
|
|
295
|
+
|
|
296
|
+
open-memex pr-status [--apply]
|
|
297
|
+
# 读分支的 GitHub PR,把它的状态映射到每条 in-repo 记忆:
|
|
298
|
+
# PR merged → published,PR approved → approved(approved_by = reviewer),
|
|
299
|
+
# changes requested 只给建议。默认只报告;--apply 在本地执行映射的流转(不 push)。
|
|
300
|
+
|
|
301
|
+
open-memex propose <id...> --to project [--local-approve]
|
|
302
|
+
# 一次 propose 一条或多条(一个分支、一个 PR),每条独立新 id。
|
|
303
|
+
# 全有或全无:id 有错整批回滚,不会留半截。
|
|
304
|
+
# 把一条 personal 记忆复制到 project scope 进入评审(复制而非移动,
|
|
305
|
+
# personal 原件保留)。结果落在 outbox;准备好进 repo 时再 sync-status / submit。
|
|
306
|
+
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
307
|
+
# 晋升一步:proposed → approved → published(或用 --reject 驳回并附注原因)。
|
|
308
|
+
# 每次流转都追加到记忆的 review_history(谁、何时、为什么)。
|
|
309
|
+
# 驳回不删文件,由你决定:接受(关 PR 删分支)、改完 --resubmit 再审、
|
|
310
|
+
# 或留着当 [rejected] 记录。
|
|
311
|
+
open-memex resolve [id-or-path]
|
|
312
|
+
# 列出冲突中的记忆文件,或对其中一个做字段级 3-way 合并。
|
|
313
|
+
# 语义冲突只报告、不自动解决。
|
|
314
|
+
```
|
|
315
|
+
|
|
265
316
|
维护:
|
|
266
317
|
|
|
267
318
|
```sh
|
|
@@ -280,7 +331,7 @@ CLI 跑在 Node 22 下。从源码 checkout 使用时走内置的实验性 TypeS
|
|
|
280
331
|
|
|
281
332
|
## MCP server
|
|
282
333
|
|
|
283
|
-
|
|
334
|
+
同一个十一个 memory tools,走 Model Context Protocol 的 stdio server——
|
|
284
335
|
不需要宿主专属插件,任何 MCP 客户端都能用 open-memex。
|
|
285
336
|
|
|
286
337
|
```sh
|
|
@@ -301,8 +352,9 @@ project scope 从进程工作目录解析,所以配置 server 时 cwd 要指
|
|
|
301
352
|
一键 `init` 配置、中文关键词捕获(含 personal/project 路由)、
|
|
302
353
|
`config` / `capture --dry-run` / `doctor` 助手命令、Visual Studio 支持。
|
|
303
354
|
|
|
304
|
-
|
|
305
|
-
|
|
355
|
+
**进行中 —— `0.4.0`:** 团队同步——用 git 做共享记忆:appdata 草稿箱 →
|
|
356
|
+
`sync-status` → `submit`(本地分支+commit,push/PR 拿你的 Yes 才做)
|
|
357
|
+
→ `promote` / `resolve` 评审工作流、仓库内 `.ai/open-memex/` 目录,
|
|
306
358
|
找 1–2 个同事做 pilot。
|
|
307
359
|
|
|
308
360
|
**Coming —— `0.3.0`(稳定版):** 组织层——组织记忆仓库、
|
package/dist/cli.js
CHANGED
|
@@ -6,8 +6,11 @@ import { syncScope, upsertFromFile, deleteFromIndex } from "./store/sync.js";
|
|
|
6
6
|
import { migrateScope, scopeHasFiles } from "./store/migrate.js";
|
|
7
7
|
import { migrateV2 } from "./store/v2migrate.js";
|
|
8
8
|
import { findDuplicates, supersede, setStatus } from "./store/lifecycle.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { proposeMemories, promoteMemory, listConflicts, resolveConflict, formatReviewHistory } from "./review.js";
|
|
10
|
+
import { getSyncStatus, formatSyncStatus, submitMemories } from "./submit.js";
|
|
11
|
+
import { getPrStatus, formatPrStatus, applyPrStatus } from "./github.js";
|
|
12
|
+
import { search, list, hitStateLabel } from "./retrieve/search.js";
|
|
13
|
+
import { writeMemoryFile, readMemoryFile, ulid, msToRfc3339, } from "./store/markdown.js";
|
|
11
14
|
import { loadConfig } from "./config.js";
|
|
12
15
|
import { paths } from "./paths.js";
|
|
13
16
|
import { redact } from "./redact.js";
|
|
@@ -26,6 +29,12 @@ Usage:
|
|
|
26
29
|
open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
27
30
|
open-memex status <id> active|deprecated|retracted|archived
|
|
28
31
|
open-memex forget <id>
|
|
32
|
+
open-memex propose <id...> --to project [--local-approve]
|
|
33
|
+
open-memex promote <id> [--reject] [--resubmit] [--note "..."] [--by NAME]
|
|
34
|
+
open-memex resolve [id-or-path]
|
|
35
|
+
open-memex sync-status
|
|
36
|
+
open-memex submit <id...> [--onto <branch>] [--base <branch>]
|
|
37
|
+
open-memex pr-status [--apply]
|
|
29
38
|
open-memex reindex
|
|
30
39
|
open-memex scopes
|
|
31
40
|
open-memex migrate [--from <key>] [--to <key>]
|
|
@@ -294,15 +303,15 @@ async function main() {
|
|
|
294
303
|
return;
|
|
295
304
|
}
|
|
296
305
|
if (cmd === "reindex") {
|
|
297
|
-
const a = syncScope(project.key);
|
|
298
|
-
const b = syncScope(PERSONAL_SCOPE.key);
|
|
306
|
+
const a = syncScope(project.key, "cli");
|
|
307
|
+
const b = syncScope(PERSONAL_SCOPE.key, "cli");
|
|
299
308
|
console.log(`reindexed. project: +${a.added} ~${a.updated} -${a.removed} (scanned ${a.scanned}), personal: +${b.added} ~${b.updated} -${b.removed} (scanned ${b.scanned})`);
|
|
300
309
|
return;
|
|
301
310
|
}
|
|
302
311
|
if (cmd === "list") {
|
|
303
312
|
const flags = parseFlags(rest);
|
|
304
313
|
const s = resolveCliScope(flags, project);
|
|
305
|
-
syncScope(s.key);
|
|
314
|
+
syncScope(s.key, "cli");
|
|
306
315
|
const hits = list(s.key, {
|
|
307
316
|
type: flags.type,
|
|
308
317
|
limit: flags.limit ? Number(flags.limit) : undefined,
|
|
@@ -313,7 +322,8 @@ async function main() {
|
|
|
313
322
|
}
|
|
314
323
|
for (const h of hits) {
|
|
315
324
|
const dep = h.status === "deprecated" ? " [deprecated]" : "";
|
|
316
|
-
|
|
325
|
+
const rev = hitStateLabel(h);
|
|
326
|
+
console.log(`[${h.type}]${dep}${rev} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
317
327
|
}
|
|
318
328
|
return;
|
|
319
329
|
}
|
|
@@ -322,8 +332,8 @@ async function main() {
|
|
|
322
332
|
if (!query || query.startsWith("--"))
|
|
323
333
|
usage();
|
|
324
334
|
const flags = parseFlags(rest.slice(1));
|
|
325
|
-
syncScope(project.key);
|
|
326
|
-
syncScope(PERSONAL_SCOPE.key);
|
|
335
|
+
syncScope(project.key, "cli");
|
|
336
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
327
337
|
const keys = flags.scope === "personal" || flags.scope === "user"
|
|
328
338
|
? [PERSONAL_SCOPE.key]
|
|
329
339
|
: flags.scope === "project"
|
|
@@ -341,7 +351,8 @@ async function main() {
|
|
|
341
351
|
for (const h of hits) {
|
|
342
352
|
const tag = h.scope_key === PERSONAL_SCOPE.key ? "personal" : "project";
|
|
343
353
|
const dep = h.status === "deprecated" ? " [deprecated]" : "";
|
|
344
|
-
|
|
354
|
+
const rev = hitStateLabel(h);
|
|
355
|
+
console.log(`[${tag}/${h.type}]${dep}${rev} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
345
356
|
}
|
|
346
357
|
return;
|
|
347
358
|
}
|
|
@@ -384,6 +395,12 @@ async function main() {
|
|
|
384
395
|
updated_at: rfc,
|
|
385
396
|
supersedes: null,
|
|
386
397
|
superseded_by: null,
|
|
398
|
+
review_state: "draft",
|
|
399
|
+
proposed_by: null,
|
|
400
|
+
approved_by: null,
|
|
401
|
+
derived_from: null,
|
|
402
|
+
review_note: null,
|
|
403
|
+
review_history: [],
|
|
387
404
|
};
|
|
388
405
|
const { filePath } = writeMemoryFile(fm, red);
|
|
389
406
|
const mf = readMemoryFile(filePath);
|
|
@@ -447,16 +464,202 @@ async function main() {
|
|
|
447
464
|
const id = rest[0];
|
|
448
465
|
if (!id)
|
|
449
466
|
usage();
|
|
450
|
-
const row = db().prepare(`SELECT
|
|
467
|
+
const row = db().prepare(`SELECT file_path FROM memories WHERE id = ?`).get(id);
|
|
451
468
|
if (!row) {
|
|
452
469
|
console.error("not found");
|
|
453
470
|
process.exit(1);
|
|
454
471
|
}
|
|
455
|
-
|
|
472
|
+
// Delete by indexed file_path — location-agnostic (appdata or in-repo, 2B/D24).
|
|
473
|
+
if (row.file_path) {
|
|
474
|
+
try {
|
|
475
|
+
fs.unlinkSync(row.file_path);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
/* already gone */
|
|
479
|
+
}
|
|
480
|
+
}
|
|
456
481
|
deleteFromIndex(id);
|
|
457
482
|
console.log(`deleted ${id}`);
|
|
458
483
|
return;
|
|
459
484
|
}
|
|
485
|
+
/** Positional args with flags (and their values) skipped. */
|
|
486
|
+
function positionalArgs(argv) {
|
|
487
|
+
const out = [];
|
|
488
|
+
for (let i = 0; i < argv.length; i++) {
|
|
489
|
+
const a = argv[i];
|
|
490
|
+
if (a.startsWith("--")) {
|
|
491
|
+
const val = argv[i + 1];
|
|
492
|
+
if (val !== undefined && !val.startsWith("--"))
|
|
493
|
+
i++; // skip the flag's value
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
out.push(a);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
if (cmd === "propose") {
|
|
502
|
+
const flags = parseFlags(rest);
|
|
503
|
+
const ids = positionalArgs(rest);
|
|
504
|
+
if (ids.length === 0)
|
|
505
|
+
usage();
|
|
506
|
+
const to = flags.to ?? "project";
|
|
507
|
+
if (to !== "project") {
|
|
508
|
+
console.error(`propose --to "${to}" is not supported yet (org sharing is Phase 4).`);
|
|
509
|
+
process.exit(2);
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const batch = proposeMemories(ids, { localApprove: flags["local-approve"] === "true" });
|
|
513
|
+
for (const { sourceId, result: r } of batch) {
|
|
514
|
+
console.log(`proposed ${sourceId} → ${r.id} [${r.reviewState}]`);
|
|
515
|
+
}
|
|
516
|
+
console.log(`Next: these are drafts in the outbox (appdata) — nothing is in git yet.`);
|
|
517
|
+
console.log(` open-memex sync-status`);
|
|
518
|
+
console.log(` open-memex submit ${batch.map(({ result: r }) => r.id).join(" ")} # new branch, local commit; prints push + PR commands`);
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
console.error(`propose failed: ${e.message}`);
|
|
522
|
+
process.exit(2);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (cmd === "promote") {
|
|
527
|
+
const flags = parseFlags(rest);
|
|
528
|
+
const id = rest.find((a) => !a.startsWith("--"));
|
|
529
|
+
if (!id)
|
|
530
|
+
usage();
|
|
531
|
+
try {
|
|
532
|
+
const r = promoteMemory(id, {
|
|
533
|
+
reject: flags.reject === "true",
|
|
534
|
+
resubmit: flags.resubmit === "true",
|
|
535
|
+
note: flags.note,
|
|
536
|
+
by: flags.by,
|
|
537
|
+
});
|
|
538
|
+
console.log(`promote ${r.id}: ${r.from} → ${r.to}`);
|
|
539
|
+
if (r.to === "published") {
|
|
540
|
+
console.log(` merged to the shared branch? Teammates pick it up with an explicit pull.`);
|
|
541
|
+
}
|
|
542
|
+
if (r.to === "rejected") {
|
|
543
|
+
console.log(` not deleted — the file stays on your branch. Your call:`);
|
|
544
|
+
console.log(` accept: close the PR and delete the branch;`);
|
|
545
|
+
console.log(` revise: edit the file, then \`open-memex promote ${r.id} --resubmit\`;`);
|
|
546
|
+
console.log(` keep: leave it as a [rejected] record.`);
|
|
547
|
+
}
|
|
548
|
+
if (r.history.length > 0) {
|
|
549
|
+
console.log(` history (${r.history.length}):`);
|
|
550
|
+
for (const h of formatReviewHistory(r.history))
|
|
551
|
+
console.log(h);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (e) {
|
|
555
|
+
console.error(`promote failed: ${e.message}`);
|
|
556
|
+
process.exit(2);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (cmd === "resolve") {
|
|
561
|
+
const target = rest.find((a) => !a.startsWith("--"));
|
|
562
|
+
try {
|
|
563
|
+
if (!target) {
|
|
564
|
+
const conflicts = listConflicts();
|
|
565
|
+
if (conflicts.length === 0) {
|
|
566
|
+
console.log("(no conflicted memory files)");
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
for (const c of conflicts)
|
|
570
|
+
console.log(`conflicted: ${c.id} ${c.filePath}`);
|
|
571
|
+
console.log(`\nRun \`open-memex resolve <id>\` to attempt a field-level 3-way merge.`);
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const outcome = resolveConflict(target);
|
|
576
|
+
if (!outcome.ok) {
|
|
577
|
+
console.error(`cannot auto-resolve ${path.basename(outcome.filePath)} — semantic conflicts need a human:`);
|
|
578
|
+
for (const c of outcome.conflicts) {
|
|
579
|
+
console.error(` ${c.field}:`);
|
|
580
|
+
console.error(` base: ${c.base}`);
|
|
581
|
+
console.error(` ours: ${c.ours}`);
|
|
582
|
+
console.error(` theirs: ${c.theirs}`);
|
|
583
|
+
}
|
|
584
|
+
console.error(`Edit the file manually, then \`git add\` it. Nothing was written.`);
|
|
585
|
+
process.exit(3);
|
|
586
|
+
}
|
|
587
|
+
console.log(`resolved ${path.basename(outcome.filePath)}`);
|
|
588
|
+
if (outcome.autoMerged.length > 0)
|
|
589
|
+
console.log(` auto-merged: ${outcome.autoMerged.join(", ")}`);
|
|
590
|
+
console.log(` next: git add ${path.relative(process.cwd(), outcome.filePath)}`);
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
console.error(`resolve failed: ${e.message}`);
|
|
594
|
+
process.exit(2);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (cmd === "sync-status") {
|
|
599
|
+
syncScope(project.key, "cli");
|
|
600
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
601
|
+
try {
|
|
602
|
+
console.log(formatSyncStatus(getSyncStatus()));
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
console.error(`sync-status failed: ${e.message}`);
|
|
606
|
+
process.exit(2);
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (cmd === "pr-status") {
|
|
611
|
+
const flags = parseFlags(rest);
|
|
612
|
+
try {
|
|
613
|
+
const st = getPrStatus();
|
|
614
|
+
console.log(formatPrStatus(st));
|
|
615
|
+
if (flags.apply === "true") {
|
|
616
|
+
const applied = applyPrStatus(st);
|
|
617
|
+
if (applied.length === 0) {
|
|
618
|
+
console.log(`nothing to apply.`);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
for (const a of applied) {
|
|
622
|
+
console.log(`applied ${a.memoryId.slice(0, 8)}: ${a.from} → ${a.to} (by ${a.by})`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
console.error(`pr-status failed: ${e.message}`);
|
|
629
|
+
process.exit(2);
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (cmd === "submit") {
|
|
634
|
+
const flags = parseFlags(rest);
|
|
635
|
+
const ids = positionalArgs(rest);
|
|
636
|
+
if (ids.length === 0)
|
|
637
|
+
usage();
|
|
638
|
+
syncScope(project.key, "submit");
|
|
639
|
+
try {
|
|
640
|
+
const r = submitMemories(ids, { onto: flags.onto, base: flags.base });
|
|
641
|
+
for (const s of r.submitted) {
|
|
642
|
+
console.log(`submitted ${s.id} → ${path.relative(process.cwd(), s.filePath)} [${s.reviewState}]`);
|
|
643
|
+
}
|
|
644
|
+
for (const id of r.skippedIdentical) {
|
|
645
|
+
console.log(`already on branch: ${id} (identical content — outbox copy removed)`);
|
|
646
|
+
}
|
|
647
|
+
if (!r.committed) {
|
|
648
|
+
console.log(`nothing new to commit — branch ${r.branch} already holds these memories.`);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
console.log(`committed on ${r.branch}.`);
|
|
652
|
+
}
|
|
653
|
+
console.log(`Next (needs your approval — open-memex never pushes for you):`);
|
|
654
|
+
console.log(` ${r.pushCommand}`);
|
|
655
|
+
console.log(` ${r.prCommand}`);
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
console.error(`submit failed: ${e.message}`);
|
|
659
|
+
process.exit(2);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
460
663
|
if (cmd === "scopes") {
|
|
461
664
|
const { memories } = paths();
|
|
462
665
|
if (!fs.existsSync(memories)) {
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,7 @@ export const DEFAULT_CONFIG = {
|
|
|
33
33
|
// Add only your own extra patterns here.
|
|
34
34
|
redactPatterns: [],
|
|
35
35
|
logLevel: "info",
|
|
36
|
+
memoryDir: ".ai/open-memex",
|
|
36
37
|
};
|
|
37
38
|
function stripJsonComments(raw) {
|
|
38
39
|
// Line comments
|
|
@@ -63,12 +64,21 @@ function toNonNegInt(v) {
|
|
|
63
64
|
throw new Error("must be a non-negative integer");
|
|
64
65
|
return n;
|
|
65
66
|
}
|
|
67
|
+
function toRelativeDir(v) {
|
|
68
|
+
if (typeof v !== "string" || !v.trim())
|
|
69
|
+
throw new Error("must be a non-empty relative directory");
|
|
70
|
+
const t = v.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
71
|
+
if (!t || path.isAbsolute(t) || t.split("/").includes(".."))
|
|
72
|
+
throw new Error('must be a relative path without ".." (e.g. ".ai/open-memex")');
|
|
73
|
+
return t;
|
|
74
|
+
}
|
|
66
75
|
/** Keys users may change via `open-memex config set <key> <value>`, with validators. */
|
|
67
76
|
export const SETTABLE_KEYS = {
|
|
68
77
|
maxProjectMemories: toNonNegInt,
|
|
69
78
|
maxProfileItems: toNonNegInt,
|
|
70
79
|
injectOnFirstTurn: toBool,
|
|
71
80
|
keywordCaptureEnabled: toBool,
|
|
81
|
+
memoryDir: toRelativeDir,
|
|
72
82
|
logLevel: (v) => {
|
|
73
83
|
if (v !== "info" && v !== "debug")
|
|
74
84
|
throw new Error('must be "info" or "debug"');
|