@agentmemory/agentmemory 0.9.28 → 0.9.30

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.
Files changed (158) hide show
  1. package/.env.example +98 -28
  2. package/AGENTS.md +19 -26
  3. package/README.md +619 -274
  4. package/dist/.env.example +98 -28
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +1328 -244
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{connect-JnsJXc68.mjs → connect-B4_ca6BD.mjs} +619 -70
  9. package/dist/connect-B4_ca6BD.mjs.map +1 -0
  10. package/dist/docker-compose.yml +42 -16
  11. package/dist/hooks/_capture.d.mts +22 -0
  12. package/dist/hooks/_capture.d.mts.map +1 -0
  13. package/dist/hooks/_capture.mjs +908 -0
  14. package/dist/hooks/_capture.mjs.map +1 -0
  15. package/dist/hooks/antigravity-bridge.d.mts +8 -0
  16. package/dist/hooks/antigravity-bridge.d.mts.map +1 -0
  17. package/dist/hooks/antigravity-bridge.mjs +123 -0
  18. package/dist/hooks/antigravity-bridge.mjs.map +1 -0
  19. package/dist/hooks/notification.mjs +34 -27
  20. package/dist/hooks/notification.mjs.map +1 -1
  21. package/dist/hooks/post-commit.mjs +84 -2
  22. package/dist/hooks/post-commit.mjs.map +1 -1
  23. package/dist/hooks/post-tool-failure.mjs +71 -26
  24. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  25. package/dist/hooks/post-tool-use.mjs +99 -36
  26. package/dist/hooks/post-tool-use.mjs.map +1 -1
  27. package/dist/hooks/pre-compact.mjs +95 -5
  28. package/dist/hooks/pre-compact.mjs.map +1 -1
  29. package/dist/hooks/pre-tool-use.mjs +72 -2
  30. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  31. package/dist/hooks/prompt-submit.mjs +25 -22
  32. package/dist/hooks/prompt-submit.mjs.map +1 -1
  33. package/dist/hooks/session-end.mjs +93 -24
  34. package/dist/hooks/session-end.mjs.map +1 -1
  35. package/dist/hooks/session-start.mjs +101 -6
  36. package/dist/hooks/session-start.mjs.map +1 -1
  37. package/dist/hooks/stop.mjs +72 -8
  38. package/dist/hooks/stop.mjs.map +1 -1
  39. package/dist/hooks/subagent-start.mjs +28 -26
  40. package/dist/hooks/subagent-start.mjs.map +1 -1
  41. package/dist/hooks/subagent-stop.mjs +29 -26
  42. package/dist/hooks/subagent-stop.mjs.map +1 -1
  43. package/dist/hooks/task-completed.mjs +31 -28
  44. package/dist/hooks/task-completed.mjs.map +1 -1
  45. package/dist/iii-config.docker.yaml +10 -10
  46. package/dist/iii-config.yaml +3 -1
  47. package/dist/{image-refs-BKLmp_sD.mjs → image-refs-BaT-9oMU.mjs} +2 -2
  48. package/dist/image-refs-BaT-9oMU.mjs.map +1 -0
  49. package/dist/{image-refs-C2YU7BIm.mjs → image-refs-T_urbwHM.mjs} +3 -3
  50. package/dist/image-refs-T_urbwHM.mjs.map +1 -0
  51. package/dist/{keyed-mutex-DTHtcZrt.mjs → image-store-BsN4vyjr.mjs} +2 -14
  52. package/dist/image-store-BsN4vyjr.mjs.map +1 -0
  53. package/dist/index.mjs +14021 -6494
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/keyed-mutex-DchXsyfP.mjs +221 -0
  56. package/dist/keyed-mutex-DchXsyfP.mjs.map +1 -0
  57. package/dist/secret-store-BnY3A50C.mjs +2348 -0
  58. package/dist/secret-store-BnY3A50C.mjs.map +1 -0
  59. package/dist/{src-CzgoepGU.mjs → src-Jsq9LCeH.mjs} +7349 -2940
  60. package/dist/src-Jsq9LCeH.mjs.map +1 -0
  61. package/dist/{standalone-YsZFFQBM.mjs → standalone-DKCdrf1d.mjs} +62 -24
  62. package/dist/standalone-DKCdrf1d.mjs.map +1 -0
  63. package/dist/standalone.d.mts.map +1 -1
  64. package/dist/standalone.mjs +298 -27
  65. package/dist/standalone.mjs.map +1 -1
  66. package/dist/status-IRz7ta8-.mjs +2344 -0
  67. package/dist/status-IRz7ta8-.mjs.map +1 -0
  68. package/dist/viewer/favicon.svg +35 -1
  69. package/dist/viewer/index.html +7428 -2608
  70. package/docker-compose.yml +42 -16
  71. package/iii-config.docker.yaml +10 -10
  72. package/iii-config.yaml +3 -1
  73. package/integrations/pi/README.md +77 -0
  74. package/integrations/pi/index.ts +402 -0
  75. package/integrations/pi/package.json +32 -0
  76. package/integrations/pi/security.ts +87 -0
  77. package/package.json +15 -7
  78. package/plugin/.claude-plugin/plugin.json +2 -2
  79. package/plugin/.codex-plugin/plugin.json +26 -4
  80. package/plugin/.devin-plugin/plugin.json +32 -0
  81. package/plugin/.mcp.codex.json +12 -0
  82. package/plugin/.mcp.copilot.json +2 -7
  83. package/plugin/.mcp.json +2 -7
  84. package/plugin/assets/icon.svg +35 -0
  85. package/plugin/cursor/hooks.json +41 -0
  86. package/plugin/cursor/mcp.json +12 -0
  87. package/plugin/hooks/hooks.antigravity.json +43 -0
  88. package/plugin/hooks/hooks.copilot.json +11 -11
  89. package/plugin/hooks/hooks.devin.json +67 -0
  90. package/plugin/hooks/hooks.droid.json +55 -0
  91. package/plugin/opencode/README.md +267 -31
  92. package/plugin/opencode/agentmemory-capture.ts +644 -15
  93. package/plugin/opencode/plugin.json +1 -1
  94. package/plugin/plugin.json +2 -2
  95. package/plugin/scripts/_capture.d.mts +22 -0
  96. package/plugin/scripts/_capture.d.mts.map +1 -0
  97. package/plugin/scripts/_capture.mjs +908 -0
  98. package/plugin/scripts/_capture.mjs.map +1 -0
  99. package/plugin/scripts/antigravity-bridge.d.mts +8 -0
  100. package/plugin/scripts/antigravity-bridge.d.mts.map +1 -0
  101. package/plugin/scripts/antigravity-bridge.mjs +123 -0
  102. package/plugin/scripts/antigravity-bridge.mjs.map +1 -0
  103. package/plugin/scripts/notification.mjs +34 -27
  104. package/plugin/scripts/notification.mjs.map +1 -1
  105. package/plugin/scripts/plugin-bridge.mjs +378 -0
  106. package/plugin/scripts/post-commit.mjs +84 -2
  107. package/plugin/scripts/post-commit.mjs.map +1 -1
  108. package/plugin/scripts/post-tool-failure.mjs +71 -26
  109. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  110. package/plugin/scripts/post-tool-use.mjs +99 -36
  111. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  112. package/plugin/scripts/pre-compact.mjs +95 -5
  113. package/plugin/scripts/pre-compact.mjs.map +1 -1
  114. package/plugin/scripts/pre-tool-use.mjs +72 -2
  115. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  116. package/plugin/scripts/prompt-submit.mjs +25 -22
  117. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  118. package/plugin/scripts/session-end.mjs +93 -24
  119. package/plugin/scripts/session-end.mjs.map +1 -1
  120. package/plugin/scripts/session-start.mjs +101 -6
  121. package/plugin/scripts/session-start.mjs.map +1 -1
  122. package/plugin/scripts/stop.mjs +72 -8
  123. package/plugin/scripts/stop.mjs.map +1 -1
  124. package/plugin/scripts/subagent-start.mjs +28 -26
  125. package/plugin/scripts/subagent-start.mjs.map +1 -1
  126. package/plugin/scripts/subagent-stop.mjs +29 -26
  127. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  128. package/plugin/scripts/task-completed.mjs +31 -28
  129. package/plugin/scripts/task-completed.mjs.map +1 -1
  130. package/plugin/skills/_shared/TROUBLESHOOTING.md +22 -6
  131. package/plugin/skills/agentmemory-agents/REFERENCE.md +8 -5
  132. package/plugin/skills/agentmemory-config/REFERENCE.md +52 -1
  133. package/plugin/skills/agentmemory-config/SKILL.md +44 -7
  134. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +4 -3
  135. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +23 -2
  136. package/plugin/skills/forget/SKILL.md +4 -1
  137. package/plugin/skills/handoff/SKILL.md +4 -2
  138. package/plugin/skills/lesson/SKILL.md +64 -0
  139. package/plugin/skills/memory-discipline/SKILL.md +65 -0
  140. package/plugin/skills/recall/SKILL.md +4 -1
  141. package/plugin/skills/remember/SKILL.md +6 -1
  142. package/dist/config-ChC9EtgV.mjs +0 -259
  143. package/dist/config-ChC9EtgV.mjs.map +0 -1
  144. package/dist/connect-JnsJXc68.mjs.map +0 -1
  145. package/dist/image-refs-BKLmp_sD.mjs.map +0 -1
  146. package/dist/image-refs-C2YU7BIm.mjs.map +0 -1
  147. package/dist/keyed-mutex-DTHtcZrt.mjs.map +0 -1
  148. package/dist/logger-BEB7pCI9.mjs +0 -43
  149. package/dist/logger-BEB7pCI9.mjs.map +0 -1
  150. package/dist/schema-CNiCWzV9.mjs +0 -78
  151. package/dist/schema-CNiCWzV9.mjs.map +0 -1
  152. package/dist/src-CzgoepGU.mjs.map +0 -1
  153. package/dist/standalone-YsZFFQBM.mjs.map +0 -1
  154. package/dist/tools-registry-D3NEoe5m.mjs +0 -1115
  155. package/dist/tools-registry-D3NEoe5m.mjs.map +0 -1
  156. package/plugin/scripts/diagnostics.d.mts +0 -17
  157. package/plugin/scripts/diagnostics.d.mts.map +0 -1
  158. package/plugin/scripts/diagnostics.mjs.map +0 -1
