@prismer/runtime 2.0.8 → 2.2.55

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 (119) hide show
  1. package/CHANGELOG.md +3430 -0
  2. package/README.md +34 -12
  3. package/apc/skills/FIELD-DICTIONARY.md +111 -0
  4. package/apc/skills/bug-reproduce/SKILL.md +150 -0
  5. package/apc/skills/bug-reproduce/skill.json +96 -0
  6. package/apc/skills/code-review/SKILL.md +198 -0
  7. package/apc/skills/code-review/skill.json +124 -0
  8. package/apc/skills/design-review/SKILL.md +122 -0
  9. package/apc/skills/design-review/skill.json +88 -0
  10. package/apc/skills/doc-sync/SKILL.md +168 -0
  11. package/apc/skills/doc-sync/skill.json +81 -0
  12. package/apc/skills/env-doctor/SKILL.md +194 -0
  13. package/apc/skills/env-doctor/skill.json +209 -0
  14. package/apc/skills/git-ops/SKILL.md +189 -0
  15. package/apc/skills/git-ops/skill.json +94 -0
  16. package/apc/skills/impact-trace/SKILL.md +168 -0
  17. package/apc/skills/impact-trace/skill.json +104 -0
  18. package/apc/skills/observability/SKILL.md +195 -0
  19. package/apc/skills/observability/skill.json +116 -0
  20. package/apc/skills/release-db-config-sync/SKILL.md +186 -0
  21. package/apc/skills/release-db-config-sync/skill.json +109 -0
  22. package/apc/skills/release-ota-promote/SKILL.md +195 -0
  23. package/apc/skills/release-ota-promote/skill.json +176 -0
  24. package/apc/skills/release-preflight/SKILL.md +174 -0
  25. package/apc/skills/release-preflight/skill.json +175 -0
  26. package/apc/skills/release-rollback/SKILL.md +214 -0
  27. package/apc/skills/release-rollback/skill.json +230 -0
  28. package/apc/skills/release-tag/SKILL.md +194 -0
  29. package/apc/skills/release-tag/skill.json +94 -0
  30. package/apc/skills/releasing-prod/SKILL.md +49 -0
  31. package/apc/skills/releasing-test/SKILL.md +135 -0
  32. package/apc/skills/sdk-release/SKILL.md +200 -0
  33. package/apc/skills/spec-intake/SKILL.md +169 -0
  34. package/apc/skills/spec-intake/skill.json +93 -0
  35. package/apc/skills/test-result-feedback/SKILL.md +239 -0
  36. package/apc/skills/test-result-feedback/skill.json +193 -0
  37. package/apc/skills/test-runner/SKILL.md +169 -0
  38. package/apc/skills/test-runner/skill.json +103 -0
  39. package/apc/skills/ui-align/SKILL.md +209 -0
  40. package/apc/skills/ui-align/skill.json +114 -0
  41. package/apc/skills/ui-canvas/SKILL.md +148 -0
  42. package/apc/skills/ui-canvas/skill.json +127 -0
  43. package/built-in-skills/agent-coordination/SKILL.md +59 -37
  44. package/built-in-skills/agent-meta/SKILL.md +1 -0
  45. package/built-in-skills/assets/SKILL.md +8 -6
  46. package/built-in-skills/browser-use/SKILL.md +93 -0
  47. package/built-in-skills/canvas-design/SKILL.md +1 -0
  48. package/built-in-skills/claim-agent-ownership/SKILL.md +3 -2
  49. package/built-in-skills/claude-api/SKILL.md +1 -0
  50. package/built-in-skills/codebase-design/DEEPENING.md +37 -0
  51. package/built-in-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/built-in-skills/codebase-design/LICENSE +21 -0
  53. package/built-in-skills/codebase-design/SKILL.md +116 -0
  54. package/built-in-skills/conversation-compaction/SKILL.md +114 -0
  55. package/built-in-skills/council-creator/SKILL.md +426 -0
  56. package/built-in-skills/diagnosing-bugs/LICENSE +21 -0
  57. package/built-in-skills/diagnosing-bugs/SKILL.md +136 -0
  58. package/built-in-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  59. package/built-in-skills/doc-coauthoring/SKILL.md +1 -0
  60. package/built-in-skills/document-generation/SKILL.md +105 -0
  61. package/built-in-skills/domain-modeling/ADR-FORMAT.md +47 -0
  62. package/built-in-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  63. package/built-in-skills/domain-modeling/LICENSE +21 -0
  64. package/built-in-skills/domain-modeling/SKILL.md +76 -0
  65. package/built-in-skills/frontend-design/SKILL.md +1 -0
  66. package/built-in-skills/human-approval/SKILL.md +17 -2
  67. package/built-in-skills/image-generate/SKILL.md +103 -302
  68. package/built-in-skills/image-generate/scripts/generate-and-deliver.mjs +289 -0
  69. package/built-in-skills/ingest/SKILL.md +13 -45
  70. package/built-in-skills/internal-comms/SKILL.md +1 -0
  71. package/built-in-skills/liteparse/SKILL.md +130 -110
  72. package/built-in-skills/mcp-builder/SKILL.md +1 -0
  73. package/built-in-skills/memory/SKILL.md +420 -55
  74. package/built-in-skills/memory-dream/SKILL.md +339 -0
  75. package/built-in-skills/office-artifacts/SKILL.md +17 -4
  76. package/built-in-skills/okr/SKILL.md +154 -0
  77. package/built-in-skills/persona/SKILL.md +81 -0
  78. package/built-in-skills/persona-generator/SKILL.md +296 -0
  79. package/built-in-skills/pkf-svg/SKILL.md +253 -0
  80. package/built-in-skills/pkf-writing/SKILL.md +236 -0
  81. package/built-in-skills/prismer-im-collab/SKILL.md +26 -6
  82. package/built-in-skills/proactivity/SKILL.md +84 -0
  83. package/built-in-skills/remotion/SKILL.md +431 -0
  84. package/built-in-skills/role-builder/SKILL.md +203 -0
  85. package/built-in-skills/role-builder/scripts/author-role.mjs +334 -0
  86. package/built-in-skills/role-builder/scripts/ingest-role.mjs +223 -0
  87. package/built-in-skills/role-builder/scripts/instantiate-and-run.mjs +290 -0
  88. package/built-in-skills/role-builder/scripts/operation-harness.mjs +267 -0
  89. package/built-in-skills/skill-authoring/SKILL.md +110 -100
  90. package/built-in-skills/skill-authoring/skill.json +3 -3
  91. package/built-in-skills/skill-builder/SKILL.md +171 -0
  92. package/built-in-skills/skill-builder/scripts/ingest.mjs +265 -0
  93. package/built-in-skills/skill-creator/SKILL.md +165 -423
  94. package/built-in-skills/skill-creator/references/external-library-import.md +110 -0
  95. package/built-in-skills/skill-creator/scripts/import-library.mjs +475 -0
  96. package/built-in-skills/slack-gif-creator/SKILL.md +20 -0
  97. package/built-in-skills/tasks/SKILL.md +38 -23
  98. package/built-in-skills/tdd/LICENSE +21 -0
  99. package/built-in-skills/tdd/SKILL.md +110 -0
  100. package/built-in-skills/tdd/mocking.md +59 -0
  101. package/built-in-skills/tdd/refactoring.md +10 -0
  102. package/built-in-skills/tdd/tests.md +61 -0
  103. package/built-in-skills/team/SKILL.md +2 -1
  104. package/built-in-skills/web-artifacts-builder/SKILL.md +1 -0
  105. package/built-in-skills/webapp-testing/SKILL.md +1 -0
  106. package/built-in-skills/wechat-pay/SKILL.md +59 -0
  107. package/dist/cli.cjs +71872 -19960
  108. package/dist/cli.js +71803 -19846
  109. package/dist/index.cjs +72010 -19966
  110. package/dist/index.d.cts +4258 -712
  111. package/dist/index.d.ts +4258 -712
  112. package/dist/index.js +72156 -20118
  113. package/package.json +37 -6
  114. package/plugins/memory/prismer/__init__.py +1211 -0
  115. package/plugins/memory/prismer/plugin.yaml +8 -0
  116. package/plugins/memory/prismer/tool-schemas.generated.json +249 -0
  117. package/plugins/tools/prismer-recall/__init__.py +282 -0
  118. package/plugins/tools/prismer-recall/plugin.yaml +15 -0
  119. package/built-in-skills/memory-curation/SKILL.md +0 -135
