Commit Graph

11 Commits

Author SHA1 Message Date
Rohit Ghumare be89b222b0 feat: devin support (cli adapter, plugin, cloud mcp) (#1214)
* feat: devin support replacing windsurf

* feat: devin cli adapter, plugin manifest, and hook payload compat

* fix: stale tool counts in translations and cwd validation
2026-08-16 14:44:10 +01:00
Rohit Ghumare 37ea1b99ad feat: cursor marketplace plugin with hooks, mcp, and skills (#1213)
* feat: cursor marketplace plugin with native hooks and mcp config

* fix: cursor payload compat and transcript prompt backfill in hooks

* fix: plugin-root hook paths, backfill ordering, session-id fallbacks

* docs: cursor plugin rows in readme, translations, and changelog

* fix: cursor native-plugin card, broken agent logos

* chore: sync openclaw and hermes plugin manifest versions

* docs: openclaw hook permission and hermes tool count

* chore: clawhub compat metadata for openclaw plugin

* docs: tested openclaw and hermes install rows in readme
2026-08-16 13:30:40 +01:00
Rohit Ghumare 6761a99ba1 fix: guard hooks against null payload (#1074)
#1047: JSON.parse("null") returns null without throwing, so every hook's parse guard passed it through and the first data.xxx access threw a TypeError. Bare main() turned that into an unhandled rejection -> exit 1 -> host reported 'hook failed' on every affected tool call. All 13 hook entrypoints now guard non-object payloads before dereferencing and wrap main() in .catch() to fail closed (silent exit 0).

#1057: mem::context and api::context filtered candidate sessions by project only, leaking cross-agent observations/summaries under AGENTMEMORY_AGENT_SCOPE=isolated. Now applies the same agent-scope filter as mem::search (#817); api::context, api::session::start, and event::session::started forward agentId.

Also: bump 0.9.28 across manifests/deploy/export-import set; refresh stale README/AGENTS stats (files/LOC/functions/KV; AGENTS tests 950+ -> 1,428+) and regenerate the website meta snapshot to 0.9.28; CHANGELOG 0.9.28 section; remove the rate-limited star-history chart from README and all 11 translations.
2026-07-19 11:40:26 +01:00
Ross Story a0da02b6b3 Add GitHub Copilot CLI support (#534)
* feat: add Copilot CLI plugin asset slice

- plugin/.plugin/plugin.json: Copilot manifest with name/version/skills/mcpServers/hooks refs
- plugin/.mcp.copilot.json: MCP server config with type:local, npx, env passthrough, tools:[*]
- plugin/hooks/hooks.copilot.json: Copilot hooks (version:1) with 11 supported events and PreToolUse matcher
- test/copilot-plugin.test.ts: 11 tests covering manifest, MCP config, and hooks validation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Copilot CLI connect support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add GitHub Copilot CLI support

Adds Copilot CLI support through a root plugin manifest, Copilot-specific MCP and hook configuration, and a connect adapter for MCP-only setup.

Includes Windows-safe Copilot MCP command generation, COPILOT_HOME handling, Copilot hook payload normalization, generated hook scripts, and targeted tests for plugin shape, hook execution, and connect behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden Copilot hook handling

Addresses upstream AI review suggestions by aligning the Copilot preToolUse matcher with the hook allowlist, narrowing hook payload fields at runtime, normalizing subagent fallbacks, and tightening hook config validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Copilot to first-run onboarding

Includes GitHub Copilot CLI in the first-run agent picker and adds a regression test so the Copilot setup path remains discoverable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Default onboarding to Copilot inside Copilot CLI

Detect Copilot CLI environment markers during first-run setup so pressing Enter wires the current agent instead of the historical Claude Code default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Support framed stdio MCP transport

Accept Content-Length framed JSON-RPC messages in addition to the existing newline-delimited transport so Copilot CLI can initialize the standalone MCP server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Narrow Copilot pre-tool session ids

Ensures pre-tool-use only forwards string session IDs and falls back to unknown for invalid Copilot payload values, with regression coverage for the generated plugin script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ross Story <rostory@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-28 09:54:54 +01:00
Rohit Ghumare 0468407249 fix(hooks): send repo basename as project, not full path (#474) (#687)
* fix(hooks): send repo basename as project, not full path (closes #474)

Hooks were sending `data.cwd` (an absolute filesystem path) as the
`project` field on every observe/session/start call. Native sessions,
replay-import, and manual memory_lesson_save calls all use the repo
basename. The mismatch caused auto-injected context to filter out the
bulk of relevant lessons because the path never matches the stored
project name.

Add shared `resolveProject(cwd)` helper:
  1. AGENTMEMORY_PROJECT_NAME env (per-repo escape hatch)
  2. basename of `git rev-parse --show-toplevel` (handles subdirs)
  3. basename of cwd (final fallback when not in a git repo)

Applied to 9 hooks: notification, post-tool-use, post-tool-failure,
prompt-submit, session-start, subagent-start, subagent-stop,
task-completed, pre-compact.

Build: split hook entries into per-entry tsdown configs so each hook
bundles into a fully self-contained .mjs. Previous shared config
hoisted helpers into hashed chunks that changed on every rebuild.

* chore(hooks): drop issue-number ref from resolveProject comment

* chore: trim verbose comments on _project.ts + tsdown.config
2026-05-27 20:32:54 +01:00
Rohit Ghumare 1ff5849d9c fix: cap session-start/subagent-start hook latency (#221) (#271)
Two hook scripts blocked Claude Code's startup waiting on REST responses
they didn't actually need:

- `session-start` awaited a 5000ms POST and discarded the response when
  `AGENTMEMORY_INJECT_CONTEXT=false` (the default). Pure latency.
- `subagent-start` had a `// fire and forget` comment but the code
  awaited a 2000ms POST. Pure latency.

Under fan-out (Slack-bot orchestrators, multi-agent harnesses, fanned
`claude -p` jobs) the awaited timeouts stack and feed back into the
engine; the reporter hit a positive feedback loop that OOM-killed
iii-engine.

Fix:

- `session-start` — fire-and-forget when `INJECT_CONTEXT=false`. Cap the
  inject path at 1500ms (down from 5000ms) so a slow server can't block
  the agent indefinitely when stdout is actually consumed.
- `subagent-start` — actually fire-and-forget, matching the existing
  comment. Cap at 800ms.

Verified live against a black-hole TCP listener (accepts, never replies):
- session-start (no inject): 5.05s → 0.85s
- session-start (inject):    5.05s → 1.55s
- subagent-start:            2.05s → 0.87s

Built artifacts in `plugin/scripts/` regenerated via `npx tsdown`.

Closes #221.
2026-05-10 19:08:34 +01:00
Rohit Ghumare 51bcb09104 address CodeRabbit review on #187 + fix CI
Findings verified against current code on this branch; all four valid.

1. config.ts loadFallbackConfig (L281) — user could set
   FALLBACK_PROVIDERS=agent-sdk and bypass the AGENTMEMORY_ALLOW_AGENT_SDK
   gate added to detectProvider. Filter it out at the fallback layer too,
   with the same warning pointing at the opt-in flag.

2. summarize.ts (L87-92) — the empty_provider_response branch returned
   without recording failure metrics or a diagnostic log, unlike the
   parse/validation paths. Record the same metricsStore failure event and
   log provider name, prompt size, system size, and observation count so
   empty responses are visible in telemetry.

3. providers/agent-sdk.ts (L14-45) — setting
   process.env.AGENTMEMORY_SDK_CHILD = '1' without restoring it caused
   every subsequent .query() in the same parent process to hit the
   short-circuit guard and return '' (classified as a SDK child it is
   not). Capture prev, set in try, restore in finally (delete if prev
   was undefined). Child processes spawned during the for-await loop
   still inherit the marker because env is inherited at spawn time; we
   only restore after the loop completes.

4. plugin/scripts/sdk-guard-DI1NUOS9.mjs — tsdown extracted the shared
   guard helper into a hashed chunk. Hash rotates on every rebuild and
   churns the diff. Stopped using the shared module from hooks entirely
   and inlined the 6-line guard function into each hook .ts file
   instead. sdk-guard.ts stays in the tree because the unit tests cover
   it directly. Deleted the tracked hashed .mjs and confirmed no new
   chunk is emitted.

Also applied the CI two-step install (npm install --package-lock-only
then npm ci) on this branch, matching #184. Without it, npm ci fails
because lockfiles are gitignored.

Tests: 74 files / 819 tests pass.
2026-04-22 11:44:05 +01:00
Rohit Ghumare 5e63846b29 fix(hooks): break Stop-hook infinite recursion via agent-sdk fallback
Reported: a user with no provider API key and AGENTMEMORY_AUTO_COMPRESS=false
(which they believed protected them) hit unbounded recursion — Stop hook
POSTs /agentmemory/summarize, handler calls provider.summarize(), agent-sdk
provider spawns @anthropic-ai/claude-agent-sdk query(), which creates a full
CC-style child session that reads ~/.claude/settings.json, registers the
same plugin hooks, and fires its own Stop -> another child -> loop. ~579
ghost 'entrypoint: sdk-ts' sessions accumulated in a few minutes, draining
Claude Pro tokens.

#149 only added a stderr warning. AGENTMEMORY_AUTO_COMPRESS gated /compress
but never /summarize, so users who followed the warning's implied guidance
still got hit. Fix the loop at every layer:

1. config.ts detectProvider
   - Treat empty-string provider keys (ANTHROPIC_API_KEY=) as unset; they
     previously passed the truthiness check identically to a real key.
   - Stop defaulting to agent-sdk. When no key is set, return a 'noop'
     provider config and warn. Agent-sdk fallback now requires an explicit
     AGENTMEMORY_ALLOW_AGENT_SDK=true opt-in with a loud second warning.

2. providers/noop.ts (new) + providers/index.ts
   - NoopProvider implements MemoryProvider and returns empty strings for
     compress and summarize so callers can detect .name === 'noop' and
     short-circuit without spawning anything.
   - Add ProviderType 'noop' and wire it through createBaseProvider.

3. providers/agent-sdk.ts
   - Before spawning query(), check process.env.AGENTMEMORY_SDK_CHILD === '1'
     and return '' instead of recursing. Set the env var to '1' before the
     spawn so any child process (including the Agent SDK session's hooks)
     inherits it.

4. hooks/sdk-guard.ts (new) + all 12 hook scripts
   - Shared isSdkChildContext(payload) checks both AGENTMEMORY_SDK_CHILD=1
     and payload.entrypoint === 'sdk-ts' (CC writes this into the stdin
     jsonl for SDK-spawned sessions). Every hook script now bails early
     when that returns true, so even if one guard layer fails the others
     break the loop.

5. functions/summarize.ts
   - Short-circuit with {success:false, error:'no_provider'} when
     provider.name === 'noop' — never reach .summarize().
   - Treat an empty provider response as empty_provider_response instead
     of trying to parse it.

Tests: 74 files / 819 tests pass (+7 new in stop-hook-recursion-guard.test.ts).
Defense in depth means any ONE of the five layers breaks the loop.
2026-04-22 10:57:57 +01:00
Rohit Ghumare 3bad5430ed fix: SessionStart context gate (#143) + retention-evict semantic leak (#124) (#145)
* fix: stop burning Claude Pro tokens on every tool call (#143)

0.8.8 fixed the agentmemory-side Claude API burn (where the engine
called Claude via the user's ANTHROPIC_API_KEY for per-observation
compression). That addressed #138 for users with API keys, but it
missed the second and much larger token-burn path: the PreToolUse
hook writing context to stdout.

Claude Code reads PreToolUse stdout and prepends it to the model's
next turn. src/hooks/pre-tool-use.ts was POSTing /agentmemory/enrich
on every Edit/Write/Read/Glob/Grep tool call and piping up to 4000
chars of response context into stdout. At ~20 tool calls per user
message this silently injected ~20K tokens per message into Claude
Code's input window — all charged against the user's Claude Pro
allocation because Claude Code was the one sending them to Anthropic.
4 messages drained the cap, which matches @adrianricardo's report.

session-start.ts had the same pattern (injected once per session,
smaller blast radius).

Fix: gate both hooks on AGENTMEMORY_INJECT_CONTEXT, default false.

- pre-tool-use.ts: when disabled, exit immediately — no stdin read,
  no fetch, no stdout write. The hot path (~20x per message) becomes
  a no-op Node startup.
- session-start.ts: when disabled, still POST /agentmemory/session/start
  so the session gets registered for observation tracking, but never
  write context to stdout. The session registration is cheap and
  doesn't touch Claude Code's input window.
- src/config.ts: new isContextInjectionEnabled() helper.
- src/index.ts: startup banner prints 'Context injection: OFF
  (default, #143)' on normal startup and a loud WARNING when
  opt-in is enabled.
- test/context-injection.test.ts: 5 subprocess tests that spawn the
  compiled pre-tool-use.mjs and session-start.mjs hooks, feed real
  JSON payloads via stdin, and assert stdout is empty in all the
  off/default paths. Also asserts the disabled path exits under 1s
  and the opt-in path with an unreachable backend still exits
  cleanly.
- README .env section: new AGENTMEMORY_INJECT_CONTEXT entry.
- CHANGELOG [0.8.10] with prominent 'Behavior change' banner.

Observations are still captured via PostToolUse regardless of the
flag — the memory store and MCP search tools are completely
unaffected by this change. The fix only severs the path where
agentmemory silently shoves memory context into the user's Claude
Code conversation.

Bumps to 0.8.10 (main + @agentmemory/mcp shim). Test count:
724 passing (was 719 + 5 new).

* chore: rewrite #143 CHANGELOG entry with corrected diagnosis

PreToolUse stdout is NOT injected into the model context — per
the Claude Code hook docs, only UserPromptSubmit and SessionStart
stdout are injected. My initial #143 PR description and CHANGELOG
claimed PreToolUse was the smoking gun behind 'Pro allocation
burned in 4 messages', which is wrong.

What's actually true:
- SessionStart stdout injection IS real (~1-2K tokens per session)
- PreToolUse stdout goes to debug log only — no tokens
- Claude Pro's Claude Code quotas are tight by design (Anthropic
  has publicly acknowledged this); 4 messages to burn is plausible
  with or without agentmemory installed

The gate on pre-tool-use.ts is still worth keeping as a resource
cleanup (skips a 20x-per-message Node+HTTP hot path) and as
forward-compat protection in case Claude Code ever changes
PreToolUse hook contract. But the CHANGELOG entry has to stop
claiming it saves tokens when it doesn't.

* fix: mem::retention-evict no longer leaks semantic memories (#124)

The eviction loop was unconditionally calling kv.delete(KV.memories,
id) for every below-threshold candidate, but retention scores are
computed for both episodic (KV.memories) and semantic (KV.semantic)
memories. When a candidate came from KV.semantic, the delete silently
became a no-op (key wasn't in mem:memories to begin with) and the
semantic row stayed alive forever with a sub-threshold score. Semantic
memories could not be evicted by this path at all.

Fix:
- Add a source: "episodic" | "semantic" discriminator to RetentionScore
- Tag it at score creation in both loops of mem::retention-score
- Branch the delete in mem::retention-evict on candidate.source,
  routing to KV.memories or KV.semantic accordingly
- Pre-0.8.10 retention rows with no source field are treated as
  episodic for backwards-compat so upgraded stores continue to evict
  their old rows without re-scoring first
- Response now includes evictedEpisodic and evictedSemantic counts
  so callers can see what was removed from each scope

Adds 3 regression tests to test/retention.test.ts:
- Scoring tags rows with the correct source
- Evicting a mixed set of below-threshold episodic + semantic
  candidates removes both from their respective scopes
- Legacy-shape score rows with no source field still evict
  to mem:memories (backwards-compat)

Full suite: 727 passing (was 724 + 3 new).

* review: probe namespaces for legacy retention rows (#124, round 2)

CodeRabbit caught a real backwards-compat hole in the #124 fix:
pre-0.8.10 stores already contain semantic retention rows with
no source field (because the old mem::retention-score scored
KV.semantic before the discriminator existed). My first fix
defaulted missing source to episodic, which meant those legacy
semantic rows still got delete-routed to KV.memories — the exact
no-op that stranded them in the first place.

Fix: when candidate.source is undefined, probe KV.memories first
for the memoryId; if it's there, route to episodic, otherwise
route to semantic. Count the resolved source in the response.

Adds one new test case: a pre-0.8.10 semantic memory with a
legacy-shape retention row (no source field) gets evicted from
mem:semantic, not silently no-op'd. Existing 'defaults to
episodic' test is kept and retargeted to the genuinely-episodic
legacy case.

Also fixes a README nit: the AGENTMEMORY_INJECT_CONTEXT comment
previously implied SessionStart fires on every tool turn. It's
once per session. Now broken out into two bullets explaining
what each hook does differently, with the note that only
SessionStart actually reaches the model (PreToolUse stdout is
debug-log only per Claude Code docs).

Full suite: 728 passing (was 727 + 1 new).

* review: audit retention evictions + assert persisted source (#124 round 3)

CodeRabbit round 3 findings, both real:

1. retention-evict performs structural deletes (memories / semantic /
   retention scores / access logs) but was not calling recordAudit().
   Repo learnings say state-changing functions must be auditable except
   for read-path bookkeeping. Now emits one batched audit row per
   non-zero eviction sweep:

     operation: 'delete'
     functionId: 'mem::retention-evict'
     targetIds: every evicted memoryId
     details: { threshold, evicted, evictedEpisodic, evictedSemantic,
                reason: 'retention score below threshold' }

   Zero-eviction sweeps intentionally do NOT write an audit row (no
   state change, no need to flood the audit log during health checks).

2. The #124 scoring test only checked result.scores (transient
   response) but not the persisted mem:retention rows. Eviction reads
   back from stored rows, so a regression in kv.set/serialization
   would have still passed the old assertion. Now also does
   kv.get('mem:retention', id) and asserts { source: ... }.

Two new tests:
- Retention evict with a mixed set of 2 episodic + 1 semantic
  candidates writes exactly one audit row with all 3 ids in targetIds
  and the correct evictedEpisodic/Semantic breakdown in details.
- Retention evict with zero candidates writes zero audit rows.

Full suite: 730 passing (was 728 + 2 new).

* review: audit retention-score + parallelize writes (#124 round 4)

CodeRabbit round 3 outside-diff findings, both addressed:

1. mem::retention-score was persisting schema-relevant writes to
   KV.retentionScores (1000+ rows in a mature store) but never called
   recordAudit(). Per the repo audit-coverage policy, state-changing
   functions need an audit row. Added a single batched audit event
   per rescore:

     operation: 'retention_score' (new audit op — added to the
                                    AuditEntry union in types.ts)
     functionId: 'mem::retention-score'
     targetIds: [] (intentionally empty — a mature store can have
                    1000+ ids per sweep; flooding the audit log
                    with every memoryId on every cron tick is
                    worse than recording just the summary counts)
     details: { total, episodic, semantic, tiers, config }

   Zero-memory stores intentionally skip the audit call.

2. The per-memory kv.set inside the score loop was O(n) sequential
   round-trips. Refactored to collect pendingWrites: [id, entry][]
   while iterating, then flush with Promise.all at the end. On a
   mature store with 1000+ memories this is ~10x faster (depends on
   backend pipelining).

Test updates:
- Added 'mem::retention-score emits audit row per rescore' covering
  the new audit call, targetIds=[], and details.episodic/semantic.
- Existing '#124 audit evict' and 'zero-evict skip audit' tests now
  filter the audit log by functionId === 'mem::retention-evict'
  because retention-score also writes one row per sweep now.

Full suite: 731 passing (was 730 + 1 new, existing tests retargeted).
2026-04-15 12:06:43 +01:00
Rohit Ghumare 4c334b0a0e fix: system audit -- 10 bugs found and resolved
1. events.ts: Event triggers were calling api:: functions which
   require ApiRequest shape and auth headers. Rewrote to call
   core functions (kv.set, sdk.trigger) directly, bypassing auth.

2. All 5 hooks: Missing AGENTMEMORY_SECRET auth header. If secret
   was set, every hook would get 401 from the API. Now all hooks
   read AGENTMEMORY_SECRET and send Bearer token.

3. observe.ts: stripPrivateData on JSON string could break JSON
   structure when replacement text differs in length. Added
   try/catch fallback to string coercion.

4. post-tool-use.ts: truncate() for objects did
   JSON.parse(str.slice(0, max-1) + '}') which produces invalid
   JSON in nearly all cases. Changed to return truncated string.

5. compress.ts: LLM-returned importance was not clamped to 1-10
   range. Added Math.max(1, Math.min(10, ...)) with NaN fallback.

6. compress.ts: LLM-returned observation type was not validated
   against ObservationType union. Invalid types now fall back to
   "other".

7. context.ts: Token estimate for observation blocks only counted
   inner content, not the "## Session..." header. Fixed to estimate
   the full block text.

8. viewer: WebSocket port now configurable via ?wsPort= query param
   for non-default III_STREAMS_PORT configurations.

9. plugin/scripts: Rebuilt with auth header support matching the
   updated hook source files.
2026-02-27 11:33:48 +05:30
Rohit Ghumare 6df02d3e20 add plugin marketplace install support
- Add .claude-plugin/marketplace.json for /plugin marketplace add
- Build hook scripts into plugin/scripts/ (self-contained)
- Fix hooks.json to use ${CLAUDE_PLUGIN_ROOT} paths
- Update README with plugin install as primary quick start
2026-02-27 11:33:48 +05:30