@@ -8,34 +8,60 @@ services:
8
8
  iii-init:
9
9
  image: busybox:1.36
10
10
  user: "0:0"
11
+ environment:
12
+ AGENTMEMORY_DOCKER_UID: "${AGENTMEMORY_DOCKER_UID:-65532}"
13
+ AGENTMEMORY_DOCKER_GID: "${AGENTMEMORY_DOCKER_GID:-65532}"
14
+ AGENTMEMORY_DOCKER_SKIP_CHOWN: "${AGENTMEMORY_DOCKER_SKIP_CHOWN:-0}"
11
15
  volumes:
12
- - iii-data:/data
13
- entrypoint: ["sh", "-c", "chown -R 65532:65532 /data && chmod 755 /data"]
16
+ - "${AGENTMEMORY_DATA_DIR:-iii-data}:/data"
17
+ entrypoint: ["sh", "-c", "if [ \"$${AGENTMEMORY_DOCKER_SKIP_CHOWN}\" = \"1\" ]; then exit 0; fi; chown -R $${AGENTMEMORY_DOCKER_UID}:$${AGENTMEMORY_DOCKER_GID} /data && chmod 755 /data"]
14
18
  restart: "no"
15
19
 
16
20
  iii-engine:
17
- # Pinned to v0.11.2 — the last engine that runs agentmemory's current
18
- # worker model cleanly. v0.11.6 introduces a new sandbox-everything-
19
- # via-`iii worker add` model that agentmemory hasn't been refactored
20
- # for yet; the architectural mismatch surfaces as EPIPE reconnect
21
- # loops and empty search after save. Bump only after agentmemory is
22
- # refactored to register as a sandboxed worker.
21
+ # Pinned to the same release as agentmemory's iii-sdk dependency
22
+ # (package.json). The worker speaks this engine's wire protocol, so
23
+ # bump the image tag and the SDK together, never one of them alone.
24
+ # 0.22.x is the last line before 0.23 removes the config.yaml worker
25
+ # lifecycle; it keeps HTTP routes owned by the reconnecting worker.
23
26
  #
