Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,49 @@ Results are commit-anchored so any number above can be checked out and re-run. S

Shared and synced memory is only useful if it cannot be silently corrupted. Skills merged from external sources (e.g. `skill sync` imports) are **treated as unproven until validated locally**: imported skills carry provenance markers, never enter the `proven` class directly, must pass canary validation on real tasks before promotion, and `anti-pattern` skills are isolated rather than deleted so they can be audited. Promotion is gated by the four-class lifecycle, not by trust in the source. See [docs/team-memory-security.md](docs/team-memory-security.md).

## Install

Three paths. Use one. Package name: `@roarpeng/graphflow`.

### Manual VSIX

Download `graphflow-<version>.vsix` from [GitHub Releases](https://github.com/Roarpeng/GraphFlow/releases) (Open VSX id `roarpeng.graphflow`). Replace `<version>` with the file you downloaded.

```bash
code --install-extension graphflow-<version>.vsix
cursor --install-extension graphflow-<version>.vsix
```

Activation registers MCP and copies the runtime to `~/.graphflow/runtime/`. From this repo, `npm run package:extension` writes `artifacts/graphflow-<version>.vsix` (`vscode-extension` runs `npx @vscode/vsce package --no-dependencies`).

### npm

```bash
npm install -g @roarpeng/graphflow
```

A global install's postinstall runs `graphflow install`: MCP, skill, and the instruction block on every detected host, plus dangling-entry repair. Check or repeat it with:

```bash
npx @roarpeng/graphflow@latest doctor
npx @roarpeng/graphflow@latest install
npx @roarpeng/graphflow install --host cursor
```

`install` leaves undetected hosts untouched. `--host` takes a registry id (`cursor`, `claude-code`, `codex`, …).

### Agent prompt

Paste this to Cursor, Claude Code, Codex, or another coding agent. It only uses the installer above:

```text
Install @roarpeng/graphflow and configure its MCP server for this agent, then verify it.

1. Run `npm install -g @roarpeng/graphflow`. The global postinstall runs `graphflow install`, which registers MCP, the skill, and the instruction block for every host it detects and repairs dangling entries.
2. Run `npx @roarpeng/graphflow doctor --json`. If this host is missing, run `npx @roarpeng/graphflow install --host <id>` with the id from that report (`cursor`, `claude-code`, or `codex`) and run doctor again.
3. It is working when doctor reports ok and this host is not missing. Restart the agent or open a new session so it loads the MCP config.
```

## Quick start

No API key needed (offline AST indexing + graph compression):
Expand Down
43 changes: 43 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,49 @@ GraphFlow 把 **记忆 + hooks + skills** 做成可移植的 MCP 表面(Cursor

**一条命令安装承诺**:`npm i -g @roarpeng/graphflow` = 安装 + 注册 + 检测 + 修复;VSIX 激活同样自动完成注册,并把运行时同步到稳定目录 `~/.graphflow/runtime/`,MCP 条目指向稳定路径——IDE 升级删旧扩展目录不再导致悬空。两种安装方式都是装完即用,三平台一致。

## 安装

三条路径,选一条。包名:`@roarpeng/graphflow`。

### 手动安装 VSIX

从 [GitHub Releases](https://github.com/Roarpeng/GraphFlow/releases) 下载 `graphflow-<version>.vsix`(Open VSX 标识 `roarpeng.graphflow`)。把 `<version>` 换成所下载文件的版本。

```bash
code --install-extension graphflow-<version>.vsix
cursor --install-extension graphflow-<version>.vsix
```

激活时扩展会注册 MCP,并把运行时复制到 `~/.graphflow/runtime/`。在本仓库执行 `npm run package:extension` 会生成 `artifacts/graphflow-<version>.vsix`(`vscode-extension` 里实际运行 `npx @vscode/vsce package --no-dependencies`)。

### npm

```bash
npm install -g @roarpeng/graphflow
```

全局安装的 postinstall 会执行 `graphflow install`:向每个检测到的宿主注册 MCP、Skill 和指令块,并修复悬空条目。检查或重跑:

```bash
npx @roarpeng/graphflow@latest doctor
npx @roarpeng/graphflow@latest install
npx @roarpeng/graphflow install --host cursor
```

`install` 不会改写未检测到的宿主。`--host` 接受注册表 id(`cursor`、`claude-code`、`codex` 等)。

### 交给编程 Agent 的提示词

把下面这段贴给 Cursor、Claude Code、Codex 或其他编程 Agent。它只用上面的安装器:

```text
Install @roarpeng/graphflow and configure its MCP server for this agent, then verify it.

1. Run `npm install -g @roarpeng/graphflow`. The global postinstall runs `graphflow install`, which registers MCP, the skill, and the instruction block for every host it detects and repairs dangling entries.
2. Run `npx @roarpeng/graphflow doctor --json`. If this host is missing, run `npx @roarpeng/graphflow install --host <id>` with the id from that report (`cursor`, `claude-code`, or `codex`) and run doctor again.
3. It is working when doctor reports ok and this host is not missing. Restart the agent or open a new session so it loads the MCP config.
```

## 快速开始

```bash
Expand Down
33 changes: 23 additions & 10 deletions tests/mcp-http-session-ttl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
* JSON mode holds no SSE stream, so the sweeper treats the gap between
* handshake POSTs as idle (`openStreams` stays empty; each request only
* refreshes `lastActivityAt` when it arrives). 80ms was shorter than that
* gap on a loaded Node 20 runner, and the session was deleted inside
* `client.connect`. This override stays far below the 30-minute product
* default; after the handshake the test waits past it with no further requests.
*/
const HANDSHAKE_SAFE_TTL_MS = 3_000;
const SWEEP_INTERVAL_MS = 200;

/**
* Opens one stateful session and abandons it WITHOUT the terminating DELETE
* (client.close() only aborts local streams in the SDK) — the exact leak the
Expand Down Expand Up @@ -70,21 +81,21 @@ describe("HTTP stateful session idle TTL", () => {
port: 0,
stateful: true,
enableJsonResponse: true,
sessionTtlMs: 80,
sessionSweepIntervalMs: 25,
sessionTtlMs: HANDSHAKE_SAFE_TTL_MS,
sessionSweepIntervalMs: SWEEP_INTERVAL_MS,
});
try {
const sessionId = await openAbandonedSession(started);
// Silent wait (polling would REFRESH lastActivityAt and defeat the
// sweep by design); one probe at 1s and a final one at 3s give the
// TTL-80ms + sweep-25ms machinery a 30x+ margin under full-suite load.
// sweep by design). The clock starts at the last handshake request,
// so this must land after HANDSHAKE_SAFE_TTL_MS plus a few sweeps.
const probe = () =>
postJson(
started.url,
{ jsonrpc: "2.0", id: "after-ttl", method: "ping" },
{ "Mcp-Session-Id": sessionId }
);
await sleep(1_000);
await sleep(HANDSHAKE_SAFE_TTL_MS + SWEEP_INTERVAL_MS * 4);
let stale = await probe();
if (stale.status !== 404) {
await sleep(2_000);
Expand All @@ -96,7 +107,7 @@ describe("HTTP stateful session idle TTL", () => {
} finally {
await started.close();
}
}, 15_000);
}, 20_000);

it("keeps sweeping sessions alive while requests keep arriving (activity refresh)", async () => {
const started = await startStreamableHttpServer(undefined, {
Expand Down Expand Up @@ -165,19 +176,21 @@ describe("HTTP stateful session idle TTL", () => {

it("GRAPHFLOW_HTTP_SESSION_TTL_MS env overrides the default TTL", async () => {
const previous = process.env.GRAPHFLOW_HTTP_SESSION_TTL_MS;
process.env.GRAPHFLOW_HTTP_SESSION_TTL_MS = "80";
process.env.GRAPHFLOW_HTTP_SESSION_TTL_MS = String(HANDSHAKE_SAFE_TTL_MS);
let started: StartedMcpHttpServer | undefined;
try {
started = await startStreamableHttpServer(undefined, {
host: "127.0.0.1",
port: 0,
stateful: true,
enableJsonResponse: true,
sessionSweepIntervalMs: 25,
sessionSweepIntervalMs: SWEEP_INTERVAL_MS,
});
const sessionId = await openAbandonedSession(started);
// Silent wait then two probes (polling refreshes activity by design).
await sleep(1_000);
// Same handshake margin as the explicit sessionTtlMs case: the env
// value is the idle TTL, and it must not fire between connect's POSTs.
await sleep(HANDSHAKE_SAFE_TTL_MS + SWEEP_INTERVAL_MS * 4);
const probe = () =>
postJson(
started.url,
Expand All @@ -198,5 +211,5 @@ describe("HTTP stateful session idle TTL", () => {
}
await started?.close();
}
}, 15_000);
}, 20_000);
});
Loading