Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
19 changes: 19 additions & 0 deletions .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "agentmemory",
"owner": {
"name": "Rohit Ghumare",
"github": "rohitg00"
},
"metadata": {
"description": "Persistent memory for AI coding agents",
"version": "0.9.28"
},
"plugins": [
{
"name": "agentmemory",
"source": "./plugin",
"description": "Cursor lifecycle hooks + MCP + skills for agentmemory",
"version": "0.9.28"
}
]
}
87 changes: 87 additions & 0 deletions integrations/cursor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# agentmemory for Cursor

Cursor-native plugin: **lifecycle hooks** (thin shim + workspace resolver), **MCP**, and shared **skills**.

Hooks delegate to the canonical compiled scripts in `plugin/scripts/*.mjs` (from `src/hooks/*.ts`). The only Cursor-specific code lives in `plugin/scripts/cursor/`.

## Quick local install

Prereq: `~/.agentmemory/.env` with `AGENTMEMORY_URL` and `AGENTMEMORY_SECRET`.

```bash
node integrations/cursor/install-local.mjs
node integrations/cursor/verify-flow.mjs
```

Then in Cursor:

1. **Settings → Plugins → Add marketplace** → select the **repo root**
`<path-to-your-agentmemory-clone>`
(must contain `.cursor-plugin/marketplace.json`)
2. Enable plugin **agentmemory**
3. **Disable** the old `rohitg00/agentmemory` marketplace entry if both are on
4. **Developer: Reload Window**
5. Confirm hooks log shows `scripts/cursor/run-hook.mjs` or `run-detached.mjs`

Only after plugin hooks are confirmed:

```bash
node integrations/cursor/install-local.mjs --clear-user-hooks
```

## Layout

```text
agentmemory/ ← marketplace root (git repo)
.cursor-plugin/marketplace.json
plugin/
.cursor-plugin/plugin.json
hooks/hooks.cursor.json ← Cursor camelCase format
scripts/cursor/
workspace.mjs ← resolveWorkspace()
run-hook.mjs ← shim → plugin/scripts/*.mjs
run-detached.mjs ← non-blocking stop / sessionEnd
scripts/*.mjs ← canonical hooks (shared with Claude/Codex)
skills/
.mcp.json
integrations/cursor/
install-local.mjs
verify-flow.mjs
close-stale-am-sessions.mjs ← dev utility
migrate-bad-projects.mjs ← one-off migration for bad .cursor projects
```

## Hooks

| Hook | Shim | Delegates to |
|------|------|--------------|
| `sessionStart` | `run-hook.mjs` | `session-start.mjs` |
| `beforeSubmitPrompt` | `run-hook.mjs` | `prompt-submit.mjs` |
| `preToolUse` / `postToolUse` | `run-hook.mjs` | `pre-tool-use.mjs` / `post-tool-use.mjs` |
| `stop` | `run-detached.mjs` | `stop.mjs` |
| `sessionEnd` | `run-detached.mjs` | `session-end.mjs` |

The shim calls `resolveWorkspace()`, sets `AGENTMEMORY_PROJECT_NAME`, enriches `cwd` on the payload, then spawns the official hook script.

## sessionEnd (Cursor 3.13.x)

- `window_close`: often broken (`MainThreadShellExec not initialized`)
- Tab close may work — test manually
- `stop` is reliable for summarize; treat `sessionEnd` as best-effort until Cursor fixes lifecycle on their side

## Dev utilities

```bash
node integrations/cursor/close-stale-am-sessions.mjs --dry-run
node integrations/cursor/migrate-bad-projects.mjs --dry-run
```

`migrate-bad-projects.mjs` is for legacy sessions stored with `project=.cursor` only. The resolver should prevent new bad data.

## PR checklist

- [x] `plugin/.cursor-plugin/plugin.json` + `hooks.cursor.json`
- [x] Shim delegates to `plugin/scripts/*.mjs` (no duplicated hook logic)
- [ ] README: Cursor = MCP + native hooks (not MCP-only) — upstream root README
- [ ] `agentmemory connect cursor` docs update
- [x] Disable duplicate user `~/.cursor/hooks.json` in docs only (not auto-cleared)
143 changes: 143 additions & 0 deletions integrations/cursor/close-stale-am-sessions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env node
/**
* Close agentmemory sessions stuck in status "active" (sessionEnd hook never ran).
*
* Usage:
* node integrations/cursor/close-stale-am-sessions.mjs --dry-run
* node integrations/cursor/close-stale-am-sessions.mjs --min-age-hours 24
* node integrations/cursor/close-stale-am-sessions.mjs --min-age-hours 6 --project my-project
* node integrations/cursor/close-stale-am-sessions.mjs --exclude ses_abc123,def456
*/
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