24
27
  # Override per-shell or via .env file:
25
- # AGENTMEMORY_III_VERSION=0.11.7 docker compose up
26
- image: iiidev/iii:${AGENTMEMORY_III_VERSION:-0.11.2}
27
- user: "65532:65532"
28
+ # AGENTMEMORY_III_VERSION=0.22.1 docker compose up
29
+ image: iiidev/iii:${AGENTMEMORY_III_VERSION:-0.22.1}
30
+ user: "${AGENTMEMORY_DOCKER_UID:-65532}:${AGENTMEMORY_DOCKER_GID:-65532}"
28
31
  depends_on:
29
32
  iii-init:
30
33
  condition: service_completed_successfully
34
+ command: ["--config", "/app/config.yaml", "--no-update-check"]
35
+ environment:
36
+ III_TELEMETRY_ENABLED: "${III_TELEMETRY_ENABLED:-false}"
37
+ III_REST_PORT: "${III_REST_PORT:-3111}"
38
+ III_STREAM_PORT: "${III_STREAM_PORT:-3112}"
39
+ III_VIEWER_PORT: "${III_VIEWER_PORT:-3113}"
40
+ III_ENGINE_PORT: "${III_ENGINE_PORT:-49134}"
41
+ AGENTMEMORY_REDIS_URL: "${AGENTMEMORY_REDIS_URL:-}"
42
+ AGENTMEMORY_STATE_SAVE_INTERVAL_MS: "${AGENTMEMORY_STATE_SAVE_INTERVAL_MS:-2000}"
43
+ MALLOC_ARENA_MAX: "${MALLOC_ARENA_MAX:-2}"
44
+ ulimits:
45
+ nofile:
46
+ soft: 10240
47
+ hard: 10240
31
48
  ports:
32
- - "127.0.0.1:49134:49134"
33
- - "127.0.0.1:3111:3111"
34
- - "127.0.0.1:3112:3112"
35
- - "127.0.0.1:9464:9464"
49
+ - "127.0.0.1:${III_ENGINE_PORT:-49134}:${III_ENGINE_PORT:-49134}"
50
+ - "127.0.0.1:${III_REST_PORT:-3111}:${III_REST_PORT:-3111}"
51
+ - "127.0.0.1:${III_STREAM_PORT:-3112}:${III_STREAM_PORT:-3112}"
52
+ - "127.0.0.1:${AGENTMEMORY_METRICS_PORT:-9464}:9464"
36
53
  volumes:
