@synap-core/cli 1.2.0 → 1.5.0

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 (74) hide show
  1. package/README.md +26 -0
  2. package/dist/commands/agents.d.ts +31 -0
  3. package/dist/commands/agents.js +478 -0
  4. package/dist/commands/agents.js.map +1 -0
  5. package/dist/commands/connect.d.ts +18 -1
  6. package/dist/commands/connect.js +154 -74
  7. package/dist/commands/connect.js.map +1 -1
  8. package/dist/commands/connections.d.ts +9 -0
  9. package/dist/commands/connections.js +161 -0
  10. package/dist/commands/connections.js.map +1 -0
  11. package/dist/commands/data.d.ts +43 -0
  12. package/dist/commands/data.js +387 -0
  13. package/dist/commands/data.js.map +1 -0
  14. package/dist/commands/finish.js +41 -8
  15. package/dist/commands/finish.js.map +1 -1
  16. package/dist/commands/infra.d.ts +21 -0
  17. package/dist/commands/infra.js +262 -0
  18. package/dist/commands/infra.js.map +1 -0
  19. package/dist/commands/init.js +188 -10
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/knowledge.d.ts +36 -0
  22. package/dist/commands/knowledge.js +123 -0
  23. package/dist/commands/knowledge.js.map +1 -0
  24. package/dist/commands/openclaw.d.ts +2 -0
  25. package/dist/commands/openclaw.js +300 -23
  26. package/dist/commands/openclaw.js.map +1 -1
  27. package/dist/commands/pods.d.ts +17 -0
  28. package/dist/commands/pods.js +371 -0
  29. package/dist/commands/pods.js.map +1 -0
  30. package/dist/commands/status.d.ts +14 -1
  31. package/dist/commands/status.js +78 -220
  32. package/dist/commands/status.js.map +1 -1
  33. package/dist/commands/update.d.ts +11 -2
  34. package/dist/commands/update.js +116 -5
  35. package/dist/commands/update.js.map +1 -1
  36. package/dist/index.js +370 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/agents-config.d.ts +20 -0
  39. package/dist/lib/agents-config.js +45 -0
  40. package/dist/lib/agents-config.js.map +1 -0
  41. package/dist/lib/auth.d.ts +4 -0
  42. package/dist/lib/auth.js +4 -0
  43. package/dist/lib/auth.js.map +1 -1
  44. package/dist/lib/browser-auth.d.ts +35 -0
  45. package/dist/lib/browser-auth.js +170 -0
  46. package/dist/lib/browser-auth.js.map +1 -0
  47. package/dist/lib/hub-client.d.ts +17 -0
  48. package/dist/lib/hub-client.js +115 -0
  49. package/dist/lib/hub-client.js.map +1 -0
  50. package/dist/lib/openclaw.js +30 -19
  51. package/dist/lib/openclaw.js.map +1 -1
  52. package/dist/lib/pod.d.ts +32 -1
  53. package/dist/lib/pod.js +121 -9
  54. package/dist/lib/pod.js.map +1 -1
  55. package/dist/lib/skills-installer.d.ts +18 -0
  56. package/dist/lib/skills-installer.js +97 -0
  57. package/dist/lib/skills-installer.js.map +1 -0
  58. package/dist/lib/targets.d.ts +65 -0
  59. package/dist/lib/targets.js +673 -0
  60. package/dist/lib/targets.js.map +1 -0
  61. package/package.json +5 -3
  62. package/skills/README.md +91 -0
  63. package/skills/synap/README.md +76 -0
  64. package/skills/synap/SKILL.md +882 -0
  65. package/skills/synap/capture.md +170 -0
  66. package/skills/synap/governance.md +206 -0
  67. package/skills/synap/linking.md +128 -0
  68. package/skills/synap/scripts/orient.sh +28 -0
  69. package/skills/synap-schema/SKILL.md +231 -0
  70. package/skills/synap-schema/property-types.md +228 -0
  71. package/skills/synap-ui/SKILL.md +295 -0
  72. package/skills/synap-ui/bento-recipes.md +608 -0
  73. package/skills/synap-ui/view-types.md +259 -0
  74. package/skills/synap-ui/widget-catalog.md +305 -0