@@ -0,0 +1,8 @@
1
+ name: prismer
2
+ version: 1.0.0
3
+ description: "Prismer Cloud — local-first memory provider. Archive recall via agent-driven memory_search/memory_load tools against the daemon's local FTS5 store; bounded core-inject; automatic extract; built-in MEMORY.md mirroring. Activating it replaces Honcho (Hermes allows one external provider)."
4
+ # All config is provided by the daemon (ensureService) via the profile .env —
5
+ # no external CLI dependency, the daemon HTTP endpoint IS the backend.
6
+ hooks:
7
+ - on_pre_compress
8
+ - on_memory_write
@@ -0,0 +1,249 @@
1
+ {
2
+ "_generated": {
3
+ "by": "npx tsx scripts/memory211/generate-memory-tool-contract.ts",
4
+ "from": "sdk/prismer/src/adapters/memory-tools.ts",
5
+ "doNotEdit": "Regenerate instead of editing — see scripts/memory211/memory-tool-contract.ts"
6
+ },
7
+ "memoryTools": [
8
+ {
9
+ "name": "memory_search",
10
+ "description": "Search workspace memory (prior decisions, user preferences, knowledge pages). Use this only when you have a specific question that you believe past memory would answer — not on every turn. Most queries do not need this. Returns 0–N ranked snippets with prismer:// URIs; empty results are normal. Each hit reports where it sits in the wiki (hubPath, children), how linked it is (inboundLinkCount, outboundPreview) and whether it is distilled knowledge or raw source (tier) — use those to decide whether to load the page or follow a link. When your wording matched nothing, the response instead carries `navigation`: startPoints are NOT answers, they are the INDEX/hub entries to walk from — browse their children, batch-load what looks relevant, then follow links. Pass several related questions at once via `queries` instead of calling this tool repeatedly.",
11
+ "parameters": {
12
+ "type": "object",
13
+ "properties": {
14
+ "query": {
15
+ "type": "string",
16
+ "description": "Free-text search query. Whitespace-separated terms are ANDed.",
17
+ "minLength": 1
18
+ },
19
+ "queries": {
20
+ "type": "array",
21
+ "description": "Batch recall: up to 8 related questions answered in ONE call, returned grouped in `resultsByQuery`. Prefer this over several separate memory_search calls.",
22
+ "items": {
23
+ "type": "string",
24
+ "minLength": 1
25
+ },
26
+ "maxItems": 8
27
+ },
28
+ "limit": {
29
+ "type": "integer",
30
+ "description": "Max number of results (default 5, max 20).",
31
+ "minimum": 1,
32
+ "maximum": 20,
33
+ "default": 5
34
+ },
35
+ "pageType": {
36
+ "type": "array",
37
+ "description": "Optional filter on page kind.",
38
+ "items": {
39
+ "type": "string",
40
+ "enum": [
41
+ "hub",
42
+ "leaf",
43
+ "decision",
44
+ "glossary",
45
+ "archive"
46
+ ]
47
+ }
48
+ }
49
+ },
50
+ "required": [
51
+ "query"
52
+ ]
53
+ }
54
+ },
55
+ {
56
+ "name": "memory_load",
57
+ "description": "Load a specific memory page by URI (prismer://workspace/<workspaceId>/memory/<path>) or by (workspaceId + path). Returns the full page content plus metadata and the page's outbound links (backlinks included) — served from the local link index even when the cloud is unreachable.",
58
+ "parameters": {
59
+ "type": "object",
60
+ "properties": {
61
+ "uri": {
62
+ "type": "string",
63
+ "description": "Full prismer:// URI (preferred). Mutually exclusive with workspaceId+path.",
64
+ "pattern": "^prismer://"
65
+ },
66
+ "workspaceId": {
67
+ "type": "string",
68
+ "description": "Workspace identifier (used with `path`)."
69
+ },
70
+ "path": {
71
+ "type": "string",
72
+ "description": "Workspace-relative memory file path (used with `workspaceId`)."
73
+ }
74
+ }
75
+ }
76
+ },
77
+ {
78
+ "name": "memory_browse",
79
+ "description": "See the current memory structure BEFORE writing: the INDEX, existing hubs (with what each is about), and pages nearest to your query. Use it to decide placement (attach under a hub via memory_write parentHubPath, extend an existing page, or create a new hub).",
80
+ "parameters": {
81
+ "type": "object",
82
+ "properties": {
83
+ "query": {
84
+ "type": "string",
85
+ "description": "Optional topic — adds the nearest existing pages to the structure view."
86
+ }
87
+ }
88
+ }
89
+ },
90
+ {
91
+ "name": "memory_write",
92
+ "description": "Write a durable memory page to the workspace knowledge wiki (the WRITE path). Use this to persist a fact, decision, definition, or piece of project context that should survive across sessions. YOU author the page: write the `content` and choose its `path`, deciding placement from the memory structure (call memory_browse FIRST) — extend/replace an existing page by reusing its path, or create a new leaf under the right topic and ATTACH it via `parentHubPath`. An unanchored new leaf is rejected with `placement_required`; create a hub first when no existing hub fits. To extend someone else's existing page, prefer op=append-section (with `section`) over overwriting the whole page. One memory per distinct topic. A returned {\"ok\":true} means the mutation succeeded: for that fact/path, do not call memory_write again in this turn to polish or verify; use memory_load instead. Canonical content is PKF; markdown exists only as a legacy compatibility input and is normalized before storage.",
93
+ "parameters": {
94
+ "type": "object",
95
+ "properties": {
96
+ "path": {
97
+ "type": "string",
98
+ "description": "Workspace-relative semantic path for the page, e.g. 'decisions/auth-model.pkf'. Reuse an existing page's path to extend/replace it.",
99
+ "minLength": 1
100
+ },
101
+ "content": {
102
+ "type": "string",
103
+ "description": "Canonical PKF page body. Markdown is accepted only for legacy compatibility and is normalized.",
104
+ "minLength": 1
105
+ },
106
+ "title": {
107
+ "type": "string",
108
+ "description": "Short page title."
109
+ },
110
+ "parentHubPath": {
111
+ "type": "string",
112
+ "description": "Attach this page under an existing hub — get hub paths from memory_browse. The daemon records a graph edge (page → hub) so the page nests under the hub instead of hanging flat off INDEX."
113
+ },
114
+ "relation": {
115
+ "type": "string",
116
+ "description": "Edge type for parentHubPath (default child-of).",
117
+ "enum": [
118
+ "child-of",
119
+ "related"
120
+ ]
121
+ },
122
+ "op": {
123
+ "type": "string",
124
+ "description": "Write operation (default replace = whole page). append-section appends a new section to an EXISTING page instead of overwriting it — preferred for extending someone else's page. rewrite-section overwrites just the section addressed by `section`.",
125
+ "enum": [
126
+ "replace",
127
+ "append-section",
128
+ "rewrite-section"
129
+ ]
130
+ },
131
+ "section": {
132
+ "type": "string",
133
+ "description": "Heading id (slug) of the target section. Required for op=append-section / rewrite-section."
134
+ },
135
+ "visibility": {
136
+ "type": "string",
137
+ "description": "Visibility scope for the page (default workspace). One of: workspace, agent:<imUserId>, private:<imUserId>, role:<slug>, council:<councilId>. role:/council: are for orchestrator writeback (e.g. a council roundtable writing a shared decision as council:<councilId>); a non-orchestrator caller falls back to workspace. Unknown values are treated as workspace."
138
+ }
139
+ },
140
+ "required": [
141
+ "path",
142
+ "content"
143
+ ]
144
+ }
145
+ },
146
+ {
147
+ "name": "memory_curate",
148
+ "description": "Curate workspace memory (knowledge-base maintenance). Ops: `candidates` READS what to converge (unplaced leaves + near-duplicate clusters + stale, remote-conflict, and oversized pages) — call this FIRST to decide clusters; `promote_to_hub` turns a leaf page into a hub; `supersede` archives a page and marks it stale; `rebuild_index` rebuilds machine-owned hub TOCs; top INDEX Contents is derived live from the graph. Section-level verbs (memory211 W5): `section_merge` folds one near-duplicate section into another (the cloud lands the supersedes/derived-from provenance edges for you), `section_supersede` retires one section in place, `rewire` re-points a broken or wrong link. `candidates` is read-only (any agent). Every other op is ORCHESTRATOR-ONLY — if you are not the workspace orchestrator they return an error.",
149
+ "parameters": {
150
+ "type": "object",
151
+ "properties": {
152
+ "op": {
153
+ "type": "string",
154
+ "description": "Curation operation. candidates = READ unplaced/near-dup/stale/conflict/oversized surfaces (call first); promote_to_hub = leaf→hub; supersede = archive+stale; rebuild_index = rebuild hub TOCs (INDEX Contents is derived); section_merge = fold a near-duplicate section into another; section_supersede = retire one section; rewire = re-point a link.",
155
+ "enum": [
156
+ "candidates",
157
+ "promote_to_hub",
158
+ "supersede",
159
+ "rebuild_index",
160
+ "section_merge",
161
+ "section_supersede",
162
+ "rewire"
163
+ ]
164
+ },
165
+ "pageId": {
166
+ "type": "string",
167
+ "description": "Target page id. Required for promote_to_hub and supersede. For section_merge this is the WINNER page (the section that absorbs the duplicate). For section_supersede this is the page whose section retires."
168
+ },
169
+ "childPaths": {
170
+ "type": "array",
171
+ "description": "promote_to_hub only — existing page paths to attach under the new hub as its children.",
172
+ "items": {
173
+ "type": "string"
174
+ }
175
+ },
176
+ "reason": {
177
+ "type": "string",
178
+ "description": "Optional reason recorded by supersede / section_merge / section_supersede."
179
+ },
180
+ "kind": {
181
+ "type": "string",
182
+ "description": "candidates only — which surface to read (default all).",
183
+ "enum": [
184
+ "orphans",
185
+ "duplicates",
186
+ "stale",
187
+ "conflicts",
188
+ "oversized",
189
+ "all"
190
+ ]
191
+ },
192
+ "limit": {
193
+ "type": "integer",
194
+ "description": "candidates only — max items per kind.",
195
+ "minimum": 1,
196
+ "maximum": 500
197
+ },
198
+ "section": {
199
+ "type": "string",
200
+ "description": "section_supersede only — anchor slug of the section to retire."
201
+ },
202
+ "targetSection": {
203
+ "type": "string",
204
+ "description": "section_merge only — anchor slug of the WINNER section that absorbs the duplicate."
205
+ },
206
+ "sourcePageId": {
207
+ "type": "string",
208
+ "description": "section_merge only — id of the LOSER page whose section is folded away."
209
+ },
210
+ "sourceSection": {
211
+ "type": "string",
212
+ "description": "section_merge only — anchor slug of the LOSER section."
213
+ },
214
+ "mergedContent": {
215
+ "type": "string",
216
+ "description": "section_merge only — the merged section body to write into the winner (PKF per pkf-writing). The cloud writes the supersedes/derived-from provenance edges automatically."
217
+ },
218
+ "supersededByPageId": {
219
+ "type": "string",
220
+ "description": "section_supersede only — optional surviving page the retired section points at."
221
+ },
222
+ "supersededBySection": {
223
+ "type": "string",
224
+ "description": "section_supersede only — anchor slug on supersededByPageId."
225
+ },
226
+ "linkId": {
227
+ "type": "string",
228
+ "description": "rewire only — id of the link to re-point."
229
+ },
230
+ "toPageId": {
231
+ "type": "string",
232
+ "description": "rewire only — new target page id (mutually exclusive with toPath)."
233
+ },
234
+ "toPath": {
235
+ "type": "string",
236
+ "description": "rewire only — new target page path (mutually exclusive with toPageId)."
237
+ },
238
+ "toSection": {
239
+ "type": "string",
240
+ "description": "rewire only — optional new target section anchor."
241
+ }
242
+ },
243
+ "required": [
244
+ "op"
245
+ ]
246
+ }
247
+ }
248
+ ]
249
+ }
@@ -0,0 +1,282 @@
1
+ """Prismer Cloud recall-tools — reversible register_tool plugin lane.
2
+
3
+ Desktop-202 doc 19 §8 — the *lightest, most reversible* way to give a Hermes
4
+ agent the memory recall affordance. It also carries one bounded, signed-runtime
5
+ lifecycle bridge needed to bind the provider's actual response routing to the
6
+ exact daemon run:
7
+
8
+ * It calls ``ctx.register_tool(...)`` TWICE (memory_search / memory_load) so
9
+ the two tools land in ``agent.tools`` via the registry's
10
+ ``get_tool_definitions()`` (agent_init.py:1156-1187 dedupes the memory-
11
+ provider injection against exactly these plugin-registered names).
12
+ * It registers ``post_api_request`` and forwards ONLY session id, the response
13
+ model (or Hermes' request-locked model when the upstream omits that field),
14
+ provider, agent and workspace. Raw response/content never crosses
15
+ this bridge. It still registers NO prefetch / sync / inject / extract /
16
+ compress behaviour and does not register a memory provider, so it does not
17
+ pin ``memory.provider`` or trip Hermes' provider exclusivity.
18
+
19
+ Reversibility judge (doc 19 §3): delete this dir + drop ``prismer-recall``
20
+ from ``plugins.enabled`` = fully gone. No distributed artifact, no version
21
+ obligation, no framework lifecycle coupling.
22
+
23
+ The handlers mirror the daemon-calling logic of the full provider shell
24
+ (plugins/memory/prismer/) — plain HTTP GET to the daemon's local loopback
25
+ ``/local/memory/search`` and ``/local/memory/load`` (FTS5 BM25 + graph, NO
26
+ vector) — but as plain callables, not provider methods.
27
+
28
+ Config (profile .env, written by the daemon / the TS adapter wiring):
29
+ PRISMER_WORKSPACE_ID — the workspace this profile's agent is bound to
30
+ PRISMER_DAEMON_PORT — daemon loopback port (default 3210)
31
+ PRISMER_DAEMON_TOKEN — optional bearer for the local data-plane
32
+ PRISMER_AGENT_IM_USER_ID — the calling agent's IM user id (M-ACTOR-THREAD).
33
+ Forwarded as ``actorImUserId`` + ``actorKind=agent``
34
+ so the daemon attributes the ``recall_pull``
35
+ observability event to this agent. The hermes
36
+ gateway already exports this var into the plugin's
37
+ inherited environment (hermes/index.ts gateway
38
+ spawn env); when ABSENT the params are omitted and
39
+ the daemon skips emission rather than fabricating
40
+ an actor.
41
+
42
+ NOTE: this file intentionally avoids the substrings the Hermes plugin loader
43
+ sniffs for to auto-coerce a plugin to ``kind="exclusive"`` (plugins.py:1318).
44
+ It is a plain ``standalone`` tool plugin — see plugin.yaml ``kind: standalone``.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import json
50
+ import logging
51
+ import os
52
+ import urllib.parse
53
+ import urllib.request
54
+ from typing import Any, Dict, Optional
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ _DEFAULT_PORT = "3210"
59
+ _RECALL_TIMEOUT = 5
60
+ _ROUTING_TIMEOUT = 2
61
+
62
+ # Tool names are FROZEN to mirror the shared TS spec
63
+ # (sdk/prismer/src/adapters/memory-tools.ts) and the OpenAI-style
64
+ # shapes in the hermes adapter (HERMES_MEMORY_TOOLS). Do NOT rename.
65
+ _TOOLSET = "prismer_recall"
66
+
67
+ _MEMORY_SEARCH_SCHEMA = {
68
+ "name": "memory_search",
69
+ "description": (
70
+ "Search workspace memory (prior decisions, user preferences, knowledge "
71
+ "pages). Use this only when you have a specific question that you believe "
72
+ "past memory would answer — not on every turn. Most queries do not need "
73
+ "this. Returns 0–N ranked snippets with prismer:// URIs; empty results "
74
+ "are normal."
75
+ ),
76
+ "parameters": {
77
+ "type": "object",
78
+ "properties": {
79
+ "query": {
80
+ "type": "string",
81
+ "description": "Free-text search query. Whitespace-separated terms are ANDed.",
82
+ "minLength": 1,
83
+ },
84
+ "limit": {
85
+ "type": "integer",
86
+ "description": "Max number of results (default 5, max 20).",
87
+ "minimum": 1,
88
+ "maximum": 20,
89
+ "default": 5,
90
+ },
91
+ "pageType": {
92
+ "type": "array",
93
+ "description": "Optional filter on page kind.",
94
+ "items": {
95
+ "type": "string",
96
+ "enum": ["hub", "leaf", "decision", "glossary", "archive"],
97
+ },
98
+ },
99
+ },
100
+ "required": ["query"],
101
+ },
102
+ }
103
+
104
+ _MEMORY_LOAD_SCHEMA = {
105
+ "name": "memory_load",
106
+ "description": (
107
+ "Load a specific memory page by URI "
108
+ "(prismer://workspace/<workspaceId>/memory/<path>) or by "
109
+ "(workspaceId + path). Returns the full page content plus metadata."
110
+ ),
111
+ "parameters": {
112
+ "type": "object",
113
+ "properties": {
114
+ "uri": {
115
+ "type": "string",
116
+ "description": "Full prismer:// URI (preferred). Mutually exclusive with workspaceId+path.",
117
+ "pattern": "^prismer://",
118
+ },
119
+ "workspaceId": {
120
+ "type": "string",
121
+ "description": "Workspace identifier (used with `path`).",
122
+ },
123
+ "path": {
124
+ "type": "string",
125
+ "description": "Workspace-relative memory file path (used with `workspaceId`).",
126
+ },
127
+ },
128
+ },
129
+ }
130
+
131
+
132
+ def _daemon_base() -> str:
133
+ port = (os.environ.get("PRISMER_DAEMON_PORT") or _DEFAULT_PORT).strip() or _DEFAULT_PORT
134
+ return f"http://127.0.0.1:{port}"
135
+
136
+
137
+ def _headers() -> Dict[str, str]:
138
+ h = {"Content-Type": "application/json"}
139
+ token = (os.environ.get("PRISMER_DAEMON_TOKEN") or "").strip()
140
+ if token:
141
+ h["Authorization"] = f"Bearer {token}"
142
+ # memory203 doc 08 §2 (F4) — forward the daemon-injected per-agent memory
143
+ # capability so the daemon enforces workspace scope on recall RPCs.
144
+ cap = (os.environ.get("PRISMER_MEMORY_CAP") or "").strip()
145
+ if cap:
146
+ h["x-prismer-memory-cap"] = cap
147
+ return h
148
+
149
+
150
+ def _http_get(path: str, timeout: int) -> Optional[dict]:
151
+ url = _daemon_base() + path
152
+ req = urllib.request.Request(url, headers=_headers(), method="GET")
153
+ try:
154
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
155
+ return json.loads(resp.read().decode("utf-8"))
156
+ except Exception as exc: # noqa: BLE001 — degrade-not-break (doc 19 §8)
157
+ logger.debug("prismer-recall GET %s failed: %s", path, exc)
158
+ return None
159
+
160
+
161
+ def _http_post(path: str, body: Dict[str, Any], timeout: int) -> Optional[dict]:
162
+ url = _daemon_base() + path
163
+ req = urllib.request.Request(
164
+ url,
165
+ data=json.dumps(body, separators=(",", ":")).encode("utf-8"),
166
+ headers=_headers(),
167
+ method="POST",
168
+ )
169
+ try:
170
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
171
+ raw = resp.read()
172
+ return json.loads(raw.decode("utf-8")) if raw else {"ok": True}
173
+ except Exception as exc: # noqa: BLE001 — routing evidence fails closed
174
+ logger.debug("prismer-recall POST %s failed: %s", path, exc)
175
+ return None
176
+
177
+
178
+ def _workspace_id() -> str:
179
+ return (os.environ.get("PRISMER_WORKSPACE_ID") or "").strip()
180
+
181
+
182
+ def _actor_params() -> Dict[str, str]:
183
+ """M-ACTOR-THREAD — the calling agent's actor identity for recall_pull.
184
+
185
+ Reads ``PRISMER_AGENT_IM_USER_ID`` from the inherited gateway env. When set,
186
+ returns ``{actorImUserId, actorKind}`` so the daemon RPC handler
187
+ (rpc.ts handleSearch / handleLoad) can attribute the ``recall_pull``
188
+ observability event to this agent. When ABSENT (legacy / anonymous), returns
189
+ ``{}`` so the daemon skips emission rather than fabricating an actor.
190
+ """
191
+ actor = (os.environ.get("PRISMER_AGENT_IM_USER_ID") or "").strip()
192
+ if not actor:
193
+ return {}
194
+ return {"actorImUserId": actor, "actorKind": "agent"}
195
+
196
+
197
+ def _handle_memory_search(args: Dict[str, Any], **_kw: Any) -> str:
198
+ ws = _workspace_id()
199
+ if not ws:
200
+ return json.dumps({"error": "no workspace bound"})
201
+ query = str(args.get("query", "") or "").strip()
202
+ if not query:
203
+ return json.dumps({"error": "missing_query"})
204
+ params = {
205
+ "workspaceId": ws,
206
+ "q": query,
207
+ "topK": str(int(args.get("limit", 5) or 5)),
208
+ **_actor_params(),
209
+ }
210
+ page_type = args.get("pageType")
211
+ if isinstance(page_type, list) and page_type:
212
+ # Daemon accepts a single pageType per request — take the first
213
+ # (mirrors memory-tools.ts:173-176).
214
+ params["pageType"] = str(page_type[0])
215
+ res = _http_get(f"/local/memory/search?{urllib.parse.urlencode(params)}", _RECALL_TIMEOUT)
216
+ return json.dumps(res if res is not None else {"query": query, "results": []})
217
+
218
+
219
+ def _handle_memory_load(args: Dict[str, Any], **_kw: Any) -> str:
220
+ ws = _workspace_id()
221
+ q: Dict[str, str] = {}
222
+ if args.get("uri"):
223
+ q["uri"] = str(args["uri"])
224
+ elif args.get("workspaceId") and args.get("path"):
225
+ q["workspaceId"] = str(args["workspaceId"])
226
+ q["path"] = str(args["path"])
227
+ elif ws and args.get("path"):
228
+ q["workspaceId"] = ws
229
+ q["path"] = str(args["path"])
230
+ else:
231
+ return json.dumps({"error": "memory_load requires either `uri` or `workspaceId`+`path`"})
232
+ q.update(_actor_params())
233
+ res = _http_get(f"/local/memory/load?{urllib.parse.urlencode(q)}", _RECALL_TIMEOUT)
234
+ return json.dumps(res if res is not None else {"error": "not_found"})
235
+
236
+
237
+ def _record_terminal_routing(**kwargs: Any) -> None:
238
+ """Forward bounded post-response evidence; ignore raw response/content."""
239
+ session_id = str(kwargs.get("session_id") or "").strip()
240
+ # Some OpenAI-compatible streaming responses omit the optional `model`
241
+ # field. Hermes still passes `model=agent.model`, which is the model locked
242
+ # onto this exact API request (`require_model_lock=true` on the sessions
243
+ # path). It is terminal lifecycle evidence here, not profile/config fallback.
244
+ served_model = str(kwargs.get("response_model") or kwargs.get("model") or "").strip()
245
+ served_provider = str(kwargs.get("provider") or "").strip()
246
+ agent_im_user_id = (os.environ.get("PRISMER_AGENT_IM_USER_ID") or "").strip()
247
+ workspace_id = _workspace_id()
248
+ if not all((session_id, served_model, served_provider, agent_im_user_id, workspace_id)):
249
+ return
250
+ _http_post(
251
+ "/v1/hooks/post_api_request",
252
+ {
253
+ "session_id": session_id,
254
+ "extra": {
255
+ "agent_im_user_id": agent_im_user_id,
256
+ "workspace_id": workspace_id,
257
+ "served_model": served_model,
258
+ "served_provider": served_provider,
259
+ },
260
+ },
261
+ _ROUTING_TIMEOUT,
262
+ )
263
+
264
+
265
+ def register(ctx) -> None:
266
+ """Register recall tools plus the bounded terminal-routing bridge."""
267
+ ctx.register_tool(
268
+ name="memory_search",
269
+ toolset=_TOOLSET,
270
+ schema=_MEMORY_SEARCH_SCHEMA,
271
+ handler=_handle_memory_search,
272
+ emoji="🔎",
273
+ )
274
+ ctx.register_tool(
275
+ name="memory_load",
276
+ toolset=_TOOLSET,
277
+ schema=_MEMORY_LOAD_SCHEMA,
278
+ handler=_handle_memory_load,
279
+ emoji="📄",
280
+ )
281
+ ctx.register_hook("post_api_request", _record_terminal_routing)
282
+ logger.info("prismer-recall registered tools + bounded post_api_request routing hook")
@@ -0,0 +1,15 @@
1
+ name: prismer-recall
2
+ version: 1.1.0
3
+ kind: standalone
4
+ description: >-
5
+ Prismer Cloud recall-tools — registers the agent-driven memory_search /
6
+ memory_load tools (FTS5 BM25 + graph, no vector) against the local daemon
7
+ via plain register_tool plus a bounded post_api_request routing-evidence
8
+ bridge. It does not own memory persistence or provider selection and does not
9
+ trip the single-external-provider exclusivity. Remove this dir + drop the
10
+ name from plugins.enabled = fully gone.
11
+ hooks:
12
+ - post_api_request
13
+ provides_tools:
14
+ - memory_search
15
+ - memory_load