37
- - iii-data:/data
54
+ - "${AGENTMEMORY_DATA_DIR:-iii-data}:/data"
38
55
  - ./iii-config.docker.yaml:/app/config.yaml:ro
56
+ # The engine persists each builtin worker's seeded config block under
57
+ # config/ next to its config file (/app/config in the image) on first
58
+ # boot and treats that copy as the source of truth afterwards. Keeping
59
+ # it on a tmpfs means every container start re-seeds from config.yaml
60
+ # and the III_* env vars, the same behavior the native CLI path has.
61
+ - type: tmpfs
62
+ target: /app/config
63
+ tmpfs:
64
+ mode: 01777
39
65
  restart: unless-stopped
40
66
  logging:
41
67
  driver: json-file
@@ -1,18 +1,23 @@
1
1
  workers:
2
2
  - name: iii-http
3
3
  config:
4
- port: 3111
4
+ port: ${III_REST_PORT:3111}
5
5
  host: 0.0.0.0
6
6
  default_timeout: 180000
7
7
  cors:
8
- allowed_origins: ["http://localhost:3111", "http://localhost:3113", "http://127.0.0.1:3111", "http://127.0.0.1:3113"]
8
+ allowed_origins: ["http://localhost:${III_REST_PORT:3111}", "http://localhost:${III_VIEWER_PORT:3113}", "http://127.0.0.1:${III_REST_PORT:3111}", "http://127.0.0.1:${III_VIEWER_PORT:3113}"]
9
9
  allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]
10
+ - name: iii-worker-manager
11
+ config:
12
+ port: ${III_ENGINE_PORT:49134}
13
+ host: 0.0.0.0
10
14
  - name: iii-state
11
15
  config:
12
16
  adapter:
13
17
  name: kv
14
18
  config:
15
19
  store_method: file_based
20
+ save_interval_ms: ${AGENTMEMORY_STATE_SAVE_INTERVAL_MS:2000}
16
21
  file_path: /data/state_store.db
17
22
  - name: iii-queue
18
23
  config:
@@ -28,26 +33,21 @@ workers:
28
33
  name: kv
29
34
  - name: iii-stream
30
35
  config:
31
- port: 3112
36
+ port: ${III_STREAM_PORT:3112}
32
37
  host: 0.0.0.0
33
38
  adapter:
34
39
  name: kv
35
40
  config:
36
41
  store_method: file_based
42
+ save_interval_ms: ${AGENTMEMORY_STATE_SAVE_INTERVAL_MS:2000}
37
43
  file_path: /data/stream_store
38
44
  - name: iii-observability
39
45
  config:
40
46
  enabled: true
41
47
  service_name: agentmemory
42
48
  exporter: memory
43
- # See iii-config.yaml for the rationale on 0.1 / console-off (#519).
49
+ # See iii-config.yaml for the rationale on 0.1 / console-off.
44
50
  sampling_ratio: 0.1
45
51
  metrics_enabled: true
46
52
  logs_enabled: true
47
53
  logs_console_output: false
48
- - name: iii-exec
49
- config:
50
- watch:
51
- - src/**/*.ts
52
- exec:
53
- - node dist/index.mjs
package/iii-config.yaml CHANGED
@@ -13,6 +13,7 @@ workers:
13
13
  name: kv
14
14
  config:
15
15
  store_method: file_based
16
+ save_interval_ms: 2000
16
17
  file_path: ./data/state_store.db
17
18
  - name: iii-queue
18
19
  config:
@@ -34,6 +35,7 @@ workers:
34
35
  name: kv
35
36
  config:
36
37
  store_method: file_based
38
+ save_interval_ms: 2000
37
39
  file_path: ./data/stream_store
38
40
  - name: iii-observability
39
41
  config:
@@ -45,7 +47,7 @@ workers:
45
47
  # subscriber lagged" WARN, and that WARN re-enters the same
46
48
  # stream the subscriber is failing to drain — a positive
47
49
  # feedback loop that wrote 137 GB to daemon.log.new on one user
48
- # in a few days (#519).
50
+ # in a few days.
49
51
  sampling_ratio: 0.1
50
52
  metrics_enabled: true
51
53
  logs_enabled: true
