open-memex 0.3.0 → 0.4.0-alpha.10
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 +20 -1
- package/README.md +117 -9
- package/README.zh-CN.md +103 -9
- package/dist/cli.js +583 -14
- package/dist/config.js +12 -0
- package/dist/distill-agents.js +55 -0
- package/dist/doctor.js +1 -1
- package/dist/export.js +157 -0
- package/dist/github.js +215 -0
- package/dist/index.js +6 -0
- package/dist/init.js +43 -18
- package/dist/mcp.js +119 -12
- package/dist/paths.js +31 -0
- package/dist/providers/git.js +142 -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 +57 -14
- package/dist/store/sync.js +74 -6
- package/dist/submit.js +347 -0
- package/dist/tools/ops.js +171 -8
- package/docs/CURATOR.md +59 -0
- package/docs/USER-GUIDE.md +197 -0
- package/docs/USER-GUIDE.zh-CN.md +161 -0
- package/docs/V2-DESIGN.md +287 -13
- package/package.json +1 -1
- package/scripts/smoke-mcp.ts +2 -2
- package/src/cli.ts +608 -15
- package/src/config.ts +26 -0
- package/src/distill-agents.ts +70 -0
- package/src/doctor.ts +1 -1
- package/src/export.ts +206 -0
- package/src/github.ts +257 -0
- package/src/index.ts +6 -0
- package/src/init.ts +43 -17
- package/src/mcp.ts +166 -11
- package/src/paths.ts +32 -0
- package/src/providers/git.ts +191 -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 +91 -14
- package/src/store/sync.ts +85 -4
- package/src/submit.ts +424 -0
- package/src/tools/ops.ts +203 -9
package/dist/mcp.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* open-memex generic MCP server (stdio transport).
|
|
3
3
|
*
|
|
4
|
-
* Exposes the
|
|
4
|
+
* Exposes the memory tools as the opencode plugin
|
|
5
5
|
* (memory_add / memory_search / memory_list / memory_supersede /
|
|
6
|
-
* memory_forget
|
|
6
|
+
* memory_forget, plus memory_status / memory_submit / memory_propose /
|
|
7
|
+
* memory_promote / memory_resolve / memory_pr_status) over the Model Context Protocol, so any MCP client —
|
|
7
8
|
* VS Code Copilot Chat, Cursor, Claude Code, etc. — can use open-memex
|
|
8
9
|
* without a host-specific plugin.
|
|
9
10
|
*
|
|
@@ -19,13 +20,69 @@
|
|
|
19
20
|
*/
|
|
20
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
22
26
|
import { z } from "zod";
|
|
23
27
|
import { loadConfig } from "./config.js";
|
|
24
28
|
import { resolveProjectScope, PERSONAL_SCOPE } from "./scope.js";
|
|
25
29
|
import { db } from "./store/db.js";
|
|
26
30
|
import { syncScope } from "./store/sync.js";
|
|
27
|
-
import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, TOOL_DESCRIPTIONS, } from "./tools/ops.js";
|
|
28
|
-
|
|
31
|
+
import { addMemory, searchMemories, listMemories, supersedeMemory, forgetMemory, statusMemories, submitMemoriesOp, proposeMemoriesOp, promoteMemoryOp, resolveMemoryOp, prStatusOp, memoryAddArgs, memorySearchArgs, memoryListArgs, memorySupersedeArgs, memoryForgetArgs, memoryStatusArgs, memorySubmitArgs, memoryProposeArgs, memoryPromoteArgs, memoryResolveArgs, memoryPrStatusArgs, TOOL_DESCRIPTIONS, } from "./tools/ops.js";
|
|
32
|
+
// Server version tracks package.json — never hardcode it here again.
|
|
33
|
+
// package.json sits two levels above this file in both layouts
|
|
34
|
+
// (src/mcp.ts and dist/mcp.js), same convention as cli.ts --version.
|
|
35
|
+
const SERVER_VERSION = (() => {
|
|
36
|
+
try {
|
|
37
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
38
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
39
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0-unknown";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return "0.0.0-unknown";
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
/**
|
|
46
|
+
* D26: session-start guidance delivered through the MCP handshake itself.
|
|
47
|
+
* The init-written instruction files only exist if the user ran
|
|
48
|
+
* `open-memex init --client`; the initialize `instructions` reach every MCP
|
|
49
|
+
* client at connect time. Still advisory — no MCP consumer offers a hard
|
|
50
|
+
* session-start hook — but it is the strongest signal available.
|
|
51
|
+
*/
|
|
52
|
+
const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server
|
|
53
|
+
(eleven memory_* tools: add, search, list, supersede, forget, status, submit,
|
|
54
|
+
propose, promote, resolve, pr_status).
|
|
55
|
+
|
|
56
|
+
- At the START of this session, when you finish a meaningful chunk of work,
|
|
57
|
+
after the user commits (git commit), and after any memory_* action completes,
|
|
58
|
+
call memory_status. If the project outbox has drafts waiting for review,
|
|
59
|
+
summarize them (one line each) and ask the user which ones to sync into the
|
|
60
|
+
repo. Sync NOTHING the user did not name.
|
|
61
|
+
- When the user says "sync memory" (or "同步记忆"), treat it as a request to run
|
|
62
|
+
the sync flow: call memory_status, summarize the outbox drafts, and ask which
|
|
63
|
+
ones to sync. ALWAYS use the memory_status tool for this — never browse the
|
|
64
|
+
appdata directory directly.
|
|
65
|
+
- After memory_submit, ask ONE follow-up: "want me to create a branch + push +
|
|
66
|
+
open the PR, or will you handle it yourself?" NEVER create branches, push, or
|
|
67
|
+
open PRs without the user's explicit approval. A "yes, you do it" covers the
|
|
68
|
+
whole chain — do NOT re-ask at each step.
|
|
69
|
+
- BE PROACTIVE: when the user shares something worth remembering across sessions
|
|
70
|
+
(a decision, a preference, a project convention, a fix and its cause), call
|
|
71
|
+
memory_add without being asked. Keep each memory to one self-contained statement.
|
|
72
|
+
- At the same checkpoints (session start, end of a work chunk, after the user
|
|
73
|
+
commits, after any memory_* action), DISTILL the session: propose 1–3 short
|
|
74
|
+
memories capturing the useful conclusion — what was learned or decided, how an
|
|
75
|
+
issue was resolved, what to avoid, where the authoritative doc lives — not the
|
|
76
|
+
raw transcript. Save NOTHING the user did not approve; on approval call
|
|
77
|
+
memory_add with source "inference" at the confirmed scope. If the knowledge
|
|
78
|
+
already lives in project docs, save a \`reference\` memory pointing at the doc
|
|
79
|
+
instead of copying it. Long-form notes are fine ONLY when the user explicitly
|
|
80
|
+
asks to save one.
|
|
81
|
+
- Before asking the user about past decisions, conventions, or preferences they
|
|
82
|
+
may have told you before, call memory_search first.
|
|
83
|
+
- Memories default to this project's scope; use the personal scope for facts
|
|
84
|
+
about the user that hold across all projects.
|
|
85
|
+
- personal scope memories NEVER leave this machine.`;
|
|
29
86
|
/** Adapt a framework-agnostic op result to an MCP tool response. */
|
|
30
87
|
function toMcp(p) {
|
|
31
88
|
return p.then((r) => ({ content: [{ type: "text", text: r.output }] }), (e) => ({
|
|
@@ -44,33 +101,83 @@ export async function runMcpServer() {
|
|
|
44
101
|
const getScope = () => scope;
|
|
45
102
|
// Init DB and one-shot sync of markdown -> index, mirroring the plugin.
|
|
46
103
|
db();
|
|
47
|
-
syncScope(scope.key);
|
|
48
|
-
syncScope(PERSONAL_SCOPE.key);
|
|
104
|
+
syncScope(scope.key, "session");
|
|
105
|
+
syncScope(PERSONAL_SCOPE.key, "session");
|
|
49
106
|
console.error(`[open-memex] MCP server up. scope=${scope.key}`);
|
|
50
|
-
|
|
107
|
+
// D12: pulls are explicit by default — session start never touches the
|
|
108
|
+
// network. With sync.autoPull, one best-effort pull; a failure never
|
|
109
|
+
// blocks the session, it just logs and continues.
|
|
110
|
+
if (cfg.sync?.autoPull) {
|
|
111
|
+
try {
|
|
112
|
+
const { GitProvider } = await import("./providers/git.js");
|
|
113
|
+
const r = new GitProvider().pull(process.cwd());
|
|
114
|
+
syncScope(scope.key, "pull");
|
|
115
|
+
console.error(`[open-memex] auto-pull: ${r.branch} ${r.fastForwarded ? "fast-forwarded" : "already up to date"}`);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.error(`[open-memex] auto-pull skipped: ${e.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// D26: re-sync on every request, not just at startup. The in-repo dir
|
|
122
|
+
// follows the current git branch, so a branch switch mid-session would
|
|
123
|
+
// otherwise leave the index pointing at files that no longer exist.
|
|
124
|
+
const withSync = (fn) => {
|
|
125
|
+
return (args) => {
|
|
126
|
+
syncScope(scope.key, "request");
|
|
127
|
+
syncScope(PERSONAL_SCOPE.key, "request");
|
|
128
|
+
return fn(args);
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const server = new McpServer({ name: "open-memex", version: SERVER_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
51
132
|
server.registerTool("memory_add", {
|
|
52
133
|
description: TOOL_DESCRIPTIONS.memory_add,
|
|
53
134
|
inputSchema: z.object(memoryAddArgs),
|
|
54
|
-
}, (args) => toMcp(addMemory(getScope, cfg, args)));
|
|
135
|
+
}, withSync((args) => toMcp(addMemory(getScope, cfg, args))));
|
|
55
136
|
server.registerTool("memory_search", {
|
|
56
137
|
description: TOOL_DESCRIPTIONS.memory_search,
|
|
57
138
|
inputSchema: z.object(memorySearchArgs),
|
|
58
139
|
annotations: { readOnlyHint: true },
|
|
59
|
-
}, (args) => toMcp(searchMemories(getScope, args)));
|
|
140
|
+
}, withSync((args) => toMcp(searchMemories(getScope, args))));
|
|
60
141
|
server.registerTool("memory_list", {
|
|
61
142
|
description: TOOL_DESCRIPTIONS.memory_list,
|
|
62
143
|
inputSchema: z.object(memoryListArgs),
|
|
63
144
|
annotations: { readOnlyHint: true },
|
|
64
|
-
}, (args) => toMcp(listMemories(getScope, args)));
|
|
145
|
+
}, withSync((args) => toMcp(listMemories(getScope, args))));
|
|
65
146
|
server.registerTool("memory_supersede", {
|
|
66
147
|
description: TOOL_DESCRIPTIONS.memory_supersede,
|
|
67
148
|
inputSchema: z.object(memorySupersedeArgs),
|
|
68
|
-
}, (args) => toMcp(supersedeMemory(cfg, args)));
|
|
149
|
+
}, withSync((args) => toMcp(supersedeMemory(cfg, args))));
|
|
69
150
|
server.registerTool("memory_forget", {
|
|
70
151
|
description: TOOL_DESCRIPTIONS.memory_forget,
|
|
71
152
|
inputSchema: z.object(memoryForgetArgs),
|
|
72
153
|
annotations: { destructiveHint: true },
|
|
73
|
-
}, (args) => toMcp(forgetMemory(args)));
|
|
154
|
+
}, withSync((args) => toMcp(forgetMemory(args))));
|
|
155
|
+
server.registerTool("memory_status", {
|
|
156
|
+
description: TOOL_DESCRIPTIONS.memory_status,
|
|
157
|
+
inputSchema: z.object(memoryStatusArgs),
|
|
158
|
+
annotations: { readOnlyHint: true },
|
|
159
|
+
}, withSync((_args) => toMcp(statusMemories())));
|
|
160
|
+
server.registerTool("memory_submit", {
|
|
161
|
+
description: TOOL_DESCRIPTIONS.memory_submit,
|
|
162
|
+
inputSchema: z.object(memorySubmitArgs),
|
|
163
|
+
}, withSync((args) => toMcp(submitMemoriesOp(args))));
|
|
164
|
+
server.registerTool("memory_propose", {
|
|
165
|
+
description: TOOL_DESCRIPTIONS.memory_propose,
|
|
166
|
+
inputSchema: z.object(memoryProposeArgs),
|
|
167
|
+
}, withSync((args) => toMcp(proposeMemoriesOp(args))));
|
|
168
|
+
server.registerTool("memory_promote", {
|
|
169
|
+
description: TOOL_DESCRIPTIONS.memory_promote,
|
|
170
|
+
inputSchema: z.object(memoryPromoteArgs),
|
|
171
|
+
}, withSync((args) => toMcp(promoteMemoryOp(args))));
|
|
172
|
+
server.registerTool("memory_resolve", {
|
|
173
|
+
description: TOOL_DESCRIPTIONS.memory_resolve,
|
|
174
|
+
inputSchema: z.object(memoryResolveArgs),
|
|
175
|
+
}, withSync((args) => toMcp(resolveMemoryOp(args))));
|
|
176
|
+
server.registerTool("memory_pr_status", {
|
|
177
|
+
description: TOOL_DESCRIPTIONS.memory_pr_status,
|
|
178
|
+
inputSchema: z.object(memoryPrStatusArgs),
|
|
179
|
+
annotations: { readOnlyHint: true },
|
|
180
|
+
}, withSync((args) => toMcp(prStatusOp(args))));
|
|
74
181
|
const transport = new StdioServerTransport();
|
|
75
182
|
await server.connect(transport);
|
|
76
183
|
}
|
package/dist/paths.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
function dataRoot() {
|
|
5
6
|
if (process.env.MY_O_MEMORY_HOME)
|
|
6
7
|
return path.resolve(process.env.MY_O_MEMORY_HOME);
|
|
@@ -34,3 +35,33 @@ export function memoriesDirPath(scopeKey) {
|
|
|
34
35
|
const { memories } = paths();
|
|
35
36
|
return path.join(memories, scopeKey);
|
|
36
37
|
}
|
|
38
|
+
/** Project root: git top-level, falling back to cwd (2B: in-repo memory dir anchor). */
|
|
39
|
+
export function projectRoot(cwd = process.cwd()) {
|
|
40
|
+
try {
|
|
41
|
+
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
45
|
+
}).trim();
|
|
46
|
+
if (top)
|
|
47
|
+
return top;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* not a git repo — use cwd */
|
|
51
|
+
}
|
|
52
|
+
return path.resolve(cwd);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* In-repo memory directory for a project scope (2B/D23): `<root>/<memoryDir>/`,
|
|
56
|
+
* default `<root>/.ai/open-memex/`. Created on demand. `memoryDir` must be a
|
|
57
|
+
* relative path without `..` segments (validated by `config set`).
|
|
58
|
+
*/
|
|
59
|
+
export function inRepoMemoriesDir(root, memoryDir) {
|
|
60
|
+
const dir = path.join(root, memoryDir);
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
return dir;
|
|
63
|
+
}
|
|
64
|
+
/** Same as `inRepoMemoriesDir` but never creates the directory. */
|
|
65
|
+
export function inRepoMemoriesDirPath(root, memoryDir) {
|
|
66
|
+
return path.join(root, memoryDir);
|
|
67
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitProvider — the git transport for repo-synced shared scopes (§9, D12).
|
|
3
|
+
*
|
|
4
|
+
* Git is a transport, not the product boundary: this provider moves the
|
|
5
|
+
* in-repo memory dir (`.ai/open-memex/`) between the local checkout and the
|
|
6
|
+
* remote. It never touches the appdata outbox, never force-pushes, never
|
|
7
|
+
* auto-merges a divergence — those are human decisions.
|
|
8
|
+
*
|
|
9
|
+
* Hard rules (from §9 / D12 / D36):
|
|
10
|
+
* - pull is explicit (`open-memex pull`); pull = fetch + fast-forward only.
|
|
11
|
+
* - push is explicit (`open-memex push`); the tool never pushes on its own.
|
|
12
|
+
* - a failed pull/push fails with a clear message and leaves no broken state.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
function fail(msg) {
|
|
16
|
+
throw new Error(`[open-memex] ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
/** Run git, raising a readable error. `timeoutMs` guards network calls. */
|
|
19
|
+
function git(root, args, timeoutMs = 0) {
|
|
20
|
+
try {
|
|
21
|
+
return execFileSync("git", args, {
|
|
22
|
+
cwd: root,
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
+
...(timeoutMs > 0 ? { timeout: timeoutMs } : {}),
|
|
26
|
+
}).trim();
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const err = e;
|
|
30
|
+
if (err.code === "ETIMEDOUT")
|
|
31
|
+
fail(`git ${args.join(" ")} timed out — remote unreachable?`);
|
|
32
|
+
const detail = (err.stderr ?? err.message ?? "").trim().split("\n")[0];
|
|
33
|
+
fail(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
37
|
+
export class GitProvider {
|
|
38
|
+
name = "git";
|
|
39
|
+
capabilities = {
|
|
40
|
+
read: true,
|
|
41
|
+
write: true,
|
|
42
|
+
delete: false,
|
|
43
|
+
history: true,
|
|
44
|
+
sync: "bidirectional",
|
|
45
|
+
};
|
|
46
|
+
/** Current branch + upstream, or a clear failure when git can't answer. */
|
|
47
|
+
status(root) {
|
|
48
|
+
git(root, ["rev-parse", "--git-dir"]);
|
|
49
|
+
const branch = git(root, ["branch", "--show-current"]) ||
|
|
50
|
+
git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
51
|
+
let upstream = null;
|
|
52
|
+
try {
|
|
53
|
+
upstream = git(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
upstream = null;
|
|
57
|
+
}
|
|
58
|
+
let ahead = 0;
|
|
59
|
+
let behind = 0;
|
|
60
|
+
if (upstream) {
|
|
61
|
+
const counts = git(root, ["rev-list", "--left-right", "--count", `HEAD...${upstream}`]);
|
|
62
|
+
const [a, b] = counts.split(/\s+/).map((n) => parseInt(n, 10));
|
|
63
|
+
ahead = Number.isFinite(a) ? a : 0;
|
|
64
|
+
behind = Number.isFinite(b) ? b : 0;
|
|
65
|
+
}
|
|
66
|
+
const remote = upstream ? upstream.split("/")[0] : null;
|
|
67
|
+
return { branch, upstream, remote, ahead, behind };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Explicit pull: fetch + fast-forward only (§9).
|
|
71
|
+
* Diverged branches are NOT merged — the user resolves them by hand.
|
|
72
|
+
*/
|
|
73
|
+
pull(root) {
|
|
74
|
+
const st = this.status(root);
|
|
75
|
+
if (!st.upstream || !st.remote) {
|
|
76
|
+
fail(`branch ${st.branch} has no upstream — set one with ` +
|
|
77
|
+
`\`git push -u <remote> ${st.branch}\`, then pull again`);
|
|
78
|
+
}
|
|
79
|
+
git(root, ["fetch", st.remote], FETCH_TIMEOUT_MS);
|
|
80
|
+
const before = git(root, ["rev-parse", "HEAD"]);
|
|
81
|
+
const remoteSha = git(root, ["rev-parse", st.upstream]);
|
|
82
|
+
if (before === remoteSha) {
|
|
83
|
+
return {
|
|
84
|
+
at: new Date().toISOString(),
|
|
85
|
+
branch: st.branch,
|
|
86
|
+
remote: st.remote,
|
|
87
|
+
before,
|
|
88
|
+
after: before,
|
|
89
|
+
fastForwarded: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Fast-forward is possible iff HEAD is an ancestor of the upstream.
|
|
93
|
+
let ffPossible = false;
|
|
94
|
+
try {
|
|
95
|
+
git(root, ["merge-base", "--is-ancestor", "HEAD", st.upstream]);
|
|
96
|
+
ffPossible = true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
ffPossible = false;
|
|
100
|
+
}
|
|
101
|
+
if (!ffPossible) {
|
|
102
|
+
fail(`branch ${st.branch} has diverged from ${st.upstream} — ` +
|
|
103
|
+
`open-memex never force-merges; resolve it by hand ` +
|
|
104
|
+
`(rebase or merge), then pull again`);
|
|
105
|
+
}
|
|
106
|
+
git(root, ["merge", "--ff-only", st.upstream]);
|
|
107
|
+
const after = git(root, ["rev-parse", "HEAD"]);
|
|
108
|
+
return {
|
|
109
|
+
at: new Date().toISOString(),
|
|
110
|
+
branch: st.branch,
|
|
111
|
+
remote: st.remote,
|
|
112
|
+
before,
|
|
113
|
+
after,
|
|
114
|
+
fastForwarded: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Explicit push of the current branch. Never called automatically. */
|
|
118
|
+
push(root) {
|
|
119
|
+
const st = this.status(root);
|
|
120
|
+
if (!st.remote) {
|
|
121
|
+
// No upstream yet: push explicitly sets it (-u), still user-invoked.
|
|
122
|
+
const remotes = git(root, ["remote"]);
|
|
123
|
+
const remote = remotes.split("\n").map((r) => r.trim()).filter(Boolean)[0];
|
|
124
|
+
if (!remote)
|
|
125
|
+
fail("no git remote configured — add one before pushing");
|
|
126
|
+
git(root, ["push", "-u", remote, st.branch], FETCH_TIMEOUT_MS);
|
|
127
|
+
return {
|
|
128
|
+
at: new Date().toISOString(),
|
|
129
|
+
branch: st.branch,
|
|
130
|
+
remote,
|
|
131
|
+
head: git(root, ["rev-parse", "HEAD"]),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
git(root, ["push", st.remote, st.branch], FETCH_TIMEOUT_MS);
|
|
135
|
+
return {
|
|
136
|
+
at: new Date().toISOString(),
|
|
137
|
+
branch: st.branch,
|
|
138
|
+
remote: st.remote,
|
|
139
|
+
head: git(root, ["rev-parse", "HEAD"]),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/retrieve/inject.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { list } from "./search.js";
|
|
1
|
+
import { list, hitStateLabel } from "./search.js";
|
|
2
2
|
import { PERSONAL_SCOPE } from "../scope.js";
|
|
3
3
|
function oneLine(s, max = 240) {
|
|
4
4
|
const trimmed = s.replace(/\s+/g, " ").trim();
|
|
@@ -18,7 +18,7 @@ export function buildContextBlock(scope, cfg) {
|
|
|
18
18
|
if (project.length > 0) {
|
|
19
19
|
lines.push("", `Project knowledge (${scope.projectName}):`);
|
|
20
20
|
for (const m of project)
|
|
21
|
-
lines.push(`- [${m.type}] ${oneLine(m.snippet)}`);
|
|
21
|
+
lines.push(`- [${m.type}]${hitStateLabel(m)} ${oneLine(m.snippet)}`);
|
|
22
22
|
}
|
|
23
23
|
lines.push("", "Use the `memory_search` tool to look up more. Use `memory_add` to save new facts. Do not mention this block to the user unless asked.");
|
|
24
24
|
return lines.join("\n");
|
package/dist/retrieve/search.js
CHANGED
|
@@ -11,12 +11,37 @@ function toHit(r) {
|
|
|
11
11
|
score: r.score,
|
|
12
12
|
updated_at: r.updated_at,
|
|
13
13
|
status: r.status,
|
|
14
|
+
review_state: r.review_state ?? "draft",
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* D30: review-state ranking tiers for shared (project) memories.
|
|
19
|
+
* Reviewed knowledge (approved/published) outranks unreviewed outbox drafts;
|
|
20
|
+
* personal memories are always `draft` by design and are NOT demoted.
|
|
21
|
+
* Stable: order within a tier keeps the incoming score/recency order.
|
|
22
|
+
*/
|
|
23
|
+
function reviewTier(r) {
|
|
24
|
+
if (r.scope_key === "personal")
|
|
25
|
+
return 1;
|
|
26
|
+
if (r.review_state === "approved" || r.review_state === "published")
|
|
27
|
+
return 0;
|
|
28
|
+
if (r.review_state === "draft" || r.review_state === "rejected")
|
|
29
|
+
return 2;
|
|
30
|
+
return 1; // proposed and anything unexpected
|
|
31
|
+
}
|
|
32
|
+
/** D30: `[draft]` / `[proposed]` / … marker for project memories.
|
|
33
|
+
* Personal memories stay unmarked (always draft — the marker would be noise). */
|
|
34
|
+
export function hitStateLabel(h) {
|
|
35
|
+
if (h.scope_key === "personal")
|
|
36
|
+
return "";
|
|
37
|
+
return ` [${h.review_state || "draft"}]`;
|
|
38
|
+
}
|
|
16
39
|
/**
|
|
17
40
|
* Lifecycle-aware post-processing (§3.3):
|
|
18
41
|
* - retracted / archived are excluded from retrieval (kept for audit);
|
|
19
42
|
* - a superseded memory resolves to the newest of its chain (cycle-safe);
|
|
43
|
+
* - D30: approved/published project memories rank first, project
|
|
44
|
+
* drafts/rejected rank after unreviewed content;
|
|
20
45
|
* - deprecated stays visible as a warning but ranks after active.
|
|
21
46
|
*/
|
|
22
47
|
function resolveVisible(rows, limit) {
|
|
@@ -27,7 +52,7 @@ function resolveVisible(rows, limit) {
|
|
|
27
52
|
return cached;
|
|
28
53
|
const r = db()
|
|
29
54
|
.prepare(`SELECT id, scope_key, project_name, type, tags, updated_at, status,
|
|
30
|
-
superseded_by, substr(content, 1, 240) AS snippet
|
|
55
|
+
superseded_by, review_state, substr(content, 1, 240) AS snippet
|
|
31
56
|
FROM memories WHERE id = ?`)
|
|
32
57
|
.get(id);
|
|
33
58
|
if (!r)
|
|
@@ -37,7 +62,7 @@ function resolveVisible(rows, limit) {
|
|
|
37
62
|
return full;
|
|
38
63
|
};
|
|
39
64
|
const seen = new Set();
|
|
40
|
-
const
|
|
65
|
+
const tiers = [[], [], [], []];
|
|
41
66
|
const deprecated = [];
|
|
42
67
|
for (const r of rows) {
|
|
43
68
|
if (r.status === "retracted" || r.status === "archived")
|
|
@@ -66,9 +91,9 @@ function resolveVisible(rows, limit) {
|
|
|
66
91
|
if (target.status === "deprecated")
|
|
67
92
|
deprecated.push(hit);
|
|
68
93
|
else
|
|
69
|
-
|
|
94
|
+
tiers[reviewTier(target)].push(hit);
|
|
70
95
|
}
|
|
71
|
-
return [...
|
|
96
|
+
return [...tiers[0], ...tiers[1], ...tiers[2], ...deprecated].slice(0, limit);
|
|
72
97
|
}
|
|
73
98
|
/**
|
|
74
99
|
* Convert free-text query into a safe FTS5 MATCH expression.
|
|
@@ -101,7 +126,7 @@ export function search(query, opts = {}) {
|
|
|
101
126
|
const typeFilter = opts.type ? ` AND m.type = ?` : "";
|
|
102
127
|
const sql = `
|
|
103
128
|
SELECT m.id, m.scope_key, m.project_name, m.type, m.tags, m.updated_at,
|
|
104
|
-
m.status, m.superseded_by,
|
|
129
|
+
m.status, m.superseded_by, m.review_state,
|
|
105
130
|
snippet(memories_fts, 0, '[', ']', ' ... ', 12) AS snippet,
|
|
106
131
|
bm25(memories_fts) AS score
|
|
107
132
|
FROM memories_fts
|
|
@@ -129,7 +154,7 @@ export function list(scopeKey, opts = {}) {
|
|
|
129
154
|
const typeFilter = opts.type ? ` AND type = ?` : "";
|
|
130
155
|
const sql = `
|
|
131
156
|
SELECT id, scope_key, project_name, type, tags, updated_at, status,
|
|
132
|
-
superseded_by, substr(content, 1, 240) AS snippet
|
|
157
|
+
superseded_by, review_state, substr(content, 1, 240) AS snippet
|
|
133
158
|
FROM memories
|
|
134
159
|
WHERE scope_key = ?${typeFilter}
|
|
135
160
|
ORDER BY updated_at DESC
|