@@ -0,0 +1,882 @@
1
+ ---
2
+ name: synap
3
+ description: >
4
+ Use this skill whenever the user wants to capture, remember, find, or structure
5
+ information in their Synap data pod. Triggers: creating a task, note, person,
6
+ company, project, event, contact, or deal; saving an article or webpage;
7
+ storing a fact about someone ("Alice prefers async"); searching the user's
8
+ knowledge ("find my notes on X", "who did I meet last week"); linking entities;
9
+ logging a meeting or a contact; capturing unstructured text into structured
10
+ entities; reading what's in the user's pod before answering questions about
11
+ their life, work, or projects; posting to their personal AI channel. The pod
12
+ is the user's sovereign source of truth — prefer it over your own context
13
+ when the user asks about their own data. Do NOT use this skill for extending
14
+ the schema (use synap-schema) or building dashboards and views (use synap-ui).
15
+ metadata:
16
+ openclaw:
17
+ requires:
18
+ env: [SYNAP_HUB_API_KEY, SYNAP_POD_URL]
19
+ optional_env:
20
+ [SYNAP_WORKSPACE_ID, SYNAP_USER_ID, SYNAP_DEFAULT_CHANNEL_ID]
21
+ primaryEnv: SYNAP_HUB_API_KEY
22
+ homepage: https://synap.live
23
+ capabilities: [memory, knowledge-graph, channels]
24
+ os: [macos, linux, windows]
25
+ userInvocable: false
26
+ ---
27
+
28
+ # Synap — core data operations
29
+
30
+ You are connected to a **Synap Data Pod** at `{SYNAP_POD_URL}`. All requests use `Authorization: Bearer {SYNAP_HUB_API_KEY}`.
31
+
32
+ **If you have Bash access** (Claude Code, agent with tools): use the `synap` CLI — see **CLI Data Operations** below. Auth is automatic, `--json` gives clean output, no manual header management.
33
+
34
+ **If you only have HTTP access**: use the REST endpoints documented below. Your `userId` is in `{SYNAP_USER_ID}` (set by `synap connect`). If it's missing, call `GET /api/hub/users/me` → `.id` once.
35
+
36
+ Your job is to turn unstructured input into a **connected** knowledge graph. Isolated entities are anti-value. Every entity you create should link to at least one other entity.
37
+
38
+ ## Mental model
39
+
40
+ Synap is a typed knowledge graph. Six layers you need:
41
+
42
+ | Layer | What it is | When to use |
43
+ | ------------- | ---------------------------------------------- | -------------------------------------------------------- |
44
+ | **Entities** | Typed structured nodes (task, person, …) | Anything worth filtering, sorting, or linking |
45
+ | **Relations** | Typed edges between entities | Making the graph traversable |
46
+ | **Documents** | Long-form markdown attached to an entity | Meeting notes, research writeups, articles |
47
+ | **Memory** | Atomic facts, no structure | Preferences, context, ephemeral notes |
48
+ | **Threads** | Channel conversations, optional entity context | Posting to the user's personal AI channel |
49
+ | **Proposals** | Writes queued for human approval | Governance for some mutations (not an error — see below) |
50
+
51
+ ## Synap-first operating mode
52
+
53
+ > **MCP clients** (Claude Desktop, Raycast, OpenClaw with MCP): use `synap_*` tool names — they wrap auth and governance automatically. **REST / HTTP clients**: use the endpoints below.
54
+
55
+ These five rules override default assistant behavior when connected to a Synap pod:
56
+
57
+ **1. Orient before acting**
58
+ Run `scripts/orient.sh` or call these endpoints at the start of every session — before searching, before creating, before answering any question about the user's data:
59
+
60
+ ```
61
+ GET /api/hub/manifest
62
+ → static capability map: view types, bento block kinds, inline patterns, browser-native cells
63
+
64
+ GET /api/hub/users/me
65
+ → { id, email, name } ← your userId
66
+
67
+ GET /api/hub/workspaces
68
+ → [{ id, name, role }] ← workspaces[0].id if only one
69
+
70
+ GET /api/hub/profiles?userId={userId}&workspaceId={workspaceId}
71
+ → [{ slug, displayName, entityScope, properties }]
72
+ ```
73
+
74
+ `entityScope: "pod"` = visible across all workspaces (note, task, project, person, company, bookmark, event, contact, article, website).
75
+ `entityScope: "workspace"` = scoped to one workspace (deal, file, capture, custom profiles).
76
+
77
+ **2. Search before answering**
78
+ Before answering any question about the user's projects, tasks, contacts, decisions, or anything they might have captured — search Synap first. Do not answer from your training or context window when Synap may have the authoritative answer.
79
+
80
+ **3. Save proactively — without waiting to be asked**
81
+ When the user shares a decision, task, meeting outcome, contact, or any durable information: save it. Don't ask "should I save this?" for obviously important information. Use:
82
+
83
+ - entities for structured data (tasks, people, projects, decisions)
84
+ - `remember_fact` / `POST /api/hub/memory` for preferences, context, loose facts
85
+ - documents for long-form notes (meeting notes, research, writeups)
86
+
87
+ **4. Link everything**
88
+ An isolated entity has no value in a knowledge graph. When creating entities, immediately link them to related entities. A task belongs to a project. A note belongs to a meeting or a person. A decision belongs to a project and may supersede another decision.
89
+
90
+ **5. Persist facts, not just conversation**
91
+ Facts about the user — preferences, team, working style, recurring context — belong in Synap memory, not in your context window. Memory survives sessions and is accessible across all AI surfaces. Context does not.
92
+
93
+ Properties with `valueType: "entity_id"` are typed links to other entities — see **Linking** below.
94
+
95
+ ## CLI Data Operations (Bash tool)
96
+
97
+ When Claude Code (or any agent with Bash access) is using this skill, prefer the `synap` CLI over raw HTTP calls — auth is automatic, output is clean JSON, no spinners in `--json` mode.
98
+
99
+ **Session context — set once, never repeat:**
100
+
101
+ The CLI reads the active pod and workspace from `~/.synap/config.json`. Set context once at the start of a session; all subsequent commands inherit it automatically. Do NOT pass `--pod-url`, `--api-key`, or `--workspace` on every command.
102
+
103
+ ```bash
104
+ synap pods use <profile-name> # switch active pod
105
+ synap use <workspace-id> # switch active workspace
106
+ synap workspace provision-agent --json # provision your agent workspace + auto-sets it active
107
+ ```
108
+
109
+ **Always orient first:**
110
+
111
+ ```bash
112
+ synap orient --json
113
+ # Returns: userId, podUrl, workspaces[{id, name, slug}]
114
+ # Never hardcode workspace IDs — discover them here.
115
+ ```
116
+
117
+ **Search (Typesense-powered, cross-collection):**
118
+
119
+ ```bash
120
+ synap search "project ideas" --json
121
+ synap search "Antoine" --type=entity --workspace=<id> --json
122
+ synap search "meeting notes" --type=doc --limit=5 --json
123
+ # Omit --workspace to search pod-wide. Include it to scope to one workspace.
124
+ # Use search for name/keyword queries. For semantic/conceptual search, use Hub Protocol memory endpoints.
125
+ ```
126
+
127
+ **Read entities:**
128
+
129
+ ```bash
130
+ synap list workspaces --json
131
+ synap list entities --workspace=<id> --json
132
+ synap list entities --profile=task --workspace=<id> --json
133
+ synap get entity <id> --json
134
+ ```
135
+
136
+ **Episodic memory (session facts, loose context):**
137
+
138
+ ```bash
139
+ synap remember "Key decision: use Typesense for search" --json
140
+ synap recall "Typesense" --limit=5 --json
141
+ ```
142
+
143
+ **Structured knowledge (durable, typed, searchable — preferred for engineering learnings):**
144
+
145
+ ```bash
146
+ # Capture a gotcha, lesson, decision, or reference into your agent workspace
147
+ synap capture --type gotcha --claim "Hono static routes must come before /:id" \
148
+ --why "First-match routing; dynamic routes eat static ones" \
149
+ --tags "repo:synap-backend,layer:routing" --json
150
+
151
+ synap capture --type lesson --claim "code-read ≠ runtime-true for library APIs" \
152
+ --evidence "tldraw 2.4.6 binding API changed silently from props.start.boundShapeId"
153
+
154
+ # Recall across your knowledge base with full-text search
155
+ synap recall "hono routing" --structured --json
156
+ synap recall "tldraw" --structured --type gotcha --json
157
+
158
+ # Prerequisite (run once): provision your agent workspace and set it active
159
+ synap workspace provision-agent --json
160
+ ```
161
+
162
+ `synap capture` / `synap recall --structured` uses the `engineering_knowledge` entity profile.
163
+ `synap remember` / `synap recall` (without `--structured`) uses the ephemeral `/memory` store.
164
+ Use structured knowledge for anything worth remembering across sessions and projects.
165
+
166
+ **Write:**
167
+
168
+ ```bash
169
+ synap create entity --profile=note --name="Meeting notes" --workspace=<id> --json
170
+ synap set entity <id> --props='{"status":"done"}' --json
171
+ ```
172
+
173
+ **Multi-agent:** If `SYNAP_AGENT` env var is set, the CLI uses that named identity's API key from `~/.synap/config.json` instead of the default pod credentials. Use `synap agents list` to see configured identities.
174
+
175
+ **Rules:**
176
+
177
+ - Always use `--json` when calling from code — clean stdout, no spinners, machine-parseable
178
+ - Run `synap orient` first to discover workspace IDs — never hardcode them
179
+ - Omit `--workspace` to operate pod-wide; include it to scope to a specific workspace
180
+ - `synap search` is Typesense (fast keyword/name search). Semantic search → Hub Protocol `GET /api/hub/memory?query=…`
181
+
182
+ ---
183
+
184
+ ## Scope — default pod-wide
185
+
186
+ **Default: pod-wide.** 13 of 17 system profiles (`note`, `task`, `project`, `event`, `person`, `contact`, `company`, `bookmark`, `article`, `website`, `decision`, `question`, `research`) are pod-scoped — entities you create show up in _every_ workspace the user owns. The backend handles this automatically when the profile is pod-scoped: you don't need to pass `workspaceId`.
187
+
188
+ **Scope a creation to one workspace only when:**
189
+
190
+ 1. The user explicitly says "in my `X` workspace" / "inside this space".
191
+ 2. You're inside a clear workspace context (the user is on a project page, discussing that project — new tasks go into that workspace).
192
+ 3. The profile is workspace-scoped by definition (`deal`, `file`, `capture`, and custom profiles). The backend already uses the user's active workspace when you don't pass one — usually this is what you want.
193
+
194
+ **Rule of thumb:** don't pass `workspaceId` unless the user's intent specifically narrows to one workspace. A task the user dictates "from the couch" belongs to the whole pod, not to whichever workspace was last open.
195
+
196
+ When you do scope to a workspace, pass `workspaceId` in the create body — the backend respects it. Never pass `workspaceId: null` explicitly to force pod-wide; the profile's `entityScope` decides.
197
+
198
+ ## The work flow — question → research → decision → action
199
+
200
+ AI-assisted work has a shape. When the user is actually _thinking about something_, it flows through four structural nodes. Each is a first-class entity. None of these are optional "nice-to-have" labels — they're the graph that makes the work _durable_ and transferrable between AIs.
201
+
202
+ | Stage | Entity | What it captures | Typical trigger |
203
+ | ----------- | ---------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
204
+ | Inquiry | `question` | What the user is trying to figure out | "I'm wondering about X" / "Should we Y or Z?" / "What's the best…" |
205
+ | Exploration | `research` | Investigation: sources consulted, conclusion, confidence | Reading articles, comparing options, summarizing findings |
206
+ | Resolution | `decision` | What was chosen + rationale + alternatives | "We decided to…" / "Let's go with…" / "I'm going with…" |
207
+ | Execution | `task` | Concrete action items that follow the decision | "Now I need to…" / "TODO: ship Y by…" |
208
+
209
+ **Link each stage to the next:**
210
+
211
+ - `question.answeredByDecisionId` → the decision that closed it
212
+ - `research.questionId` → the question it investigates
213
+ - `decision.projectId` → the project it affects (same for question / research)
214
+ - Use `POST /relations type=source` to link research to its sources (articles, websites, documents)
215
+
216
+ Traversing in either direction gives the user answers like:
217
+
218
+ - "What am I currently exploring about Project Eve?" → `GET /entities?profileSlug=question&…` filtered by open
219
+ - "What decisions have we made on this project?" → filtered by `projectId`
220
+ - "What was the research behind this decision?" → reverse-lookup from `decision` via the research entities that reference the same `projectId` and question
221
+
222
+ ### When to create each
223
+
224
+ **`question` — substantive inquiries only.** The test: _would the user want to find this later?_ "What's the weather" = no, don't create. "Should we use LangGraph or CrewAI?" = yes, create. Casual chitchat never becomes a question.
225
+
226
+ **`research` — when you investigate.** Any time you go off and read articles / websites / past notes to answer something, that's research. Create the entity upfront (`status: "ongoing"`), link sources as you pull them (`POST /relations type=source`), set `conclusion` when you're done (`status: "concluded"`).
227
+
228
+ **`decision` — when the user picks a path.** Already covered in the memory-vs-entity section above. Link back to the question it answers (set `question.answeredByDecisionId`).
229
+
230
+ **`task` — when the decision implies concrete work.** Link with `projectId` if not already inferred.
231
+
232
+ ### Worked example
233
+
234
+ User: _"I'm trying to figure out whether we should build our own orchestrator or standardize on OpenClaude's. Can you help me think through it?"_
235
+
236
+ 1. Create the question:
237
+
238
+ ```json
239
+ POST /api/hub/entities
240
+ { "profileSlug": "question",
241
+ "title": "Build custom orchestrator or use OpenClaude native?",
242
+ "properties": {
243
+ "questionStatus": "exploring",
244
+ "askedAt": "2026-04-20",
245
+ "projectId": "ent_project_eve",
246
+ "description": "Weighing separation-of-concerns vs. out-of-the-box capability."
247
+ } }
248
+ ```
249
+
250
+ 2. As you investigate, create a research entity and link sources:
251
+
252
+ ```json
253
+ POST /api/hub/entities
254
+ { "profileSlug": "research",
255
+ "title": "LangGraph vs CrewAI capability survey",
256
+ "properties": {
257
+ "researchStatus": "ongoing",
258
+ "questionId": "ent_question_1",
259
+ "projectId": "ent_project_eve"
260
+ } }
261
+
262
+ POST /api/hub/relations
263
+ { "sourceEntityId": "ent_research_1", "targetEntityId": "ent_article_langgraph_docs", "type": "source" }
264
+ ```
265
+
266
+ 3. When you reach a conclusion, update the research:
267
+
268
+ ```json
269
+ PATCH /api/hub/entities/ent_research_1
270
+ { "properties": {
271
+ "researchStatus": "concluded",
272
+ "conclusion": "LangGraph separates orchestration brain from UX. CrewAI adds agent abstractions but couples to its runtime.",
273
+ "researchConfidence": "high"
274
+ } }
275
+ ```
276
+
277
+ 4. When the user picks, create a decision linked to the question:
278
+
279
+ ```json
280
+ POST /api/hub/entities
281
+ { "profileSlug": "decision",
282
+ "title": "Use LangGraph orchestrator over OpenClaude native",
283
+ "properties": {
284
+ "decisionStatus": "accepted",
285
+ "decidedAt": "2026-04-22",
286
+ "rationale": "Separates Orchestration Brain from UX.",
287
+ "alternatives": "Standardize on OpenClaude's multi-agent logic.",
288
+ "projectId": "ent_project_eve"
289
+ } }
290
+
291
+ PATCH /api/hub/entities/ent_question_1
292
+ { "properties": {
293
+ "questionStatus": "answered",
294
+ "answeredByDecisionId": "ent_decision_1"
295
+ } }
296
+ ```
297
+
298
+ 5. Tasks follow as usual, linked to the project.
299
+
300
+ **The payoff:** six months later, any AI (or the user alone) can reconstruct the reasoning by traversing from the project → question → research → decision → tasks. That's the durability Synap provides on top of chat.
301
+
302
+ ### Creation is silent by default
303
+
304
+ Don't interrupt the conversation to ask "should I log this as a question?" — just do it and add a one-line trailer at the end of your response:
305
+
306
+ > (Logged as question on Project Eve. Review: https://studio.synap.live/proposals/…)
307
+
308
+ If the creation was auto-approved (entity.create is on the whitelist), there's no proposal; just show a link to the entity:
309
+
310
+ > (Logged as question → https://studio.synap.live/entities/ent_question_1)
311
+
312
+ ## Linking — the core principle
313
+
314
+ **Never create orphan entities.** A task alone is near-useless. A task linked to a project, an assignee, and the source document shows up in traversals, context panels, and downstream queries.
315
+
316
+ Two ways to connect. Pick one:
317
+
318
+ **Way 1 — entity_id properties (fast path, auto-syncs).** Set the property when creating the entity. For system profiles this auto-creates a row in the relations table.
319
+
320
+ ```json
321
+ POST /api/hub/entities
322
+ {
323
+ "userId": "{userId}",
324
+ "workspaceId": "{workspaceId}",
325
+ "profileSlug": "task",
326
+ "title": "Design new onboarding flow",
327
+ "properties": {
328
+ "status": "todo",
329
+ "priority": "high",
330
+ "projectId": "ent_abc", // auto-creates belongs_to_project relation
331
+ "assignee": "usr_def" // auto-creates assigned_to relation
332
+ }
333
+ }
334
+ ```
335
+
336
+ **Way 2 — explicit relations.** For custom links, after-the-fact connections, or anything without a matching entity_id property.
337
+
338
+ ```json
339
+ POST /api/hub/relations
340
+ {
341
+ "userId": "{userId}",
342
+ "sourceEntityId": "ent_task",
343
+ "targetEntityId": "ent_document",
344
+ "type": "references"
345
+ }
346
+ ```
347
+
348
+ For auto-sync mapping, conventional relation types, and edge cases, read **`linking.md`**.
349
+
350
+ ## Writing — governance in one paragraph
351
+
352
+ Every write returns a `status` field:
353
+
354
+ ```
355
+ "approved" → done, use { id }
356
+ "proposed" → queued for user approval; response also carries { proposalId, summary, reasoning, reviewPath, reviewUrl } — surface the link
357
+ "denied" → blocked, explain reason to user
358
+ ```
359
+
360
+ **`"proposed"` is not an error.** It's the governance system queueing your change. When you get it:
361
+
362
+ 1. Tell the user exactly what was queued — use the `summary` field **verbatim**. Don't paraphrase.
363
+ 2. Give them the link to review — `reviewUrl` opens the proposal in Synap Studio. Show the link as-is.
364
+ 3. Move on with the conversation. Don't wait or poll.
365
+
366
+ Example response to the user:
367
+
368
+ > I queued **Delete task "Q2 plan review"** for your review. Destructive actions need your approval. Open it: https://studio.synap.live/proposals/prp_abc
369
+
370
+ Auto-approved by default (for agent API keys): `entity.create`, `entity.update`, `document.create`, `relation.create`, `view.create`, `profile.create`, `property_def.create`, `channel.create`, `memory.*`, all reads. Destructive actions (`delete`, `archive`, `purge`) always propose in agent-owned workspaces.
371
+
372
+ For the full whitelist, agent-user semantics, and workspace overrides, read **`governance.md`**.
373
+
374
+ ## Core writes
375
+
376
+ ### Create an entity (always with links)
377
+
378
+ ```json
379
+ POST /api/hub/entities
380
+ {
381
+ "userId": "{userId}",
382
+ "workspaceId": "{workspaceId}",
383
+ "profileSlug": "task", // from /profiles — never guess
384
+ "title": "Weekly team sync",
385
+ "properties": { "status": "todo", "projectId": "ent_..." }
386
+ }
387
+ ```
388
+
389
+ ### Update an entity
390
+
391
+ ```json
392
+ PATCH /api/hub/entities/{entityId}
393
+ { "title": "…", "properties": { "status": "done" } }
394
+ ```
395
+
396
+ ### Create a document (markdown, attached to an entity)
397
+
398
+ ```json
399
+ POST /api/hub/documents
400
+ {
401
+ "userId": "{userId}",
402
+ "workspaceId": "{workspaceId}",
403
+ "title": "Meeting notes — 2026-04-20",
404
+ "content": "# Attendees\n- …\n\n# Decisions\n- …",
405
+ "entityId": "ent_event_..." // attach to an entity for context
406
+ }
407
+ ```
408
+
409
+ The reverse lookup is `entities WHERE documentId = ?`. Always attach the document to a meaningful entity (the meeting event, the project, the person) — a floating document is another orphan.
410
+
411
+ ### Store a fact (memory) — use sparingly
412
+
413
+ ```json
414
+ POST /api/hub/memory
415
+ { "userId": "{userId}", "fact": "User prefers async communication over meetings" }
416
+ ```
417
+
418
+ Always auto-approved. **Memory is for loose, unstructured, hard-to-title facts only.** The seductive thing about memory is it has zero friction — no dedup, no linking, no proposals. That makes it easy to misuse.
419
+
420
+ **The test:** if the user later asked "show me all X," can memory answer? Memory can only keyword-match — it has no structure. So:
421
+
422
+ | Input | Use |
423
+ | ------------------------------------------------------- | --------------------------------------------------------------- |
424
+ | "User prefers async communication" | memory — it's a preference |
425
+ | "Garage code is 4321" | memory — throwaway fact |
426
+ | "Should we use LangGraph or CrewAI for Eve?" | **entity `question`** — substantive inquiry, start of flow |
427
+ | "Here's what I found comparing LangGraph and CrewAI…" | **entity `research`** — investigation with sources + conclusion |
428
+ | "We decided to use LangGraph over OpenClaude's native…" | **entity `decision`** — has title, rationale, project |
429
+ | "Key insight: tasks need better retry logic" | **entity `note` with tag "insight"** + link to project |
430
+ | "John is now head of engineering at Acme" | **update `contact` entity** — that's a property change |
431
+ | "Launch date moved to May 15" | **update `project` entity** — change the startDate |
432
+ | "Action item from meeting: ship MVP by Friday" | **entity `task`** linked to the `event` (meeting) |
433
+ | "Agreed with Sarah: we'll split backend & frontend" | **entity `decision`** linked to Sarah + the project |
434
+
435
+ **Rule of thumb:** if it has a title-worthy noun OR context to link to (a project, a person, a meeting) OR a lifecycle (status/supersession) — it's an entity, not memory. Memory is the fallback, not the default.
436
+
437
+ **For decisions specifically** — use the `decision` system profile:
438
+
439
+ ```json
440
+ POST /api/hub/entities
441
+ {
442
+ "userId": "{userId}",
443
+ "profileSlug": "decision",
444
+ "title": "Use LangGraph orchestrator over OpenClaude native",
445
+ "properties": {
446
+ "decisionStatus": "accepted",
447
+ "decidedAt": "2026-04-20",
448
+ "summary": "Dedicated orchestrator service; OpenClaude CLI as UX",
449
+ "rationale": "Separates the Orchestration Brain (LangGraph) from the UX (OpenClaude CLI).",
450
+ "alternatives": "Standardize entirely on OpenClaude's multi-agent logic.",
451
+ "projectId": "ent_project_eve"
452
+ }
453
+ }
454
+ ```
455
+
456
+ This creates a first-class decision entity linked to Project Eve. It shows up in traversals, can be superseded later (`supersededBy: newDecisionId`), and survives governance. Memory can't do any of that.
457
+
458
+ ### Post to the user's personal channel
459
+
460
+ ```
461
+ GET /api/hub/channels/personal?userId={userId}&workspaceId={workspaceId}
462
+ → { id, name, … } (get-or-create, needs hub-protocol.write scope)
463
+
464
+ POST /api/hub/threads/{threadId}/messages
465
+ { "userId": "{userId}", "role": "user", "content": "…" }
466
+ ```
467
+
468
+ ## Reading
469
+
470
+ Graph-based, not semantic. Type filter → relations → neighborhood.
471
+
472
+ ```
473
+ # Keyword search across everything (entities, documents, views, threads)
474
+ GET /api/hub/search?query={query}&userId={SYNAP_USER_ID}&workspaceId={id}
475
+
476
+ # Entities of a specific type (q= is the param for entities endpoint)
477
+ GET /api/hub/entities?q={query}&profileSlug={slug}&workspaceId={id}
478
+
479
+ # Recent entities
480
+ GET /api/hub/entities?sort=updatedAt:desc&limit=20&workspaceId={id}
481
+
482
+ # The full connected neighborhood of an entity (prefer this)
483
+ GET /api/hub/entities/{id}/connections?userId={userId}&workspaceId={id}
484
+ → { connections: [{ entityId, entity, label, direction,
485
+ source: "graph"|"property"|"thread" }],
486
+ counts: { total, graph, structural, threads } }
487
+
488
+ # BFS traversal (expensive at depth 3+)
489
+ GET /api/hub/graph/traverse?entityId={id}&maxDepth=2&workspaceId={id}
490
+
491
+ # Memory facts (keyword)
492
+ GET /api/hub/memory?userId={userId}&query={keywords}
493
+ ```
494
+
495
+ No SQL joins. The graph is the join.
496
+
497
+ ## Multi-entity capture from free-form text
498
+
499
+ When the user pastes a block of unstructured content (a meeting transcript, an email, a LinkedIn bio), use the capture pipeline instead of chaining manual creates:
500
+
501
+ ```
502
+ POST /api/hub/capture/structure → returns proposals + relations
503
+ POST /api/hub/capture/execute → commits (after user confirms)
504
+ ```
505
+
506
+ The pipeline extracts multiple entities with their relations in one LLM call. Read **`capture.md`** for the full flow.
507
+
508
+ ## Worked examples
509
+
510
+ ### Example 1 — "Remind me to send the proposal to Acme on Friday"
511
+
512
+ 1. Search for the Acme entity: `GET /entities?q=Acme&profileSlug=company` → got `ent_acme`
513
+ 2. Search for an existing task: `GET /entities?q=proposal&profileSlug=task&workspaceId=…` → none
514
+ 3. Create the task with links:
515
+
516
+ ```json
517
+ POST /api/hub/entities
518
+ { "userId": "{userId}", "workspaceId": "{wsId}",
519
+ "profileSlug": "task",
520
+ "title": "Send proposal to Acme",
521
+ "properties": {
522
+ "status": "todo", "priority": "high",
523
+ "dueDate": "2026-04-24"
524
+ }
525
+ }
526
+ ```
527
+
528
+ 4. Link to Acme (Acme is not an entity_id property on task — use Way 2):
529
+
530
+ ```json
531
+ POST /api/hub/relations
532
+ { "userId": "{userId}",
533
+ "sourceEntityId": "ent_new_task",
534
+ "targetEntityId": "ent_acme",
535
+ "type": "related_to" }
536
+ ```
537
+
538
+ 5. Confirm: "Task created and linked to Acme, due Friday."
539
+
540
+ ### Example 2 — "Who's Sarah at Acme?"
541
+
542
+ 1. Search person: `GET /entities?q=Sarah&profileSlug=person` → `ent_sarah`
543
+ 2. Pull her connections: `GET /entities/ent_sarah/connections` → company=Acme, 3 recent emails, 1 meeting
544
+ 3. Answer from the returned data, not from your own context.
545
+
546
+ ### Example 3 — "Save this article for later: https://…"
547
+
548
+ 1. Search for existing bookmark: `GET /entities?q=<url>&profileSlug=article` → none
549
+ 2. Create an article entity:
550
+
551
+ ```json
552
+ POST /api/hub/entities
553
+ { "userId": "{userId}", "workspaceId": "{wsId}",
554
+ "profileSlug": "article",
555
+ "title": "<page title>",
556
+ "properties": { "url": "<url>", "domain": "<host>" }
557
+ }
558
+ ```
559
+
560
+ 3. If the user said why ("interesting for the onboarding project"), also create a relation to that project — never drop the reason as a plain comment, turn it into a link.
561
+
562
+ ## CRM Workspaces — 4-Entity Model
563
+
564
+ Some workspaces use a CRM data structure with four entities: `person` and `company` (identity records), `deal` (pipeline record), and `client` (post-win relationship marker). Understand this pattern when proposing lead captures, deal updates, or campaign membership.
565
+
566
+ **The model:**
567
+
568
+ - **`person` + `company`** — Identity only, no sales state. Persist across deals.
569
+ - **`deal`** — Pipeline record with `dealStage` property (lead, contacted, qualifying, proposal, negotiating, won, lost, inactive). Represents what people often call a "lead" (when stage=lead). Linked to person/company via `linked_to_deal` relation.
570
+ - **`client`** — Post-win relationship marker. Created automatically when deal transitions to stage=won. Status: active, paused, or churned. Linked via `is_client` (party → client) and `produced_by_deal` (deal → client).
571
+ - **`journey`** — Documents anchored to a deal (not a person). Linked via `has_journey`.
572
+
573
+ **AI behavior — lead capture:**
574
+
575
+ When the user describes a lead (inbound person, prospect, or company lead), propose the full bundle:
576
+
577
+ 1. Create `person` entity (if not exists) with email, role, company name
578
+ 2. Create `company` entity (if not exists)
579
+ 3. Create `deal` entity with `dealStage: "lead"` and `estimatedValue` (if known)
580
+ 4. Create `linked_to_deal` relation connecting person/company to deal
581
+
582
+ Never create a person with a sales-state flag. The deal is the lead container.
583
+
584
+ ```json
585
+ POST /api/hub/entities
586
+ { "userId": "{userId}", "workspaceId": "{wsId}",
587
+ "profileSlug": "person",
588
+ "title": "Alice Johnson",
589
+ "properties": { "email": "alice@acme.com", "role": "VP Engineering" }
590
+ }
591
+
592
+ POST /api/hub/entities
593
+ { "userId": "{userId}", "workspaceId": "{wsId}",
594
+ "profileSlug": "deal",
595
+ "title": "Acme prospect",
596
+ "properties": { "dealStage": "lead", "estimatedValue": 50000 }
597
+ }
598
+
599
+ POST /api/hub/relations
600
+ { "userId": "{userId}",
601
+ "sourceEntityId": "ent_person_alice",
602
+ "targetEntityId": "ent_deal_acme",
603
+ "type": "linked_to_deal"
604
+ }
605
+ ```
606
+
607
+ **AI behavior — moving deals to won:**
608
+
609
+ When a deal transitions to `dealStage: "won"`, also propose client creation if not yet linked:
610
+
611
+ ```json
612
+ PATCH /api/hub/entities/ent_deal_acme
613
+ { "properties": { "dealStage": "won" } }
614
+
615
+ POST /api/hub/entities
616
+ { "userId": "{userId}", "workspaceId": "{wsId}",
617
+ "profileSlug": "client",
618
+ "title": "Acme (active)",
619
+ "properties": { "clientStatus": "active" }
620
+ }
621
+
622
+ POST /api/hub/relations
623
+ { "userId": "{userId}",
624
+ "sourceEntityId": "ent_person_alice",
625
+ "targetEntityId": "ent_client_acme",
626
+ "type": "is_client"
627
+ }
628
+
629
+ POST /api/hub/relations
630
+ { "userId": "{userId}",
631
+ "sourceEntityId": "ent_deal_acme",
632
+ "targetEntityId": "ent_client_acme",
633
+ "type": "produced_by_deal"
634
+ }
635
+ ```
636
+
637
+ **AI behavior — campaign membership:**
638
+
639
+ When the user describes campaign members (segment for outreach, tracking, or automation), use polymorphic `member_of` relations. Members can be persons, companies, or deals:
640
+
641
+ ```json
642
+ POST /api/hub/relations
643
+ { "userId": "{userId}",
644
+ "sourceEntityId": "ent_person_alice",
645
+ "targetEntityId": "ent_campaign_enterprise",
646
+ "type": "member_of"
647
+ }
648
+
649
+ // Same relation type, different entity type
650
+ POST /api/hub/relations
651
+ { "userId": "{userId}",
652
+ "sourceEntityId": "ent_deal_acme",
653
+ "targetEntityId": "ent_campaign_enterprise",
654
+ "type": "member_of"
655
+ }
656
+ ```
657
+
658
+ **Property names:**
659
+
660
+ - `dealStage` (not `crmStatus` or `status`) — values: lead, contacted, qualifying, proposal, negotiating, won, lost, inactive
661
+ - `clientStatus` (post-win only) — values: active, paused, churned
662
+ - Identities (person, company) carry no sales state
663
+
664
+ **Why separate identity from state:**
665
+
666
+ This model enables renewals (new deal linking to existing client), multi-stakeholder deals (multiple `linked_to_deal` relations per deal), campaigns with mixed entity types (persons + companies + deals as members), and clean churn tracking. It matches Synap's core pattern: entities + relations = graph.
667
+
668
+ ## Common mistakes
669
+
670
+ 1. **Creating orphan entities.** Always connect to at least one other entity on creation. Search first; if nothing links, reconsider whether this should be memory.
671
+ 2. **Guessing profile slugs.** Always `GET /profiles` first. `deal`, `capture`, and custom profiles may not exist in this workspace.
672
+ 3. **Using the deprecated `type` field.** Always `profileSlug`.
673
+ 4. **Treating `"proposed"` as an error.** It's a governance queue.
674
+ 5. **Forcing `source` to bypass governance.** Governance is determined by the agent user + whitelist, not by `source`. Don't set it.
675
+ 6. **Not knowing your userId.** Use `{SYNAP_USER_ID}` from the env (set by `synap connect`). Or call `GET /api/hub/users/me` → `.id` once and cache it. Never hardcode or guess.
676
+ 7. **Skipping the search step.** Duplicates degrade the graph more than missing data.
677
+ 8. **Forgetting that `GET /channels/personal` needs `hub-protocol.write`** scope — it's get-or-create, not a pure read.
678
+
679
+ ## AI Inline Patterns — reference entities in your replies
680
+
681
+ When the user is interacting with Synap's AI Companion (the in-browser chat panel), you can embed **inline chips** directly in your reply text. These render as clickable buttons the user can tap to open entities, views, or documents without leaving the conversation.
682
+
683
+ ### Syntax
684
+
685
+ | Pattern | Renders as | Effect |
686
+ | ---------------------------- | --------------------------- | --------------------------------- |
687
+ | `[[entity:UUID\|Name]]` | Purple entity chip | Opens entity detail in side panel |
688
+ | `[[view:UUID\|Name]]` | Blue view chip | Opens view |
689
+ | `[[open:side\|view:UUID]]` | Amber "Open in side" button | Opens view in side panel |
690
+ | `[[open:main\|view:UUID]]` | Amber "Open" button | Opens view in main panel |
691
+ | `[[open:side\|entity:UUID]]` | Amber "Open in side" button | Opens entity in side panel |
692
+ | `[[run:UUID\|Label]]` | Green "Run" button | Navigates to automation entity |
693
+ | `[[doc:UUID\|Name]]` | Gray doc chip | Opens document |
694
+
695
+ ### Rules
696
+
697
+ - **Always use real IDs.** Never hallucinate UUIDs. Only emit patterns for entities/views you just created or retrieved via Hub Protocol.
698
+ - **Emit after creation.** When you create a view or entity, immediately reference it: `"Created your pipeline → [[view:abc123|Active Tasks]]"`
699
+ - **Prefer side panel.** Use `[[open:side|view:UUID]]` so the user keeps their current context.
700
+ - **Only in Companion replies.** These patterns are silently ignored in non-companion channels, documents, and memory. Do not use them there.
701
+ - **Combine with prose.** Don't lead with a chip — embed it naturally: `"Here are your open deals → [[view:xyz|Deals Pipeline]] · [[open:side|view:xyz]]"`
702
+
703
+ ## When you need more
704
+
705
+ - Linking conventions, auto-sync table, relation types → **`linking.md`**
706
+ - Full governance whitelist, proposal lifecycle, agent users → **`governance.md`**
707
+ - Unstructured capture pipeline → **`capture.md`**
708
+ - Extending the data model (new profiles, new properties) → install the **`synap-schema`** skill
709
+ - Building views, dashboards, and bento layouts → install the **`synap-ui`** skill
710
+
711
+ ## ViewFrame Cells — Custom View Generation
712
+
713
+ ViewFrame is the standard way to create custom data visualizations in Synap. Use it whenever an existing cell (table, kanban, list, chart) does not cover the needed chart type, 3D layout, map, or bespoke AI-generated UI.
714
+
715
+ ### When to Use ViewFrame
716
+
717
+ | Situation | Action |
718
+ | ---------------------------------------------------------------------- | ----------------------------- |
719
+ | An existing cell or view type covers the need | Use the existing cell or view |
720
+ | User asks for a specific chart type, map, 3D scene, or custom layout | Generate a ViewFrame widget |
721
+ | User says "show X as a [funnel / heatmap / treemap / scatter / globe]" | Generate a ViewFrame widget |
722
+
723
+ ### What ViewFrame Is
724
+
725
+ - A sandboxed iframe that renders any ES module (React component or plain JS module)
726
+ - Dependencies resolved at runtime via **esm.sh import maps** — no build step
727
+ - The host injects a `SynapWidget` bridge for data access (same postMessage protocol as iframe widgets)
728
+ - Security: `sandbox="allow-scripts allow-modals allow-popups"`, no `allow-same-origin`, no cookies, no pod token
729
+
730
+ ### Register a Widget via the Hub Protocol
731
+
732
+ List installed widgets:
733
+
734
+ ```
735
+ GET /api/hub/widget-definitions?workspaceId={workspaceId}
736
+ Authorization: Bearer {SYNAP_HUB_API_KEY}
737
+ ```
738
+
739
+ Install a new ViewFrame widget (creates a proposal for user review):
740
+
741
+ ```
742
+ POST /api/hub/widget-definitions
743
+ Authorization: Bearer {SYNAP_HUB_API_KEY}
744
+ Content-Type: application/json
745
+
746
+ {
747
+ "userId": "{SYNAP_USER_ID}",
748
+ "workspaceId": "{workspaceId}",
749
+ "typeKey": "deal-stage-funnel",
750
+ "name": "Deal Stage Funnel",
751
+ "description": "Funnel chart of deal pipeline stages",
752
+ "rendererType": "iframe",
753
+ "rendererSource": "<full HTML document — see template below>",
754
+ "defaultSize": { "w": 8, "h": 6 },
755
+ "category": "visualization"
756
+ }
757
+ ```
758
+
759
+ `typeKey` must be kebab-case matching `/^[a-z][a-z0-9-]+$/`. Use a descriptive name specific to the widget's content.
760
+
761
+ The response carries `status: "ok"` (installed immediately) or `status: "proposed"` (queued for user review — surface `reviewUrl` to the user).
762
+
763
+ ### The SynapWidget Bridge (inside the iframe)
764
+
765
+ `window.SynapWidget` is injected automatically — do NOT import or `<script>` it.
766
+
767
+ ```js
768
+ SynapWidget.onInit(async ({ config, context }) => {
769
+ // context: { workspaceId, viewId?, entityId?, sdkVersion }
770
+ const items = await SynapWidget.query("list_entities", {
771
+ profileSlug: "deal", // or 'task', 'person', 'company', any custom slug
772
+ limit: 200,
773
+ });
774
+ render(items ?? []);
775
+ SynapWidget.resize(document.body.scrollHeight);
776
+ });
777
+ ```
778
+
779
+ All `query()` calls return a Promise. Entity shape: `{ id, title, profileSlug, properties, createdAt, … }`.
780
+
781
+ Navigation and notifications:
782
+
783
+ ```js
784
+ SynapWidget.navigate({ entityId: "entity-uuid" }); // opens entity detail
785
+ SynapWidget.toast("Done!", "success"); // 'success' | 'error' | 'info'
786
+ ```
787
+
788
+ ### Common Dependency Patterns (esm.sh import map)
789
+
790
+ ```html
791
+ <script type="importmap">
792
+ {
793
+ "imports": {
794
+ "react": "https://esm.sh/react@19",
795
+ "react-dom/client": "https://esm.sh/react-dom@19/client",
796
+ "react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime",
797
+ "recharts": "https://esm.sh/recharts@2.12.0"
798
+ }
799
+ }
800
+ </script>
801
+ ```
802
+
803
+ Common library choices:
804
+
805
+ | Category | Packages |
806
+ | -------- | -------------------------------------------------------------- |
807
+ | Data viz | `recharts@2.12.0`, `d3@7`, `chart.js@4`, `observable-plot@0.6` |
808
+ | Tables | `@tanstack/react-table@8` |
809
+ | 3D | `three@0.165.0`, `@react-three/fiber@8`, `@react-three/drei@9` |
810
+ | Maps | `leaflet@1.9.4`, `react-leaflet@4` |
811
+
812
+ ### Minimal Widget Template
813
+
814
+ ```html
815
+ <!DOCTYPE html>
816
+ <html>
817
+ <head>
818
+ <meta charset="utf-8" />
819
+ <style>
820
+ * {
821
+ box-sizing: border-box;
822
+ margin: 0;
823
+ }
824
+ body {
825
+ font-family: -apple-system, sans-serif;
826
+ padding: 16px;
827
+ background: transparent;
828
+ }
829
+ </style>
830
+ <script type="importmap">
831
+ {
832
+ "imports": {
833
+ "react": "https://esm.sh/react@19",
834
+ "react-dom/client": "https://esm.sh/react-dom@19/client",
835
+ "react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime"
836
+ }
837
+ }
838
+ </script>
839
+ </head>
840
+ <body>
841
+ <div id="root"></div>
842
+ <script type="module">
843
+ import { createRoot } from "react-dom/client";
844
+
845
+ SynapWidget.onInit(async ({ context }) => {
846
+ const items = await SynapWidget.query("list_entities", {
847
+ profileSlug: "deal",
848
+ limit: 200,
849
+ }).catch(() => []);
850
+
851
+ // Build your UI here. Plain DOM or React both work.
852
+ document.getElementById("root").textContent =
853
+ `Loaded ${(items ?? []).length} deals`;
854
+
855
+ SynapWidget.resize(document.body.scrollHeight);
856
+ });
857
+ </script>
858
+ </body>
859
+ </html>
860
+ ```
861
+
862
+ ### Rules
863
+
864
+ - **Always call `SynapWidget.onInit()`** — the host will not send data until you register this handler.
865
+ - **Call `SynapWidget.resize()`** after rendering to prevent clipping.
866
+ - **Handle errors** — `query()` can fail; always `.catch()`.
867
+ - **Transparent background** — `background: transparent` on `body` inherits the host surface color.
868
+ - **No external fetch** — the sandbox has no cross-origin access; all data must go through `SynapWidget`.
869
+ - **Inline all styles** — no external CSS imports; CDN JS via import map is fine.
870
+
871
+ ---
872
+
873
+ ## Authentication
874
+
875
+ ```
876
+ Authorization: Bearer {SYNAP_HUB_API_KEY}
877
+ X-Workspace-Id: {workspaceId} (optional; also pass in body/query)
878
+
879
+ Scopes:
880
+ hub-protocol.read → most GET endpoints
881
+ hub-protocol.write → all writes AND GET /channels/personal
882
+ ```