@@ -0,0 +1,77 @@
1
+ <p align="center">
2
+ <img src="../../assets/banner.png" alt="agentmemory" width="640" />
3
+ </p>
4
+
5
+ <h1 align="center">
6
+ &nbsp;agentmemory for pi
7
+ </h1>
8
+
9
+ <p align="center">
10
+ <strong>Your pi sessions remember everything. No more re-explaining.</strong><br/>
11
+ <sub>Persistent cross-session memory via <a href="https://github.com/rohitg00/agentmemory">agentmemory</a> — shared with Claude Code, Codex CLI, Gemini CLI, Hermes, OpenClaw, and more.</sub>
12
+ </p>
13
+
14
+ ---
15
+
16
+ ## Quick setup
17
+
18
+ Start the agentmemory server in a separate terminal:
19
+
20
+ ```bash
21
+ npx @agentmemory/agentmemory
22
+ ```
23
+
24
+ Copy this folder into pi's global extensions directory:
25
+
26
+ ```bash
27
+ mkdir -p ~/.pi/agent/extensions/agentmemory
28
+ cp integrations/pi/index.ts ~/.pi/agent/extensions/agentmemory/index.ts
29
+ ```
30
+
31
+ Then enable it in `~/.pi/agent/settings.json` if you prefer explicit loading:
32
+
33
+ ```json
34
+ {
35
+ "extensions": ["~/.pi/agent/extensions/agentmemory"]
36
+ }
37
+ ```
38
+
39
+ If you place it under `~/.pi/agent/extensions/agentmemory/`, pi will also auto-discover it and `/reload` can hot-reload it.
40
+
41
+ ## What it adds
42
+
43
+ - `memory_health` — confirm the shared memory server is reachable
44
+ - `memory_search` — search prior decisions, bugs, workflows, and preferences
45
+ - `memory_save` — write durable facts back to long-term memory
46
+ - `/agentmemory-status` — check health from inside pi
47
+ - `before_agent_start` recall — injects relevant memories into the prompt
48
+ - `agent_end` capture — saves completed conversation turns back to agentmemory
49
+
50
+ ## Environment variables
51
+
52
+ | Variable | Default | Description |
53
+ |---|---|---|
54
+ | `AGENTMEMORY_URL` | `http://localhost:3111` | agentmemory server URL |
55
+ | `AGENTMEMORY_SECRET` | (none) | Bearer token for protected instances |
56
+ | `AGENTMEMORY_REQUIRE_HTTPS` | (off) | When set to `1`, refuse to send a bearer token over plaintext HTTP to a non-loopback host. Sends the token only when `AGENTMEMORY_URL` is `https://...` or points at `localhost`/`127.0.0.1`/`::1`. With this off, the plugin warns once but still sends. |
57
+
58
+ ## Smoke test
59
+
60
+ Run pi and ask it to use the `memory_health` tool, or call the command directly:
61
+
62
+ ```text
63
+ /agentmemory-status
64
+ ```
65
+
66
+ You should see `agentmemory healthy` and a footer status like `🧠 agentmemory`.
67
+
68
+ ## Notes
69
+
70
+ - This extension uses pi's extension API, not MCP, so it can hook directly into the agent lifecycle.
71
+ - One local agentmemory server can be shared across pi, pi2, Hermes, OpenClaw, Claude Code, Codex CLI, and Gemini CLI.
72
+
73
+ ## See also
74
+
75
+ - [agentmemory main README](../../README.md)
76
+ - [Hermes integration](../hermes/README.md)
77
+ - [OpenClaw integration](../openclaw/README.md)
@@ -0,0 +1,402 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { execFileSync } from "node:child_process";
6
+ import { createPlaintextBearerAuthGuard, resolveSecret } from "./security.js";
7
+
8
+ type TextBlock = { type?: string; text?: string };
9
+ type AssistantMessage = { role?: string; content?: unknown };
10
+ type SmartSearchResult = {
11
+ title?: string;
12
+ narrative?: string;
13
+ type?: string;
14
+ combinedScore?: number;
15
+ score?: number;
16
+ observation?: {
17
+ title?: string;
18
+ narrative?: string;
19
+ type?: string;
20
+ };
21
+ };
22
+
23
+ type HealthResponse = {
24
+ status?: string;
25
+ service?: string;
26
+ version?: string;
27
+ health?: {
28
+ status?: string;
29
+ notes?: string[];
30
+ };
31
+ };
32
+
33
+ const DEFAULT_URL = process.env.AGENTMEMORY_URL || "http://localhost:3111";
34
+ const guardPlaintextBearerAuth = createPlaintextBearerAuthGuard();
35
+ const TOOL_GUIDANCE = [
36
+ "agentmemory is available for cross-session memory.",
37
+ "Use memory_search to recall prior decisions, preferences, bugs, and workflows.",
38
+ "Use memory_save when you discover durable facts worth remembering beyond this session.",
39
+ ].join(" ");
40
+
41
+ function normalizeBaseUrl(url: string): string {
42
+ return url.replace(/\/+$/, "");
43
+ }
44
+
45
+ function getText(content: unknown): string {
46
+ if (typeof content === "string") return content;
47
+ if (!Array.isArray(content)) return "";
48
+ return content
49
+ .flatMap((part) => {
50
+ if (!part || typeof part !== "object") return [] as string[];
51
+ const block = part as TextBlock;
52
+ if (block.type === "text" && typeof block.text === "string") return [block.text];
53
+ return [] as string[];
54
+ })
55
+ .join("\n")
56
+ .trim();
57
+ }
58
+
59
+ function getLastAssistantText(messages: unknown[]): string {
60
+ for (const msg of [...messages].reverse()) {
61
+ if (!msg || typeof msg !== "object") continue;
62
+ const assistant = msg as AssistantMessage;
63
+ if (assistant.role !== "assistant") continue;
64
+ const text = getText(assistant.content);
65
+ if (text) return text;
66
+ }
67
+ return "";
68
+ }
69
+
70
+ function formatSearchResults(results: SmartSearchResult[]): string {
71
+ if (!results.length) return "No relevant memories found.";
72
+ return results
73
+ .slice(0, 5)
74
+ .map((result, index) => {
75
+ const obs = result.observation ?? result;
76
+ const title = obs.title?.trim() || `Memory ${index + 1}`;
77
+ const narrative = obs.narrative?.trim() || "";
78
+ const type = obs.type?.trim() || "memory";
79
+ const score = result.combinedScore ?? result.score;
80
+ const scoreText = typeof score === "number" ? ` [score=${score.toFixed(3)}]` : "";
81
+ return `- ${title} (${type})${scoreText}${narrative ? `: ${narrative}` : ""}`;
82
+ })
83
+ .join("\n");
84
+ }
85
+
86
+ async function callAgentMemory<T>(
87
+ pathname: string,
88
+ options?: {
89
+ method?: "GET" | "POST";
90
+ body?: unknown;
91
+ baseUrl?: string;
92
+ timeoutMs?: number;
93
+ },
94
+ ): Promise<T | null> {
95
+ const baseUrl = normalizeBaseUrl(options?.baseUrl || process.env.AGENTMEMORY_URL || DEFAULT_URL);
96
+ const method = options?.method || "POST";
97
+ const url = `${baseUrl}/agentmemory/${pathname.replace(/^\/+/, "")}`;
98
+ const headers: Record<string, string> = {};
99
+ const secret = resolveSecret(baseUrl, process.env.AGENTMEMORY_SECRET);
100
+ guardPlaintextBearerAuth(baseUrl, secret);
101
+ if (options?.body !== undefined) headers["Content-Type"] = "application/json";
102
+ if (secret) headers.Authorization = `Bearer ${secret}`;
103
+
104
+ try {
105
+ const response = await fetch(url, {
106
+ method,
107
+ headers,
108
+ body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
109
+ signal: options?.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
110
+ });
111
+ if (!response.ok) return null;
112
+ return (await response.json()) as T;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ export default function agentmemoryExtension(pi: ExtensionAPI) {
119
+ if (process.env.AGENTMEMORY_REQUIRE_HTTPS === "1") {
120
+ guardPlaintextBearerAuth(
121
+ normalizeBaseUrl(process.env.AGENTMEMORY_URL || DEFAULT_URL),
122
+ process.env.AGENTMEMORY_SECRET,
123
+ );
124
+ }
125
+ let sessionId = `ephemeral-${crypto.randomUUID().slice(0, 8)}`;
126
+ // Canonical project scope, matching the hooks' resolveProject order (env
127
+ // override, git toplevel basename, cwd basename) so Pi sessions share a
128
+ // project bucket with every other agent instead of scoping on a raw path.
129
+ const projectCache = new Map<string, string>();
130
+ function resolveProjectName(dir: string): string {
131
+ const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]?.trim();
132
+ if (explicit) return explicit;
133
+ const cached = projectCache.get(dir);
134
+ if (cached) return cached;
135
+ let name = path.basename(dir) || dir;
136
+ try {
137
+ const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
138
+ cwd: dir,
139
+ stdio: ["ignore", "pipe", "ignore"],
140
+ encoding: "utf8",
141
+ }).trim();
142
+ if (top) name = path.basename(top);
143
+ } catch {
144
+ // not a git repo
145
+ }
146
+ projectCache.set(dir, name);
147
+ return name;
148
+ }
149
+ let currentCwd = process.cwd();
150
+ let currentProject = resolveProjectName(currentCwd);
151
+ let lastPrompt = "";
152
+ let lastHealthOk = false;
153
+
154
+ const toolObserveEnabled = process.env.AGENTMEMORY_TOOL_OBSERVE !== "0";
155
+
156
+ // Skips the round-trip when an auto-retry re-submits an identical prompt.
157
+ const DEDUP_WINDOW_MS = 5 * 60 * 1000;
158
+ const recentHashes = new Map<string, number>();
159
+ function isDuplicate(data: string): boolean {
160
+ const hash = crypto.createHash("sha256").update(data).digest("hex");
161
+ const now = Date.now();
162
+ const prev = recentHashes.get(hash);
163
+ if (prev !== undefined && now - prev < DEDUP_WINDOW_MS) return true;
164
+ if (recentHashes.size > 500) {
165
+ for (const [key, ts] of recentHashes) {
166
+ if (now - ts >= DEDUP_WINDOW_MS) recentHashes.delete(key);
167
+ }
168
+ }
169
+ recentHashes.set(hash, now);
170
+ return false;
171
+ }
172
+
173
+ async function getHealth() {
174
+ return await callAgentMemory<HealthResponse>("health", { method: "GET" });
175
+ }
176
+
177
+ async function refreshStatus(ctx: { ui: { setStatus: (key: string, text: string) => void } }) {
178
+ // Bind before the await: ctx goes stale if the session is replaced.
179
+ let setStatus: (key: string, text: string) => void;
180
+ try {
181
+ const ui = ctx.ui;
182
+ setStatus = ui.setStatus.bind(ui);
183
+ } catch {
184
+ return;
185
+ }
186
+ const health = await getHealth();
187
+ lastHealthOk =
188
+ !!health &&
189
+ (health.status === "ok" ||
190
+ health.status === "healthy" ||
191
+ health.health?.status === "healthy");
192
+ try {
193
+ setStatus("agentmemory", lastHealthOk ? "🧠 agentmemory" : "🧠 agentmemory off");
194
+ } catch {
195
+ // status is best-effort
196
+ }
197
+ }
198
+
199
+ pi.registerCommand("agentmemory-status", {
200
+ description: "Check local agentmemory server health",
201
+ handler: async (_args, ctx) => {
202
+ const health = await getHealth();
203
+ if (!health) {
204
+ ctx.ui.notify("agentmemory is unreachable at http://localhost:3111", "warning");
205
+ return;
206
+ }
207
+ ctx.ui.notify(
208
+ `agentmemory ${health.status || health.health?.status || "unknown"}${health.version ? ` v${health.version}` : ""}`,
209
+ "info",
210
+ );
211
+ },
212
+ });
213
+
214
+ pi.registerTool({
215
+ name: "memory_health",
216
+ label: "Memory Health",
217
+ description: "Check whether the local agentmemory server is reachable and healthy",
218
+ parameters: Type.Object({}),
219
+ async execute() {
220
+ const health = await getHealth();
221
+ if (!health) {
222
+ return {
223
+ content: [{ type: "text", text: "agentmemory is unreachable at http://localhost:3111" }],
224
+ details: { ok: false },
225
+ };
226
+ }
227
+ return {
228
+ content: [
229
+ {
230
+ type: "text",
231
+ text: `agentmemory status: ${health.status || health.health?.status || "unknown"}${health.version ? ` (v${health.version})` : ""}`,
232
+ },
233
+ ],
234
+ details: health,
235
+ };
236
+ },
237
+ });
238
+
239
+ pi.registerTool({
240
+ name: "memory_search",
241
+ label: "Memory Search",
242
+ description: "Search agentmemory for cross-session project memory, prior decisions, bugs, and user preferences",
243
+ parameters: Type.Object({
244
+ query: Type.String({ description: "What to search for in memory" }),
245
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10, default: 5, description: "Maximum results" })),
246
+ }),
247
+ async execute(_toolCallId, params) {
248
+ const result = await callAgentMemory<{ results?: SmartSearchResult[] }>("smart-search", {
249
+ body: { query: params.query, limit: params.limit ?? 5, project: currentProject },
250
+ });
251
+ const results = result?.results || [];
252
+ return {
253
+ content: [{ type: "text", text: formatSearchResults(results) }],
254
+ details: { query: params.query, results },
255
+ };
256
+ },
257
+ });
258
+
259
+ pi.registerTool({
260
+ name: "memory_save",
261
+ label: "Memory Save",
262
+ description: "Save a durable fact, convention, workflow, preference, or bug fix into agentmemory",
263
+ parameters: Type.Object({
264
+ content: Type.String({ description: "What should be remembered" }),
265
+ type: Type.Optional(
266
+ Type.String({
267
+ description: "Memory type",
268
+ default: "fact",
269
+ }),
270
+ ),
271
+ }),
272
+ async execute(_toolCallId, params) {
273
+ const result = await callAgentMemory<Record<string, unknown>>("remember", {
274
+ body: { content: params.content, type: params.type || "fact", project: currentProject },
275
+ });
276
+ if (!result) {
277
+ return {
278
+ content: [{ type: "text", text: "Failed to save memory to agentmemory." }],
279
+ details: { ok: false },
280
+ };
281
+ }
282
+ return {
283
+ content: [{ type: "text", text: `Saved memory (${params.type || "fact"}): ${params.content}` }],
284
+ details: result,
285
+ };
286
+ },
287
+ });
288
+
289
+ pi.on("session_start", async (_event, ctx) => {
290
+ const sessionFile = ctx.sessionManager.getSessionFile();
291
+ sessionId = sessionFile ? path.basename(sessionFile).replace(/\.[^.]+$/, "") : `ephemeral-${crypto.randomUUID().slice(0, 8)}`;
292
+ currentCwd = process.cwd();
293
+ currentProject = resolveProjectName(currentCwd);
294
+ await refreshStatus(ctx);
295
+ // After refreshStatus: that is where lastHealthOk is first populated.
296
+ if (lastHealthOk) {
297
+ await callAgentMemory("session/start", {
298
+ body: { sessionId, project: currentProject, cwd: currentCwd },
299
+ });
300
+ }
301
+ });
302
+
303
+ pi.on("before_agent_start", async (event, ctx) => {
304
+ currentCwd = event.systemPromptOptions.cwd || process.cwd();
305
+ currentProject = resolveProjectName(currentCwd);
306
+ lastPrompt = event.prompt?.trim() || "";
307
+ if (!lastPrompt) return;
308
+
309
+ if (lastHealthOk && !isDuplicate(`prompt_submit:${sessionId}:${lastPrompt}`)) {
310
+ void callAgentMemory("observe", {
311
+ body: {
312
+ hookType: "prompt_submit",
313
+ sessionId,
314
+ project: currentProject,
315
+ cwd: currentCwd,
316
+ timestamp: new Date().toISOString(),
317
+ data: { prompt: lastPrompt },
318
+ },
319
+ });
320
+ }
321
+
322
+ const result = await callAgentMemory<{ results?: SmartSearchResult[] }>("smart-search", {
323
+ body: { query: lastPrompt, limit: 5, project: currentProject },
324
+ });
325
+ const results = result?.results || [];
326
+ const recallBlock = results.length
327
+ ? [
328
+ "Relevant long-term memory from agentmemory:",
329
+ formatSearchResults(results),
330
+ ].join("\n")
331
+ : "";
332
+
333
+ await refreshStatus(ctx);
334
+ return {
335
+ systemPrompt: [event.systemPrompt, TOOL_GUIDANCE, recallBlock].filter(Boolean).join("\n\n"),
336
+ };
337
+ });
338
+
339
+ pi.on("tool_result", (event) => {
340
+ if (!toolObserveEnabled || !lastHealthOk || !sessionId) return;
341
+ const toolName = event.toolName;
342
+ if (!toolName) return;
343
+ let input = "";
344
+ try {
345
+ input = typeof event.input === "string" ? event.input : JSON.stringify(event.input ?? {});
346
+ } catch {
347
+ // non-serializable
348
+ }
349
+ let output = "";
350
+ try {
351
+ output = typeof event.content === "string" ? event.content : JSON.stringify(event.content ?? "");
352
+ } catch {
353
+ // non-serializable
354
+ }
355
+ void callAgentMemory("observe", {
356
+ body: {
357
+ hookType: "post_tool_use",
358
+ sessionId,
359
+ project: currentProject,
360
+ cwd: currentCwd,
361
+ timestamp: new Date().toISOString(),
362
+ data: {
363
+ tool_name: toolName,
364
+ tool_input: input.slice(0, 8000),
365
+ tool_output: output.slice(0, 8000),
366
+ ...(event.isError ? { tool_error: true } : {}),
367
+ },
368
+ },
369
+ });
370
+ });
371
+
372
+ pi.on("agent_end", async (event) => {
373
+ if (!lastHealthOk || !lastPrompt) return;
374
+ const assistantText = getLastAssistantText(event.messages as unknown[]);
375
+ if (!assistantText) return;
376
+ void callAgentMemory("observe", {
377
+ body: {
378
+ hookType: "post_tool_use",
379
+ sessionId,
380
+ project: currentProject,
381
+ cwd: currentCwd,
382
+ timestamp: new Date().toISOString(),
383
+ data: {
384
+ tool_name: "conversation",
385
+ tool_input: lastPrompt.slice(0, 8000),
386
+ tool_output: assistantText.slice(0, 8000),
387
+ },
388
+ },
389
+ });
390
+ });
391
+
392
+ pi.on("session_shutdown", async (event) => {
393
+ // /new, /resume, /fork and reloads fire this too; only quit ends the session.
394
+ if (event.reason !== "quit") return;
395
+ if (!lastHealthOk || !sessionId) return;
396
+ await callAgentMemory("session/end", {
397
+ body: { sessionId },
398
+ timeoutMs: 5_000,
399
+ });
400
+ void callAgentMemory("consolidate", { body: {} });
401
+ });
402
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "agentmemory-pi-extension",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "agentmemory extension for the pi coding agent: memory recall on agent start, capture on agent end, memory_search / memory_save / memory_health tools, /agentmemory-status command",
6
+ "type": "module",
7
+ "license": "Apache-2.0",
8
+ "keywords": [
9
+ "pi-package",
10
+ "agentmemory",
11
+ "memory"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/rohitg00/agentmemory.git",
16
+ "directory": "integrations/pi"
17
+ },
18
+ "files": [
19
+ "index.ts",
20
+ "security.ts",
21
+ "README.md"
22
+ ],
23
+ "pi": {
24
+ "extensions": [
25
+ "./index.ts"
26
+ ]
27
+ },
28
+ "peerDependencies": {
29
+ "@earendil-works/pi-coding-agent": "*",
30
+ "typebox": "*"
31
+ }
32
+ }