function loadEnv() {
const envPath = join(homedir(), '.agentmemory', '.env');
const env = {};
if (!existsSync(envPath)) return env;
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
}
return env;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Merge runtime environment values with ~/.agentmemory/.env. Both maintenance CLIs currently ignore process.env, despite the documented configuration contract.

  • integrations/cursor/close-stale-am-sessions.mjs#L15-L27: resolve AGENTMEMORY_URL and AGENTMEMORY_SECRET from process.env before falling back to file values.
  • integrations/cursor/migrate-bad-projects.mjs#L17-L29: apply the same precedence rule.
📍 Affects 2 files
  • integrations/cursor/close-stale-am-sessions.mjs#L15-L27 (this comment)
  • integrations/cursor/migrate-bad-projects.mjs#L17-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/cursor/close-stale-am-sessions.mjs` around lines 15 - 27, Update
loadEnv in integrations/cursor/close-stale-am-sessions.mjs (lines 15-27) and the
corresponding environment-loading logic in
integrations/cursor/migrate-bad-projects.mjs (lines 17-29) to merge process.env
with values from ~/.agentmemory/.env, giving process.env precedence for
AGENTMEMORY_URL and AGENTMEMORY_SECRET while retaining file values as fallbacks.


function parseArgs() {
const args = process.argv.slice(2);
const opts = {
dryRun: args.includes('--dry-run'),
minAgeHours: 24,
project: null,
exclude: new Set()
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--min-age-hours' && args[i + 1]) {
const raw = args[++i];
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 0) {
console.error(`Invalid --min-age-hours value: ${raw}`);
process.exit(1);
}
opts.minAgeHours = parsed;
} else if (args[i] === '--project' && args[i + 1]) {
opts.project = args[++i];
} else if (args[i] === '--exclude' && args[i + 1]) {
for (const id of args[++i].split(',')) {
const t = id.trim();
if (t) opts.exclude.add(t);
}
}
}
return opts;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function main() {
const opts = parseArgs();
const { AGENTMEMORY_URL: restUrl, AGENTMEMORY_SECRET: secret } = loadEnv();
if (!restUrl || !secret) {
console.error('Missing AGENTMEMORY_URL or AGENTMEMORY_SECRET in ~/.agentmemory/.env');
process.exit(1);
}

const headers = {
Authorization: `Bearer ${secret}`,
'Content-Type': 'application/json'
};

const listRes = await fetch(`${restUrl}/agentmemory/sessions`, { headers });
if (!listRes.ok) {
console.error('Failed to list sessions:', listRes.status, await listRes.text());
process.exit(1);
}

const { sessions } = await listRes.json();
const cutoff = Date.now() - opts.minAgeHours * 3600000;
const candidates = sessions.filter((s) => {
if (s.status !== 'active') return false;
if (opts.exclude.has(s.id)) return false;
if (opts.project && s.project !== opts.project) return false;
const started = Date.parse(s.startedAt);
if (!Number.isFinite(started) || started > cutoff) return false;
return true;
});

candidates.sort((a, b) => Date.parse(a.startedAt) - Date.parse(b.startedAt));

console.log(
`Found ${candidates.length} active session(s) older than ${opts.minAgeHours}h` +
(opts.project ? ` (project=${opts.project})` : '')
);

for (const s of candidates) {
const ageH = ((Date.now() - Date.parse(s.startedAt)) / 3600000).toFixed(1);
console.log(
` ${s.id.slice(0, 12)}… ${s.project} obs=${s.observationCount ?? 0} age=${ageH}h`
);
}

if (!candidates.length) {
console.log('Nothing to close.');
return;
}

if (opts.dryRun) {
console.log(`Dry run: would close ${candidates.length} session(s) via POST /agentmemory/session/end`);
return;
}

let closed = 0;
let failed = 0;

for (const s of candidates) {
const res = await fetch(`${restUrl}/agentmemory/session/end`, {
method: 'POST',
headers,
body: JSON.stringify({ sessionId: s.id })
});
if (res.ok) {
closed++;
} else {
failed++;
console.error(` failed ${s.id}: ${res.status} ${await res.text()}`);
}
}

console.log(`Closed ${closed} session(s), ${failed} failed.`);

const verify = await fetch(`${restUrl}/agentmemory/sessions`, { headers }).then((r) =>
r.json()
);
const stillActive = verify.sessions.filter(
(s) => s.status === 'active' && Date.parse(s.startedAt) <= cutoff
).length;
console.log(`Remaining stale active (>${opts.minAgeHours}h): ${stillActive}`);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading