@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,170 @@
1
+ # Capture pipeline — reference
2
+
3
+ Use the capture pipeline when the user pastes a block of unstructured text (email, meeting transcript, LinkedIn bio, article, screenshot of a whiteboard) and you need to extract multiple entities with their relations.
4
+
5
+ Chaining manual `POST /entities` + `POST /relations` for this is wasteful — the pipeline extracts everything in one server-side LLM call and returns proposals you can review before committing.
6
+
7
+ ## Two-step flow
8
+
9
+ ### 1. Structure the text
10
+
11
+ ```json
12
+ POST /api/hub/capture/structure
13
+ {
14
+ "userId": "{userId}",
15
+ "workspaceId": "{workspaceId}",
16
+ "text": "Had lunch with Sarah Chen from Acme Corp today. \
17
+ She's leading their Series B and asked me to send \
18
+ our sales deck by Friday. Mentioned her VP Engineering \
19
+ Tom Walker also wants a demo.",
20
+ "context": { "source": "chat", "channelId": "ch_…" }, // optional
21
+ "profileSlugs": ["person", "company", "task", "event"] // optional filter
22
+ }
23
+ ```
24
+
25
+ Response:
26
+
27
+ ```json
28
+ {
29
+ "proposals": [
30
+ {
31
+ "tempId": "t1",
32
+ "profileSlug": "person",
33
+ "title": "Sarah Chen",
34
+ "properties": { "companyId": "t2" },
35
+ "action": "create"
36
+ },
37
+ {
38
+ "tempId": "t2",
39
+ "profileSlug": "company",
40
+ "title": "Acme Corp",
41
+ "properties": {},
42
+ "action": "create"
43
+ },
44
+ {
45
+ "tempId": "t3",
46
+ "profileSlug": "person",
47
+ "title": "Tom Walker",
48
+ "properties": { "role": "VP Engineering", "companyId": "t2" },
49
+ "action": "create"
50
+ },
51
+ {
52
+ "tempId": "t4",
53
+ "profileSlug": "task",
54
+ "title": "Send sales deck to Sarah",
55
+ "properties": {
56
+ "status": "todo",
57
+ "dueDate": "2026-04-24",
58
+ "assignee": null
59
+ },
60
+ "action": "create"
61
+ },
62
+ {
63
+ "tempId": "t5",
64
+ "profileSlug": "event",
65
+ "title": "Lunch with Sarah",
66
+ "properties": { "startDate": "2026-04-20", "attendees": ["t1"] },
67
+ "action": "create"
68
+ }
69
+ ],
70
+ "relations": [
71
+ {
72
+ "sourceTempId": "t4",
73
+ "targetTempId": "t1",
74
+ "relationType": "for_person"
75
+ },
76
+ {
77
+ "sourceTempId": "t4",
78
+ "targetTempId": "t5",
79
+ "relationType": "from_meeting"
80
+ }
81
+ ],
82
+ "followUp": "Should I also schedule a reminder to follow up with Tom?"
83
+ }
84
+ ```
85
+
86
+ **`tempId` is only valid within this response.** It's used to link proposals together before they have real entity IDs.
87
+
88
+ **`action` can be:**
89
+
90
+ - `"create"` — new entity
91
+ - `"update"` — existing entity found by title match; `existingEntityId` will be set
92
+ - `"skip"` — duplicate already exists, nothing to do
93
+
94
+ ### 2. Execute (commit) after user confirmation
95
+
96
+ Present the proposals to the user. Let them edit, remove, or add items. Then:
97
+
98
+ ```json
99
+ POST /api/hub/capture/execute
100
+ {
101
+ "userId": "{userId}",
102
+ "workspaceId": "{workspaceId}",
103
+ "entities": [ /* edited proposals array */ ],
104
+ "relations": [ /* edited relations array */ ]
105
+ }
106
+ ```
107
+
108
+ Response:
109
+
110
+ ```json
111
+ {
112
+ "created": [{ "tempId": "t1", "entityId": "ent_real_1" }, …],
113
+ "updated": [{ "tempId": "t2", "entityId": "ent_existing" }, …],
114
+ "skipped": [{ "tempId": "t5", "reason": "duplicate" }],
115
+ "relations": { "created": 2, "failed": 0 },
116
+ "status": "approved"
117
+ }
118
+ ```
119
+
120
+ The executor goes through governance (see `governance.md`). Expect the same three `status` values.
121
+
122
+ ## When to use the pipeline vs. manual CRUD
123
+
124
+ | Situation | Use |
125
+ | ---------------------------------------------------------------- | --------------------------------------------------- |
126
+ | User pastes a chunk of text with multiple people/companies/tasks | Pipeline |
127
+ | User says "create a task to call Sarah at 3pm" | Manual create (1 entity) |
128
+ | User forwards an email | Pipeline |
129
+ | User asks "remind me to X" | Manual create |
130
+ | OCR'd screenshot, meeting transcript, article body | Pipeline |
131
+ | User edits an existing entity | Manual PATCH |
132
+ | User wants to save a webpage they're on | Manual create (article) + optional capture for body |
133
+
134
+ Rule of thumb: **one intent, one call.** Multi-entity content, use the pipeline.
135
+
136
+ ## Dedup within the pipeline
137
+
138
+ The pipeline does its own duplicate check per entity (by title + profileSlug within workspace). When it finds a match, `action` becomes `"update"` with `existingEntityId` set, so the real entity is updated in place, not duplicated.
139
+
140
+ You can still prefix with a manual search if you want to show the user "I found 3 existing people that might match" before running capture. But the pipeline alone is usually good enough.
141
+
142
+ ## Presenting proposals to the user
143
+
144
+ A good UX turn:
145
+
146
+ > I picked up **5 things** from that paste:
147
+ >
148
+ > - **Sarah Chen** — person at Acme Corp (new)
149
+ > - **Acme Corp** — company (new)
150
+ > - **Tom Walker** — person at Acme (new)
151
+ > - **Task**: Send sales deck to Sarah, due Friday
152
+ > - **Event**: Lunch with Sarah today
153
+ >
154
+ > Plus 2 connections: task → Sarah, task → lunch.
155
+ >
156
+ > Ship it?
157
+
158
+ Keep the user in control. Don't auto-execute without confirmation unless the user explicitly told you to.
159
+
160
+ ## Edge cases
161
+
162
+ - **Empty text → empty proposals.** Don't call execute with zero entities.
163
+ - **Ambiguous references ("he", "they").** The pipeline will often return null properties. Prompt the user to clarify before executing.
164
+ - **Dates.** Always absolute in responses (ISO 8601). If the input says "Friday," the pipeline resolves it against today.
165
+ - **Custom profiles.** If you want the pipeline to consider custom profiles, pass `profileSlugs` in the request. Otherwise it defaults to system profiles only.
166
+ - **Rate limits.** 20 captures/hour per user. If you hit the limit, fall back to manual CRUD.
167
+
168
+ ## Followup question
169
+
170
+ The response includes a `followUp` field — a single sentence question the pipeline thinks is worth asking next. Surface it to the user verbatim. It's a cheap way to deepen capture without extra prompts.
@@ -0,0 +1,206 @@
1
+ # Governance — reference
2
+
3
+ Synap queues some agent writes for human approval. This file covers what triggers a proposal, the auto-approve whitelist, how to handle each response status, and agent-user semantics.
4
+
5
+ ## Response statuses
6
+
7
+ Every write endpoint returns a `status` field. Handle all three.
8
+
9
+ ```json
10
+ { "status": "approved", "id": "ent_…", "message": "…" }
11
+ → The write committed. Use { id } normally.
12
+
13
+ {
14
+ "status": "proposed",
15
+ "proposalId": "prp_…",
16
+ "summary": "Delete task \"Q2 plan review\"",
17
+ "reasoning": "Destructive actions require your approval",
18
+ "reviewPath": "/proposals/prp_…",
19
+ "reviewUrl": "https://studio.synap.live/proposals/prp_…",
20
+ "message": "…"
21
+ }
22
+ → Queued for user review. Surface `summary` + `reviewUrl` to the user; do not retry.
23
+
24
+ { "status": "denied", "reason": "…" }
25
+ → Policy blocked the write. Explain the reason. Do not retry.
26
+ ```
27
+
28
+ **`"proposed"` is not an error.** If your retry logic treats it as one, the system will create duplicate proposals.
29
+
30
+ ## What triggers a proposal
31
+
32
+ Two independent factors:
33
+
34
+ ### 1. Agent identity
35
+
36
+ Agent identity is determined by the API key. An agent-owned Hub API key (created via `POST /api/hub/setup/agent`) automatically attributes all requests to the agent user — no `agentUserId` field is needed in request bodies. The middleware resolves the agent from the key and tags writes with `source: "agent"`.
37
+
38
+ If the key belongs to a human user (personal API key), writes go through unless the workspace has `aiGovernance.forceProposals = true`.
39
+
40
+ ### 2. Action type
41
+
42
+ The auto-approve whitelist is a list of `subjectType.action` pairs. For example `entity.create`, `document.read`, `view.create`.
43
+
44
+ ## Default auto-approve whitelist
45
+
46
+ Agents can perform these actions directly without a proposal:
47
+
48
+ **Reads** (always, no gating):
49
+
50
+ - `search.*`
51
+ - `entity.read`, `document.read`, `relation.read`
52
+ - `memory.recall`
53
+ - `context.*`
54
+ - `filesystem.read`
55
+
56
+ **Writes** (auto-approved by default):
57
+
58
+ - `entity.create`, `entity.update`
59
+ - `relation.create`
60
+ - `document.create`
61
+ - `memory.store` (always auto-approved, never proposed)
62
+ - `view.create`
63
+ - `profile.create`, `profile.update`
64
+ - `property_def.create`, `property_def.update`
65
+ - `channel.create`
66
+ - `bento.arrange`
67
+ - `filesystem.write_workspace` (OpenClaw sandbox only)
68
+
69
+ **Proposal-gated** (always require approval when done by agents):
70
+
71
+ - `entity.delete`, `entity.archive`, `entity.purge`
72
+ - `view.update`, `view.delete`
73
+ - `profile.delete`
74
+ - `property_def.delete`
75
+ - `workspace.create`, `workspace.update`, `workspace.delete`
76
+
77
+ ## Destructive action override
78
+
79
+ In **agent-owned workspaces** (workspaces where the owner is an agent user, not a human), destructive actions (`delete`, `archive`, `purge`) **always propose** even if they appear in the whitelist. This prevents an agent from wiping its own workspace without human sign-off.
80
+
81
+ ## Workspace overrides
82
+
83
+ Each workspace has an `aiGovernance` settings object that can:
84
+
85
+ - **Replace the whitelist entirely** (`aiGovernance.autoApproveFor = ["entity.read", "search.*"]`) — this becomes the full allowed list; defaults no longer apply
86
+ - **Switch to agent-owned mode** (`settings.governanceMode = "agent-owned"`) — destructive actions (delete/archive/purge) always propose regardless of whitelist
87
+ - **Change who approves** (`aiGovernance.proposalApprovalPolicy = "admins_only" | "any_editor" | "owner_and_admins"`)
88
+
89
+ Don't try to infer the workspace's policy — either check explicitly (see "Runtime introspection" below) or just write and handle whatever `status` comes back.
90
+
91
+ ## Don't set `source` manually
92
+
93
+ `source` is set by the backend based on the auth context. Setting it manually in the request body is ignored. If your API key has an agentUserId, writes are tagged `source: "agent"` automatically.
94
+
95
+ ## The proposal lifecycle
96
+
97
+ When a write is proposed:
98
+
99
+ 1. A row is created in the `proposals` table: `{ id, proposal, request, status: "pending" }`.
100
+ 2. A notification is sent to the workspace admins.
101
+ 3. The `proposed` response is returned to you with `proposalId`.
102
+ 4. A human reviews in Synap's Proposals page, approves or rejects.
103
+ 5. On approve: the request is replayed with proper permissions. On reject: the write is discarded.
104
+
105
+ You do not poll or wait. The user's experience is asynchronous.
106
+
107
+ ## Agent users
108
+
109
+ An agent user is a pod-wide user with `agentMetadata.writesRequireProposal` set. Created by `POST /api/hub/setup/agent`. Each agent user has:
110
+
111
+ - `agentType`: a free-form string (`"claude-code"`, `"openclaw"`, `"raycast"`, `"custom"`)
112
+ - `agentMetadata`: arbitrary JSON for config
113
+ - Hub API keys scoped to `hub-protocol.read` + `hub-protocol.write` + `mcp.*`
114
+
115
+ The `agentUserId` on a Hub API key binds all writes done with that key to the agent user. Multiple keys can share one agent user.
116
+
117
+ ## `userId` in request bodies
118
+
119
+ For every write that takes `userId`, pass the **human user's ID**. The agent identity is derived automatically from the API key — do not pass `agentUserId` in the body. Both IDs are tracked internally: the human appears in `createdBy`, the agent appears in `performedBy`.
120
+
121
+ ```json
122
+ POST /api/hub/entities
123
+ {
124
+ "userId": "usr_antoine", // the human user (from SYNAP_USER_ID)
125
+ "workspaceId": "ws_…",
126
+ "profileSlug": "task",
127
+
128
+ }
129
+ ```
130
+
131
+ If you pass the API key owner (often a system account) as `userId`, the entity appears in no one's feed. Always pass the real human user's ID.
132
+
133
+ ## Handling proposed writes gracefully
134
+
135
+ When a write is proposed, surface three things to the user: what was queued, why, and how to act on it. The response carries everything you need — don't invent paraphrases.
136
+
137
+ **The formula:**
138
+
139
+ ```
140
+ "I queued **{summary}** for your review. {reasoning}. Review: {reviewUrl}"
141
+ ```
142
+
143
+ **Concrete examples:**
144
+
145
+ ```
146
+ good: "I queued **Delete task \"Q2 plan review\"** for your review.
147
+ Destructive actions need your approval.
148
+ https://studio.synap.live/proposals/prp_abc"
149
+
150
+ good: "I queued **Create entity \"Acme Corp\"** for your review.
151
+ https://studio.synap.live/proposals/prp_def"
152
+
153
+ good: "Queued 3 changes for your review — I'll post the links after each:
154
+ 1. https://studio.synap.live/proposals/prp_1 — Delete task "X"
155
+ 2. https://studio.synap.live/proposals/prp_2 — Delete task "Y"
156
+ 3. https://studio.synap.live/proposals/prp_3 — Delete task "Z""
157
+
158
+ bad: "Error: write was not approved."
159
+ bad: "Something needs your attention in Synap." ← too vague
160
+ bad: "I tried to delete that but I'm not sure it worked." ← sounds like a failure
161
+ ```
162
+
163
+ **Rules:**
164
+
165
+ - Use the `summary` field **verbatim**, in bold. It's already short and human-readable.
166
+ - Include `reviewUrl` as a raw URL — let the client render it as a link.
167
+ - One line of chat per proposal. Don't bury the link in a paragraph.
168
+ - If the user asks "can you just do it?" — tell them no, it's queued; they can open the link to approve.
169
+ - **Don't wait or poll.** Move on with the conversation. The user approves in their own time; you'll see the effect when they ask you to read the data again.
170
+
171
+ **When multiple writes in one turn produce multiple proposals:** list them, one line each, with their links. Don't aggregate into a single "some stuff was queued" message.
172
+
173
+ **Clients without rich link rendering** (plain terminals, voice, SMS-style interfaces): still say the URL — the user can copy-paste it. Even if they can't click, they know where to go.
174
+
175
+ ## Runtime introspection (preferred over hardcoded rules)
176
+
177
+ When you need to know the actual policy for a workspace — e.g., to tell the user "I can create entities directly, but deleting them will need your approval" — call:
178
+
179
+ ```
180
+ GET /api/hub/workspaces/{workspaceId}/governance
181
+
182
+ → {
183
+ workspaceId: "ws_…",
184
+ effective: {
185
+ autoApproveFor: [ // the actual whitelist at this workspace
186
+ "entity.create", "entity.update", "relation.create", "document.create",
187
+ "view.create", "profile.create", "property_def.create", "memory.*", …
188
+ ],
189
+ governanceMode: "default" | "agent-owned",
190
+ proposalApprovalPolicy: "owner_and_admins" | "any_editor" | "admins_only",
191
+ destructiveAlwaysPropose: false, // true when governanceMode === "agent-owned"
192
+ destructiveActions: ["delete", "archive", "purge"]
193
+ },
194
+ source: "workspace" | "default", // where `autoApproveFor` came from
195
+ defaults: {
196
+ autoApproveFor: [ /* the backend default list, for comparison */ ]
197
+ }
198
+ }
199
+ ```
200
+
201
+ An action is auto-approved when:
202
+
203
+ - it matches an entry in `effective.autoApproveFor` (exact match or `subject.*` glob), AND
204
+ - it is NOT in `destructiveActions` when `destructiveAlwaysPropose` is true
205
+
206
+ Prefer this endpoint over the tables in this file — they snapshot the default at the time of writing and don't reflect workspace customization.
@@ -0,0 +1,128 @@
1
+ # Linking — reference
2
+
3
+ Two mechanisms connect entities in Synap. This file covers when to use which, auto-sync behaviour, and relation conventions.
4
+
5
+ ## Auto-sync table (ENTITY_ID properties → relations)
6
+
7
+ When a profile property has `valueType: "entity_id"`, setting it on entity creation or update automatically writes a row in the `relations` table. No second call needed.
8
+
9
+ Known system-profile auto-syncs:
10
+
11
+ | Profile | Property | Target profile | Auto-relation type |
12
+ | -------- | ----------- | -------------- | -------------------- |
13
+ | task | `projectId` | project | `belongs_to_project` |
14
+ | task | `assignee` | person | `assigned_to` |
15
+ | contact | `companyId` | company | `works_at` |
16
+ | deal | `contactId` | contact | `deal_for` |
17
+ | deal | `companyId` | company | `deal_with` |
18
+ | document | `entityId` | (any) | `attached_to` |
19
+ | anchor | `channelId` | (channel) | `anchored_in` |
20
+
21
+ Custom profiles: if you created a property with `valueType: "entity_id"`, the auto-sync kicks in, but the relation type defaults to `related_to` unless the profile defines a `relationDefinition`. When precision matters, create the relation explicitly (Way 2) with a specific `type`.
22
+
23
+ **Do not trust this table to stay current.** Always verify with `GET /api/hub/profiles` — the returned profile includes `properties[].valueType` and `properties[].targetProfileSlug`. Any property with `valueType: "entity_id"` is an auto-sync candidate.
24
+
25
+ ## Way 1 — set the property
26
+
27
+ ```json
28
+ POST /api/hub/entities
29
+ {
30
+ "userId": "{userId}",
31
+ "workspaceId": "{workspaceId}",
32
+ "profileSlug": "task",
33
+ "title": "Finalize Q2 plan",
34
+ "properties": {
35
+ "projectId": "ent_project_q2", // one call, two rows written
36
+ "assignee": "usr_antoine"
37
+ }
38
+ }
39
+ ```
40
+
41
+ After this single call, these all work:
42
+
43
+ ```
44
+ GET /entities/ent_project_q2/connections → task appears
45
+ GET /relations?entityId=ent_project_q2 → belongs_to_project row appears
46
+ GET /graph/traverse?entityId=ent_project_q2 → task included as node
47
+ ```
48
+
49
+ Use Way 1 whenever a matching entity_id property exists on the profile. It is cheaper and semantically typed.
50
+
51
+ ## Way 2 — explicit relations
52
+
53
+ For:
54
+
55
+ - Links between two already-existing entities (after creation)
56
+ - Custom connections where no entity_id property exists
57
+ - Cross-type links not captured by the schema (e.g., task → document, entity → bookmark)
58
+ - Links involving custom profiles without a relationDefinition
59
+
60
+ ```json
61
+ POST /api/hub/relations
62
+ {
63
+ "userId": "{userId}",
64
+ "workspaceId": "{workspaceId}",
65
+ "sourceEntityId": "ent_source",
66
+ "targetEntityId": "ent_target",
67
+ "type": "references"
68
+ }
69
+ ```
70
+
71
+ ## Conventional relation types
72
+
73
+ String-typed, case-insensitive by convention. Use these first before inventing new ones — workspace UI often renders known types specially.
74
+
75
+ | Type | Direction | When |
76
+ | -------------- | --------------- | ------------------------------------------------- |
77
+ | `related_to` | bidirectional | Generic association, no stronger label fits |
78
+ | `parent_of` | source → target | Hierarchy (project parent of sub-project) |
79
+ | `child_of` | source → target | Hierarchy (inverse of parent_of) |
80
+ | `belongs_to` | source → target | Membership |
81
+ | `authored_by` | source → target | Note/document authored by a person |
82
+ | `depends_on` | source → target | Task blocked by another task, project needs input |
83
+ | `references` | source → target | Task references a document, note cites an article |
84
+ | `mentions` | source → target | Entity mentioned within another entity |
85
+ | `works_with` | bidirectional | People who collaborate |
86
+ | `part_of` | source → target | Component relationship |
87
+ | `from_meeting` | source → target | Any entity extracted from a meeting/event |
88
+ | `anchored_in` | source → target | Anchor (pinned chat message) in a channel |
89
+
90
+ If none fits, invent a snake_case verb. Keep it short and symmetric with existing verbs. Don't create `related-to-this-specific-thing` — prefer a generic `related_to` plus a more specific property or document.
91
+
92
+ ## Decision table
93
+
94
+ | Situation | Use |
95
+ | ------------------------------------------------------ | -------------------------------------- |
96
+ | Profile has matching `valueType: "entity_id"` property | Way 1 — set the property |
97
+ | Linking two already-existing entities | Way 2 — create a relation |
98
+ | Custom connection, no matching property | Way 2 |
99
+ | Unsure whether a property exists | `GET /profiles` first |
100
+ | Linking a document to its parent entity | Use `entityId` on the document (Way 1) |
101
+ | Multi-party link (entity A ↔ entity B ↔ entity C) | Two relations, both Way 2 |
102
+
103
+ ## Reading the graph
104
+
105
+ ```
106
+ # The complete picture — graph relations + property-derived links + thread refs
107
+ GET /api/hub/entities/{id}/connections
108
+ → { connections: [{ entityId, entity, label, direction,
109
+ source: "graph"|"property"|"thread" }] }
110
+
111
+ # Raw graph relations only
112
+ GET /api/hub/relations?entityId={id}
113
+
114
+ # BFS neighborhood
115
+ GET /api/hub/graph/traverse?entityId={id}&maxDepth=2
116
+ → { nodes: Entity[], edges: Relation[] }
117
+ ```
118
+
119
+ **Prefer `/entities/{id}/connections`** when you want "everything connected to X" — it unifies property-derived links (which the raw relations table doesn't always surface for custom profiles) with graph relations and thread anchors.
120
+
121
+ `/graph/traverse` is best for "what's in the 2-hop neighborhood of this entity" — good for context gathering, but expensive at `maxDepth ≥ 3`.
122
+
123
+ ## Common mistakes
124
+
125
+ - **Creating an orphan, then forgetting to link it.** Every `POST /entities` should include properties that link, OR be immediately followed by a `POST /relations`. Never close the operation with a disconnected node.
126
+ - **Double-linking.** If you set `properties.projectId` AND also `POST /relations` with type `belongs_to_project`, the auto-sync already did it. Don't duplicate.
127
+ - **Using `related_to` when a specific verb fits.** `related_to` is the fallback. `authored_by`, `depends_on`, `references` carry more meaning to both the user and downstream views.
128
+ - **Inverting direction.** Source is "the thing doing the action or owning the relationship." `task depends_on task` means the source is blocked by the target. Check twice.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Orient: fetch identity, workspaces, and profiles in one deterministic shot.
3
+ # Requires: SYNAP_HUB_API_KEY, SYNAP_POD_URL.
4
+ # Output: single JSON object { user, workspaces, profiles }.
5
+ # Exit codes: 0 ok, 1 env missing, 2 HTTP failure.
6
+
7
+ set -euo pipefail
8
+
9
+ : "${SYNAP_HUB_API_KEY:?SYNAP_HUB_API_KEY not set}"
10
+ : "${SYNAP_POD_URL:?SYNAP_POD_URL not set}"
11
+
12
+ H="Authorization: Bearer $SYNAP_HUB_API_KEY"
13
+ POD="${SYNAP_POD_URL%/}"
14
+
15
+ user=$(curl -fsS -H "$H" "$POD/api/hub/users/me") || { echo "users/me failed" >&2; exit 2; }
16
+ user_id=$(printf '%s' "$user" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' | head -n1)
17
+
18
+ workspaces=$(curl -fsS -H "$H" "$POD/api/hub/workspaces") || { echo "workspaces failed" >&2; exit 2; }
19
+ ws_id="${SYNAP_WORKSPACE_ID:-}"
20
+ if [ -z "$ws_id" ]; then
21
+ ws_id=$(printf '%s' "$workspaces" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' | head -n1)
22
+ fi
23
+
24
+ profiles=$(curl -fsS -H "$H" "$POD/api/hub/profiles?userId=$user_id&workspaceId=$ws_id") \
25
+ || { echo "profiles failed" >&2; exit 2; }
26
+
27
+ printf '{"user":%s,"workspaces":%s,"workspaceId":"%s","profiles":%s}\n' \
28
+ "$user" "$workspaces" "$ws_id" "$profiles"