@synap-core/cli 1.10.0 → 1.11.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 (120) hide show
  1. package/dist/commands/capability.js +79 -8
  2. package/dist/commands/capability.js.map +1 -1
  3. package/dist/commands/centrality.js +2 -1
  4. package/dist/commands/centrality.js.map +1 -1
  5. package/dist/commands/context.js +2 -1
  6. package/dist/commands/context.js.map +1 -1
  7. package/dist/commands/data-extra.js +7 -4
  8. package/dist/commands/data-extra.js.map +1 -1
  9. package/dist/commands/data.js +14 -4
  10. package/dist/commands/data.js.map +1 -1
  11. package/dist/commands/diagnose.js +44 -1
  12. package/dist/commands/diagnose.js.map +1 -1
  13. package/dist/commands/digest.js +2 -1
  14. package/dist/commands/digest.js.map +1 -1
  15. package/dist/commands/doc.d.ts +14 -0
  16. package/dist/commands/doc.js +61 -4
  17. package/dist/commands/doc.js.map +1 -1
  18. package/dist/commands/doctor.d.ts +21 -0
  19. package/dist/commands/doctor.js +189 -0
  20. package/dist/commands/doctor.js.map +1 -0
  21. package/dist/commands/events.js +3 -1
  22. package/dist/commands/events.js.map +1 -1
  23. package/dist/commands/graph.js +5 -3
  24. package/dist/commands/graph.js.map +1 -1
  25. package/dist/commands/import.js +40 -9
  26. package/dist/commands/import.js.map +1 -1
  27. package/dist/commands/knowledge.d.ts +6 -0
  28. package/dist/commands/knowledge.js +253 -30
  29. package/dist/commands/knowledge.js.map +1 -1
  30. package/dist/commands/launch.js +2 -2
  31. package/dist/commands/launch.js.map +1 -1
  32. package/dist/commands/lens.js +1 -1
  33. package/dist/commands/lens.js.map +1 -1
  34. package/dist/commands/market-authoring.d.ts +45 -0
  35. package/dist/commands/market-authoring.js +225 -0
  36. package/dist/commands/market-authoring.js.map +1 -0
  37. package/dist/commands/market.d.ts +22 -7
  38. package/dist/commands/market.js +335 -60
  39. package/dist/commands/market.js.map +1 -1
  40. package/dist/commands/mcp.d.ts +24 -2
  41. package/dist/commands/mcp.js +84 -3
  42. package/dist/commands/mcp.js.map +1 -1
  43. package/dist/commands/pod.js +1 -1
  44. package/dist/commands/pod.js.map +1 -1
  45. package/dist/commands/pods.d.ts +14 -0
  46. package/dist/commands/pods.js +156 -2
  47. package/dist/commands/pods.js.map +1 -1
  48. package/dist/commands/proposals-actions.js +3 -0
  49. package/dist/commands/proposals-actions.js.map +1 -1
  50. package/dist/commands/sessions.js +2 -1
  51. package/dist/commands/sessions.js.map +1 -1
  52. package/dist/commands/skills.js +3 -1
  53. package/dist/commands/skills.js.map +1 -1
  54. package/dist/commands/template.d.ts +42 -0
  55. package/dist/commands/template.js +191 -0
  56. package/dist/commands/template.js.map +1 -0
  57. package/dist/commands/upload.d.ts +30 -0
  58. package/dist/commands/upload.js +155 -0
  59. package/dist/commands/upload.js.map +1 -0
  60. package/dist/index.js +118 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/lib/capture-lane.js +5 -3
  63. package/dist/lib/capture-lane.js.map +1 -1
  64. package/dist/lib/capture-structure.d.ts +123 -0
  65. package/dist/lib/capture-structure.js +57 -0
  66. package/dist/lib/capture-structure.js.map +1 -0
  67. package/dist/lib/cp-packages.d.ts +56 -0
  68. package/dist/lib/cp-packages.js +171 -0
  69. package/dist/lib/cp-packages.js.map +1 -1
  70. package/dist/lib/hub-client.d.ts +14 -1
  71. package/dist/lib/hub-client.js +110 -19
  72. package/dist/lib/hub-client.js.map +1 -1
  73. package/dist/lib/id.d.ts +23 -0
  74. package/dist/lib/id.js +35 -0
  75. package/dist/lib/id.js.map +1 -0
  76. package/dist/lib/pod.d.ts +53 -3
  77. package/dist/lib/pod.js +81 -9
  78. package/dist/lib/pod.js.map +1 -1
  79. package/dist/lib/template-file.d.ts +74 -0
  80. package/dist/lib/template-file.js +140 -0
  81. package/dist/lib/template-file.js.map +1 -0
  82. package/node_modules/@synap/hub-rest-client/dist/index.cjs +1261 -0
  83. package/node_modules/@synap/hub-rest-client/dist/index.d.cts +1647 -0
  84. package/node_modules/@synap/hub-rest-client/dist/index.d.ts +1647 -0
  85. package/node_modules/@synap/hub-rest-client/dist/index.js +1231 -0
  86. package/node_modules/@synap/hub-rest-client/package.json +45 -0
  87. package/node_modules/@synap/hub-rest-client/src/client.ts +1631 -0
  88. package/node_modules/@synap/hub-rest-client/src/errors.ts +30 -0
  89. package/node_modules/@synap/hub-rest-client/src/index.ts +153 -0
  90. package/node_modules/@synap/hub-rest-client/src/setup.ts +133 -0
  91. package/node_modules/@synap/hub-rest-client/src/types.ts +1299 -0
  92. package/package.json +3 -2
  93. package/skills/agent-os/SKILL.md +6 -0
  94. package/skills/manifest.json +1 -1
  95. package/skills/onboard/SKILL.md +7 -0
  96. package/skills/synap/SKILL.md +110 -29
  97. package/skills/synap/capture.md +50 -152
  98. package/skills/synap/common-mistakes.md +1 -0
  99. package/skills/synap/content-os.md +83 -0
  100. package/skills/synap/digesting-a-channel.md +25 -0
  101. package/skills/synap/mental-model.md +7 -7
  102. package/skills/synap/multi-entity-capture.md +6 -4
  103. package/skills/synap/worked-examples.md +21 -0
  104. package/skills/synap/writes.md +75 -18
  105. package/skills/synap-app/SKILL.md +319 -0
  106. package/skills/synap-app/_order.txt +5 -0
  107. package/skills/synap-app/_skill.yaml +20 -0
  108. package/skills/synap-app/creating-the-base-app.md +64 -0
  109. package/skills/synap-app/governance.md +42 -0
  110. package/skills/synap-app/install-commands.md +55 -0
  111. package/skills/synap-app/overview.md +50 -0
  112. package/skills/synap-app/using-the-sdk.md +73 -0
  113. package/skills/synap-market/SKILL.md +335 -0
  114. package/skills/synap-market/_order.txt +5 -0
  115. package/skills/synap-market/_skill.yaml +24 -0
  116. package/skills/synap-market/author-and-publish.md +75 -0
  117. package/skills/synap-market/discover-and-fetch.md +59 -0
  118. package/skills/synap-market/governance-and-catalog.md +56 -0
  119. package/skills/synap-market/install.md +55 -0
  120. package/skills/synap-market/overview.md +51 -0
@@ -22,6 +22,31 @@ Do NOT assume this channel is a CRM. Do NOT reach for a contacts/notes/links tem
22
22
 
23
23
  Every person, company, deal, or resource you are about to propose may already exist. Resolve first, create only what is new. Match on STRONG signals — email, phone, url, handle — not a fuzzy name. Run ONE batched search over all candidates, reuse on match (add the newly-seen handle or spelling as an alias), and mint an entity only for something genuinely absent. The full resolve → reuse → alias discipline in `crm.md` applies here verbatim.
24
24
 
25
+ ## When the channel is UNBOUND — classify it and PROPOSE a bind
26
+
27
+ A channel that is **not yet bound to any entity** (no `contextObjectId`, no `branchPurpose`) is a different job from a normal digest. Before you mine it for signal, answer a prior question: **what IS this channel, and which entity does it belong to?** This is the "I think #acme is a client — link it?" affordance.
28
+
29
+ Work it in this order — identity FIRST, classification, then propose:
30
+
31
+ 1. **Read the window + the channel NAME.** The name (`acme`, `weex-partnership`, `general`, `eng-team`) is often the strongest single signal; the messages confirm or correct it. Who posts, what they talk about, whether it reads client-facing or internal.
32
+ 2. **Resolve the entity FIRST — do NOT mint a duplicate.** Match the channel to something that already exists in the pod on STRONG signals — the name, a domain/email/url mentioned, a handle — using `resolve_identity` / a batched search. Only if nothing genuinely matches do you consider a new entity, and even then propose the entity through the normal capture door, never a throwaway. The whole point of binding is to point the channel at the REAL thing; binding to a fabricated duplicate is worse than not binding at all.
33
+ 3. **Classify what the channel is** — pick the one that fits:
34
+ - **client** — a client-facing channel (mixes their people and ours). Suggests `branchPurpose: "client-comms"`.
35
+ - **partner** — a partner/vendor relationship channel. Suggests `branchPurpose` like `"partner"` (or leave to the human).
36
+ - **internal team** — everyone here is OUR team; ops/eng/planning. Suggests `branchPurpose: "team"`.
37
+ - **project** — work-scoped, cross-cutting. Suggests a project-oriented purpose (or `"team"` if internal).
38
+ 4. **Emit ONE governed bind proposal** with `propose_channel_bind` — `channelId` (the Synap channel UUID), the resolved `contextObjectId`, and your suggested `branchPurpose` + a one-line `reasoning`. The result comes back `proposed`: that is the system working. You do not bind — the operator confirms.
39
+
40
+ ### The firewall — the one rule you never bend
41
+
42
+ `branchPurpose: "client-comms"` is **irreversible**. Once a channel is client-comms the backend refuses to reclassify it. So:
43
+
44
+ - **Only suggest `client-comms` when you are confident the channel is client-facing.** A `general`, `random`, or internal team channel must NEVER be proposed as client-comms — misfiling an internal room as client comms is exactly the mistake the firewall exists to prevent.
45
+ - **When the side is unclear, OMIT `branchPurpose`** and let the human choose on approval. An unbound bind with the purpose left open is fine; a wrong client-comms bind is not.
46
+ - You always PROPOSE. There is no path here where you silently set client-comms. The human owns that decision by approving.
47
+
48
+ Auto-binding on strong identity signals lives elsewhere; **from this skill you always propose.**
49
+
25
50
  ## Role-aware people guard (read this twice)
26
51
 
27
52
  WHO is in the channel changes what a person IS. Get this wrong and you propose your own teammates as leads.
@@ -25,13 +25,13 @@ Ask yourself: _who does this knowledge serve?_ **There is no private AI scratchp
25
25
 
26
26
  ### Data layers — the graph itself
27
27
 
28
- | Layer | What it is | When to use |
29
- | ------------- | ---------------------------------------------- | -------------------------------------------------------- |
30
- | **Entities** | Typed structured nodes (task, person, …) | Anything worth filtering, sorting, or linking |
31
- | **Relations** | Typed edges between entities | Making the graph traversable |
32
- | **Documents** | Long-form markdown attached to an entity | Meeting notes, research writeups, articles |
33
- | **Threads** | Channel conversations, optional entity context | Posting to the user's personal AI channel |
34
- | **Proposals** | Writes queued for human approval | Governance for some mutations (not an error — see below) |
28
+ | Layer | What it is | When to use |
29
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
30
+ | **Entities** | Typed structured nodes (task, person, …) | Anything worth filtering, sorting, or linking |
31
+ | **Relations** | Typed edges between entities | Making the graph traversable |
32
+ | **Documents** | Long-form versioned body attached to an entity — auto-materialized from an entity's `content`, or created standalone via `synap_create_document` | Meeting notes, research writeups, articles — **never** a `file`/`document`-kind entity for text you authored |
33
+ | **Threads** | Channel conversations, optional entity context | Posting to the user's personal AI channel |
34
+ | **Proposals** | Writes queued for human approval | Governance for some mutations (not an error — see below) |
35
35
 
36
36
  ### Key profiles for AI use
37
37
 
@@ -1,10 +1,12 @@
1
1
  ## Multi-entity capture from free-form text
2
2
 
3
- 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:
3
+ When the user pastes a block of unstructured content (a meeting transcript, an email, a LinkedIn bio) or when several related things come up at once, send it through the **one capture door** — `synap_capture` (CLI: `synap capture`). Don't chain manual creates, and don't run a two-step "structure then commit" dance.
4
+
5
+ The payload is a gradient in a single call:
4
6
 
5
7
  ```
6
- POST /api/hub/capture/structure → returns proposals + relations
7
- POST /api/hub/capture/execute → commits (after user confirms)
8
+ { "text": "…paste the raw content…" } → the AI structures it into entities
9
+ { "entities": [ … ], "relations": [ … ] } → you supply the graph directly (refs link them)
8
10
  ```
9
11
 
10
- The pipeline extracts multiple entities with their relations in one LLM call. Read **`capture.md`** for the full flow.
12
+ Everything lands as ONE reviewable proposal (or auto-applies when every op is safe), and you get back one receipt — `status: "applied" | "proposed" | "rejected"`. `proposed` is success: surface the review link. There is no separate commit step. Read **`capture.md`** for the full flow, dedup signals, name-refs, and reject reasons.
@@ -51,3 +51,24 @@
51
51
  ```
52
52
 
53
53
  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.
54
+
55
+ ### Example 4 — "Write up a strategic plan for the Q3 launch"
56
+
57
+ You are authoring this text yourself — it is not a file you have. Don't create
58
+ a `file`/`document`-kind entity and stuff the Markdown into it.
59
+
60
+ 1. Search for an existing plan: `GET /entities?q=Q3 launch&profileSlug=knowledge&workspaceId=…` → none
61
+ 2. Create a CONTENT-kind entity carrying the plan as `content` (the doc auto-materializes):
62
+
63
+ ```json
64
+ POST /api/hub/entities
65
+ { "userId": "{userId}", "workspaceId": "{wsId}",
66
+ "profileSlug": "knowledge",
67
+ "title": "Q3 launch strategic plan",
68
+ "properties": { "ek_type": "reference" },
69
+ "content": "# Q3 Launch Plan\n\n## Goals\n…\n\n## Timeline\n…"
70
+ }
71
+ ```
72
+
73
+ 3. Link it to the relevant project: `POST /api/hub/relations` `{ sourceEntityId: "ent_new_plan", targetEntityId: "ent_project_q3", type: "related_to" }`.
74
+ 4. Confirm: "Plan captured and linked to Q3 launch." No upload, no `file` entity, no separate `synap_create_document` call.
@@ -1,6 +1,12 @@
1
1
  ## Core writes
2
2
 
3
- ### Create an entity (schema-first, complete intent)
3
+ **Two write doors, one gradient.** `create_entity` is for exactly ONE
4
+ fully-structured, typed entity you already have. For anything unstructured,
5
+ several entities, or a graph — or **when in doubt** — use the capture door
6
+ (`synap_capture`, see `capture.md`): precision comes from sending more structure
7
+ in the SAME call, never from picking a different tool or a second commit step.
8
+
9
+ ### Create an entity (one exact typed entity)
4
10
 
5
11
  Before this call, use `/discover?userId=…&profileSlugs=<kind>` to read the
6
12
  real fields, required/default values, constraints and reference targets. Omit
@@ -22,15 +28,27 @@ POST /api/hub/entities
22
28
  ```
23
29
 
24
30
  The response has legacy `status`/`id` fields plus `writeReceipt`:
25
- `pending` means a proposal exists and no entity is live; `applied` means the
26
- reported direct write completed; `partial` means a follow-up (for example a
27
- facet) failed after the entity applied. Never claim completion from `pending`,
28
- and only enrich again when the receipt identifies a real missing fact.
31
+ `pending`/`proposed` means a proposal exists and no entity is live yet;
32
+ `applied` means the reported direct write completed; `partial` means a follow-up
33
+ (for example a facet) failed after the entity applied. **A `proposed`/`pending`
34
+ receipt is a governed success, not an error** — surface its `reviewUrl`, never
35
+ claim completion, and only enrich again when the receipt identifies a real
36
+ missing fact.
29
37
 
30
38
  For several entities, creation-time roles/facets, or relations that need one
31
- review, submit one `POST /api/hub/capture/graph` plan instead of sequencing
32
- independent creates. It returns a pending receipt and materializes on human
33
- approval.
39
+ review, send the whole graph through the capture door (`synap_capture` with
40
+ `entities[]` + `relations[]`) instead of sequencing independent creates. It is
41
+ ONE governed call: policy auto-applies when every op is safe, otherwise the
42
+ whole graph is proposed (atomic). There is no separate commit step.
43
+
44
+ **Name-refs, not UUIDs.** Reference an existing project by name — the server
45
+ resolves it against the caller's own projects (exact match files there; no match
46
+ proposes, never mis-files). Never ask the user for a UUID.
47
+
48
+ **Dedup is advisory across kinds.** Strong signals (`email`/`phone`/`website` —
49
+ not a bare `url`) dedup within a kind; a same-title hit in a _different_ kind
50
+ comes back as an advisory candidate, never an auto-merge. "No exact match" is not
51
+ "safe to create" when advisory candidates are returned — review them first.
34
52
 
35
53
  ### Update an entity
36
54
 
@@ -41,6 +59,41 @@ PATCH /api/hub/entities/{entityId}
41
59
 
42
60
  **Properties are deep-merged — send only the keys you want to change.** An update with `{ "status": "done" }` leaves all other properties untouched. You never need to re-send the full properties object.
43
61
 
62
+ ### Authored text is content, not a `file`
63
+
64
+ Something **you** author — a pitch deck, a strategic plan, a note body — is
65
+ **never** a `file`- or `document`-kind entity. Create it as the right CONTENT
66
+ kind (`note`, `knowledge`, or a fitting domain kind) with `content` set to the
67
+ Markdown body; Synap **auto-materializes** that `content` into a real
68
+ versioned document behind the scenes (`entities WHERE documentId = ?`) — no
69
+ upload step needed. `file` is reserved for real uploaded bytes you actually
70
+ have (the upload door / `synap upload`) — an agent has no filesystem, so it
71
+ rarely touches `file` at all. Reach for `synap_create_document` /
72
+ `POST /api/hub/documents` directly only for a standalone rich document that
73
+ isn't itself a title-worthy entity (see below) — never as a substitute for an
74
+ entity's `content`.
75
+
76
+ ### "I want to add a real FILE" — the decision tree
77
+
78
+ You CAN store content you **hold** — you send it inline. Pick by what you have:
79
+
80
+ | You have… | Do this | Result |
81
+ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
82
+ | **Content in hand** — a report/CSV/image/PDF/etc. you generated or received (text or bytes) | **`synap_store_file`** (`content` for text, `contentBase64` for binary) + `mimeType`/`filename` | a `file` entity, content stored **as-is, never read** (≤10MB inline) |
83
+ | Text you're **authoring as a note/idea** (not "a file") — a plan, a thought | `synap_create_entity` with `content` (a content kind) | body auto-becomes a document — NOT a `file` |
84
+ | Only a **URL/link** (no bytes) — a Google Doc, a PDF url, an article | `synap_create_document` with **`url`**, or attach via `entityId` | an external **reference** document (no bytes) |
85
+ | A **large file on a local disk** you don't hold in context | the CLI `synap upload <path>` (streams it) — an agent can't send bytes it doesn't have | a `file` entity backed by stored bytes |
86
+ | Only a file's **name**, nothing else | you **cannot** invent it — ask the human/client to provide the content or a link | — |
87
+
88
+ **Store ≠ analyze.** `synap_store_file` / `synap_create_document` store content
89
+ **deterministically — the file is NEVER read by an LLM.** Only fetch a document
90
+ and reason over it when the user explicitly says "read/analyze this file."
91
+
92
+ **Do NOT over-structure a file request.** "Store this file" / "how do I add this
93
+ file" is ONE intent → at most one stored file (plus, only if asked, a linking
94
+ relation). Never inflate it into a task + note + placeholder-file scaffold, and
95
+ never dump the request text into a note and call it done.
96
+
44
97
  ### Create a document (attach to an entity)
45
98
 
46
99
  ```json
@@ -98,21 +151,25 @@ Content is a **full replacement**, not a patch. Fetch the current content first
98
151
 
99
152
  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.
100
153
 
101
- ### Store a fact (memory) — use sparingly
154
+ ### Remember a fact about the user — use sparingly
102
155
 
103
- ```json
104
- POST /api/hub/memory
105
- { "userId": "{userId}", "fact": "User prefers async communication over meetings" }
106
- ```
156
+ A fact _about the user_ goes through `remember_fact` (CLI: `synap capture --type
157
+ observation`). It writes a governed `user_observation` — not an ungoverned
158
+ throwaway row: a fact the user explicitly stated auto-approves; a fact you
159
+ inferred returns `proposed` (normal — surface the review link). Because it's a
160
+ real entity it's addressable, linkable and revertible.
107
161
 
108
- 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.
162
+ **Use it only for loose, unstructured, hard-to-title facts about the user** — a
163
+ stated preference, a throwaway detail. Everything with a title-worthy noun or
164
+ something to link to is an ENTITY through the capture door, not an observation.
109
165
 
110
- **The test:** if the user later asked "show me all X," can memory answer? Memory can only keyword-match — it has no structure. So:
166
+ **The test:** if the user later asked "show me all X," could a loose observation
167
+ answer? It can only keyword-match — it has no structure. So:
111
168
 
112
169
  | Input | Use |
113
170
  | ------------------------------------------------------- | --------------------------------------------------------------- |
114
- | "User prefers async communication" | memory — it's a preference |
115
- | "Garage code is 4321" | memory — throwaway fact |
171
+ | "User prefers async communication" | observation — it's a preference |
172
+ | "Garage code is 4321" | observation — throwaway fact |
116
173
  | "Should we use LangGraph or CrewAI for Eve?" | **entity `question`** — substantive inquiry, start of flow |
117
174
  | "Here's what I found comparing LangGraph and CrewAI…" | **entity `research`** — investigation with sources + conclusion |
118
175
  | "We decided to use LangGraph over OpenClaude's native…" | **entity `decision`** — has title, rationale, project |
@@ -122,7 +179,7 @@ Always auto-approved. **Memory is for loose, unstructured, hard-to-title facts o
122
179
  | "Action item from meeting: ship MVP by Friday" | **entity `task`** linked to the `event` (meeting) |
123
180
  | "Agreed with Sarah: we'll split backend & frontend" | **entity `decision`** linked to Sarah + the project |
124
181
 
125
- **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.
182
+ **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 through the capture door, not an observation. A user observation is the fallback, not the default.
126
183
 
127
184
  **For decisions specifically** — use the `decision` system profile:
128
185
 
@@ -0,0 +1,319 @@
1
+ ---
2
+ name: synap-app
3
+ description: >
4
+ Use this skill when the user wants to BUILD THEIR OWN APP on top of Synap — a
5
+ personal project or a product — using Synap as the backend (auth, typed data
6
+ API, realtime). Two paths: add the SDK (@synap-core/sdk) to an existing app,
7
+ or scaffold the opinionated Next.js + HeroUI base app (npx create-synap-app).
8
+ Triggers: "build an app on Synap", "use Synap as my backend", "scaffold a
9
+ Synap project", "add the Synap SDK", "connect my app to my pod",
10
+ "npx create-synap-app". NOT the `synap` skill (capture/recall in a pod) nor
11
+ `synap-ui` (views inside the pod's own UI) — this is about writing app CODE.
12
+ metadata:
13
+ openclaw:
14
+ requires:
15
+ env: []
16
+ optional_env: [SYNAP_POD_URL, SYNAP_WORKSPACE_ID]
17
+ primaryEnv: null
18
+ homepage: https://synap.live
19
+ capabilities: [sdk, scaffold, auth, realtime]
20
+ os: [macos, linux, windows]
21
+ userInvocable: true
22
+ ---
23
+
24
+ # Building an app on Synap
25
+
26
+ Synap is a sovereign data pod with a complete, typed API. You build an app on it
27
+ the same way you'd build on any backend — except the backend is the user's own
28
+ pod, and **every write is governed** (see `governance.md`).
29
+
30
+ ## Decide the path first
31
+
32
+ | The user has… | Do this | Read |
33
+ |---|---|---|
34
+ | an existing app (any framework) | add the SDK | `using-the-sdk.md`, `install-commands.md` |
35
+ | nothing yet, wants a full app fast | scaffold the base app | `creating-the-base-app.md`, `install-commands.md` |
36
+ | a server / script / CLI (no browser) | SDK with a session token or API key | `using-the-sdk.md` |
37
+
38
+ Both paths use the same underlying packages; only the amount of scaffolding differs.
39
+
40
+ ## The packages (all published under `@synap-core/*`)
41
+
42
+ - `@synap-core/sdk` — typed tRPC client. `createSynapClient({ podUrl, ... })`. **The one you almost always want.**
43
+ - `@synap-core/react` — React/TanStack-Query bindings (`SynapProvider`, hooks).
44
+ - `@synap-core/sdk-realtime` — Socket.IO presence/live-updates (`createRealtimeClient`).
45
+ - `@synap-core/auth` — sign-in orchestration (Synap Cloud + pod handshake), framework-agnostic.
46
+ - `@synap-core/auth-bootstrap` — zero-dep server helpers (`setupAgent`, `handshake`) for machine clients.
47
+
48
+ Pin `@^1.0.0` on `sdk`/`react` so you never resolve an old, abandoned `0.x`.
49
+
50
+ ## The three rules that trip people up
51
+
52
+ 1. **A write can come back `"proposed"` — that is SUCCESS, not an error.** It means
53
+ the change is queued for the pod owner's review (like a PR). Surface the review,
54
+ don't retry. Full contract: `governance.md`.
55
+ 2. **The pod URL has no `/trpc` suffix and no trailing slash.** The SDK appends `/trpc`.
56
+ 3. **Non-browser clients (server/CLI/mobile) authenticate with a token or API key**,
57
+ never a cookie. Browser apps use the Synap Cloud sign-in flow (`@synap-core/auth`).
58
+
59
+ ## Get a pod to build against
60
+
61
+ - **Self-host (free):** `git clone` the backend, `docker compose up -d` (run its
62
+ `install.sh` to generate secrets). Pod answers on `:4000`.
63
+ - **Managed (paid):** sign up at synap.live and provision a pod.
64
+
65
+ You can develop the base app with **no pod at all** using its mock mode
66
+ (`NEXT_PUBLIC_BASE_MOCK_MODE=true`).
67
+
68
+ ## Honest scope
69
+
70
+ Building on the user's OWN pod is fully supported today. Building a **multi-tenant
71
+ commercial app** where *other* people connect *their* pods is **not yet supported**
72
+ — per-app data scoping is still being built. If the user asks for that, say so;
73
+ don't architect around a capability that isn't there.
74
+
75
+ ---
76
+
77
+ # Install & scaffold commands
78
+
79
+ ## Scaffold a full app (recommended for a fresh start)
80
+
81
+ ```bash
82
+ npx create-synap-app my-app
83
+ ```
84
+
85
+ Creates a Next.js + HeroUI project from the base template, with the SDK, auth,
86
+ app shell, command palette, realtime, and mock mode pre-wired. It also drops an
87
+ `AGENTS.md` into the project so your AI coding tool knows how to work with it.
88
+
89
+ ## Add the SDK to an existing app
90
+
91
+ ```bash
92
+ # npm
93
+ npm install @synap-core/sdk@^1.0.0
94
+
95
+ # with React bindings
96
+ npm install @synap-core/sdk@^1.0.0 @synap-core/react@^1.0.0
97
+
98
+ # realtime
99
+ npm install @synap-core/sdk-realtime
100
+
101
+ # auth (browser sign-in) or auth-bootstrap (server credential bootstrap)
102
+ npm install @synap-core/auth # or @synap-core/auth-bootstrap
103
+ ```
104
+
105
+ Pin `@^1.0.0` on `sdk`/`react`. **Do not** install unpinned `@synap-core/sdk` —
106
+ older `0.x` versions on the registry are a different, abandoned package.
107
+
108
+ ## Get this knowledge into your AI coding tool
109
+
110
+ ```bash
111
+ # add the app-creation skill (this skill) to a connected AI tool
112
+ synap skills get synap-app
113
+
114
+ # or wire a tool end-to-end (skills + MCP), any target
115
+ synap connect --target=cursor # or claude, openclaw, …
116
+ ```
117
+
118
+ Alternatively, the scaffolded project ships an `AGENTS.md` at its root — most AI
119
+ coding tools read it automatically, so a freshly-scaffolded app teaches the AI
120
+ how to build on it with zero extra setup.
121
+
122
+ ## Verify an install actually works
123
+
124
+ Publishing bugs have shipped uninstallable Synap packages before. After adding a
125
+ package, confirm it resolves in a clean directory:
126
+
127
+ ```bash
128
+ mkdir /tmp/synap-check && cd /tmp/synap-check && npm init -y
129
+ npm install @synap-core/sdk@^1.0.0
130
+ node -e "console.log(typeof require('@synap-core/sdk').createSynapClient)" # → function
131
+ ```
132
+
133
+ ---
134
+
135
+ # Using the SDK
136
+
137
+ ## Vanilla / TypeScript / server / CLI — `@synap-core/sdk`
138
+
139
+ ```ts
140
+ import { createSynapClient } from "@synap-core/sdk";
141
+
142
+ const synap = createSynapClient({
143
+ podUrl: "https://pod.example.com", // NO trailing slash, NO /trpc suffix
144
+ apiKey: process.env.SYNAP_API_KEY, // or sessionToken (see auth below)
145
+ workspaceId: process.env.SYNAP_WORKSPACE_ID ?? "",
146
+ });
147
+
148
+ // It's a typed tRPC client — full autocomplete over every router.
149
+ const me = await synap.users.me.query();
150
+ const entities = await synap.entities.list.query({ /* filter */ });
151
+ const result = await synap.entities.create.mutate({ /* ... */ });
152
+ // result may be { status: "proposed", ... } — that is success. See governance.md.
153
+ ```
154
+
155
+ ### Auth modes (pick ONE)
156
+ - **`apiKey`** — a Bearer key generated in the workspace's developer settings.
157
+ Best for servers, scripts, CLIs.
158
+ - **`sessionToken`** — an Ory Kratos session token, sent as `X-Session-Token`.
159
+ Best for non-browser clients that completed a sign-in.
160
+ - **cookie fallback** — omit both; the client uses the ambient Kratos cookie.
161
+ Browser, same-origin only.
162
+
163
+ For machine clients that need to bootstrap a credential, use
164
+ `@synap-core/auth-bootstrap` (`setupAgent(provisioningToken)` → API key,
165
+ `handshake(jwt)` → session token). Zero-dependency, safe in edge/serverless.
166
+
167
+ ## React — `@synap-core/react`
168
+
169
+ ```tsx
170
+ import { SynapProvider, useSynapContext } from "@synap-core/react";
171
+
172
+ function App() {
173
+ return (
174
+ <SynapProvider podUrl={podUrl} sessionToken={token} workspaceId={ws}>
175
+ <Dashboard />
176
+ </SynapProvider>
177
+ );
178
+ }
179
+ ```
180
+
181
+ `SynapProvider` honours every `createSynapClient` option, incl. `sessionToken`
182
+ and `onAuthError` (a 401 handler). `react` is a peer of your app's React — it
183
+ does not bundle its own.
184
+
185
+ ## Realtime — `@synap-core/sdk-realtime`
186
+
187
+ ```ts
188
+ import { createRealtimeClient } from "@synap-core/sdk-realtime";
189
+
190
+ const rt = createRealtimeClient({ podUrl, sessionToken });
191
+ const stop = rt.onInvalidate((target) => {
192
+ // e.g. refetch the affected query
193
+ });
194
+ ```
195
+
196
+ Websocket-only transport (credentials never ride a long-poll URL) and it refuses
197
+ to send credentials over non-TLS unless you explicitly pass `allowInsecure`.
198
+
199
+ ## Two transports, on purpose
200
+ - `@synap-core/sdk` (tRPC) — for apps. Typed, hooks, realtime.
201
+ - `@synap-core/hub-rest-client` (REST, zero-dep) — for agents/CLIs/scripts. Carries
202
+ agent verbs the tRPC surface has no first-class path for (`ask`, `orient`,
203
+ `discover`). Use it when you want a dependency-free REST client, not a full app.
204
+
205
+ ## Keep types fresh
206
+ The SDK is typed by `@synap-core/api-types`, generated from the pod's live router.
207
+ If the pod adds a procedure and you don't see it, bump `@synap-core/api-types`.
208
+
209
+ ---
210
+
211
+ # Creating the base app
212
+
213
+ The base app is an opinionated, batteries-included starter: **Next.js 16 (App
214
+ Router) + React 19 + HeroUI + Tailwind**, with auth (incl. TOTP), an app shell
215
+ (tabs + slide-in right panel), a Cmd+K command palette, a realtime bridge, and a
216
+ mock mode — all pre-wired. You add your screens; the plumbing is done.
217
+
218
+ ## Scaffold it
219
+
220
+ ```bash
221
+ npx create-synap-app my-app
222
+ cd my-app
223
+ cp .env.example .env.local # fill in your pod / control-plane URLs
224
+ pnpm dev # http://localhost:3010
225
+ ```
226
+
227
+ Develop with **no pod** by setting `NEXT_PUBLIC_BASE_MOCK_MODE=true`.
228
+
229
+ ## What's where
230
+
231
+ ```
232
+ app/
233
+ layout.tsx Root layout → ClientProviders (client-only, see below)
234
+ ClientProviders Loads the provider tree with { ssr: false } (REQUIRED — see rule 1)
235
+ providers.tsx The real provider stack (auth, tRPC, realtime, palette)
236
+ (app)/home Your home screen — replace this
237
+ login/ onboarding First-run + auth screens
238
+ lib/
239
+ registerAll.ts Boot hook — registers cells/views (override in your app)
240
+ auth.ts mockMode Session + mock helpers
241
+ components/layout/
242
+ AppShell Root layout primitive — pass your sidebar as `leftNav`
243
+ ```
244
+
245
+ ## Four rules that keep the build green (all Next 16 / Turbopack specifics)
246
+
247
+ 1. **The provider tree must load client-only.** `app/ClientProviders.tsx` does
248
+ `dynamic(() => import("./providers"), { ssr: false })`. base's provider tree
249
+ pulls a dep that touches `window` at module scope, which crashes a server
250
+ render. base is a client app shell — there is nothing to server-render — so
251
+ this is correct, not a hack. Keep it.
252
+ 2. **`config.matcher` in `middleware.ts` must be an inline literal**, not a value
253
+ read from a helper — Next's static analyzer can't parse an expression there.
254
+ 3. **In `globals.css`, `@import` must come before the `@tailwind` directives.**
255
+ Turbopack enforces the CSS spec strictly.
256
+ 4. **Build with Turbopack (the default) — do NOT pass `--webpack`.** In Next 16
257
+ the legacy `--webpack` flag silently produces nothing (exit 0, no output). If
258
+ you add a webpack alias, port it to `turbopack.resolveAlias` in `next.config.ts`.
259
+
260
+ ## Deriving your own app
261
+
262
+ 1. Rename the namespaced storage keys (`base:session`, `base-workspace`, …) so
263
+ two Synap apps don't share state in one browser. The README lists every key.
264
+ 2. Replace `app/(app)/home/page.tsx` with your dashboard.
265
+ 3. Pass your sidebar: `<AppShell leftNav={<MySidebar />}>`.
266
+ 4. Register app-specific Cmd+K commands with `useRegisterCommand([...])`.
267
+ 5. Register only the cells you need in `lib/registerAll.ts` (a lean template
268
+ registers fewer than the full set).
269
+
270
+ ## Auth flow (already wired)
271
+
272
+ `/login` → Synap Cloud sign-in (email + password or magic link) → optional TOTP →
273
+ pod picker → handshake → session stored → `/home`. Self-hosted pods fall back to
274
+ the pod's own Kratos at `/login?mode=kratos`. You don't build this; you theme it.
275
+
276
+ ---
277
+
278
+ # Governance — `"proposed"` is success, not an error
279
+
280
+ This is the single most important thing to get right when writing code that
281
+ mutates a Synap pod.
282
+
283
+ ## The contract
284
+
285
+ Every mutation (`entities.create`, `entities.update`, `relations.create`, …)
286
+ resolves to one of three outcomes, decided **by the pod**, not by your app:
287
+
288
+ | Outcome | Meaning | What your code should do |
289
+ |---|---|---|
290
+ | `approved` | applied immediately | proceed |
291
+ | `proposed` | queued for the pod owner's review (like a PR) | **surface the review — do NOT treat as failure, do NOT retry** |
292
+ | `denied` | the principal may not do this | show why; do not retry blindly |
293
+
294
+ ## Why it matters for your code
295
+
296
+ - **Never treat `proposed` as an error.** It is the normal, healthy path for any
297
+ write that governance decides a human should see first. Retrying a `proposed`
298
+ write creates duplicates.
299
+ - **Never `try/catch` a `proposed` as if it threw.** It's a successful response
300
+ with a status, not an exception.
301
+ - **Surface the review link/id** so the user can approve it. An unreviewed
302
+ proposal is not yet in the graph — it won't show up in reads until approved.
303
+
304
+ ## Pattern
305
+
306
+ ```ts
307
+ const res = await synap.entities.create.mutate(input);
308
+ switch (res.status) {
309
+ case "approved": /* it's live */ break;
310
+ case "proposed": /* tell the user: "queued for review" + show res.proposalId */ break;
311
+ case "denied": /* show res.reason; don't retry */ break;
312
+ }
313
+ ```
314
+
315
+ ## Idempotency
316
+
317
+ If you must retry a create after a network error, pass an idempotency key where
318
+ the API accepts one — a `proposed` write that you retry without one will
319
+ duplicate on approval. When unsure, read back before re-creating.
@@ -0,0 +1,5 @@
1
+ overview.md
2
+ install-commands.md
3
+ using-the-sdk.md
4
+ creating-the-base-app.md
5
+ governance.md
@@ -0,0 +1,20 @@
1
+ name: synap-app
2
+ description: >
3
+ Use this skill when the user wants to BUILD THEIR OWN APP on top of Synap — a
4
+ personal project or a product — using Synap as the backend (auth, typed data
5
+ API, realtime). Two paths: add the SDK (@synap-core/sdk) to an existing app,
6
+ or scaffold the opinionated Next.js + HeroUI base app (npx create-synap-app).
7
+ Triggers: "build an app on Synap", "use Synap as my backend", "scaffold a
8
+ Synap project", "add the Synap SDK", "connect my app to my pod",
9
+ "npx create-synap-app". NOT the `synap` skill (capture/recall in a pod) nor
10
+ `synap-ui` (views inside the pod's own UI) — this is about writing app CODE.
11
+ metadata:
12
+ openclaw:
13
+ requires:
14
+ env: []
15
+ optional_env: [SYNAP_POD_URL, SYNAP_WORKSPACE_ID]
16
+ primaryEnv: null
17
+ homepage: https://synap.live
18
+ capabilities: [sdk, scaffold, auth, realtime]
19
+ os: [macos, linux, windows]
20
+ userInvocable: true
@@ -0,0 +1,64 @@
1
+ # Creating the base app
2
+
3
+ The base app is an opinionated, batteries-included starter: **Next.js 16 (App
4
+ Router) + React 19 + HeroUI + Tailwind**, with auth (incl. TOTP), an app shell
5
+ (tabs + slide-in right panel), a Cmd+K command palette, a realtime bridge, and a
6
+ mock mode — all pre-wired. You add your screens; the plumbing is done.
7
+
8
+ ## Scaffold it
9
+
10
+ ```bash
11
+ npx create-synap-app my-app
12
+ cd my-app
13
+ cp .env.example .env.local # fill in your pod / control-plane URLs
14
+ pnpm dev # http://localhost:3010
15
+ ```
16
+
17
+ Develop with **no pod** by setting `NEXT_PUBLIC_BASE_MOCK_MODE=true`.
18
+
19
+ ## What's where
20
+
21
+ ```
22
+ app/
23
+ layout.tsx Root layout → ClientProviders (client-only, see below)
24
+ ClientProviders Loads the provider tree with { ssr: false } (REQUIRED — see rule 1)
25
+ providers.tsx The real provider stack (auth, tRPC, realtime, palette)
26
+ (app)/home Your home screen — replace this
27
+ login/ onboarding First-run + auth screens
28
+ lib/
29
+ registerAll.ts Boot hook — registers cells/views (override in your app)
30
+ auth.ts mockMode Session + mock helpers
31
+ components/layout/
32
+ AppShell Root layout primitive — pass your sidebar as `leftNav`
33
+ ```
34
+
35
+ ## Four rules that keep the build green (all Next 16 / Turbopack specifics)
36
+
37
+ 1. **The provider tree must load client-only.** `app/ClientProviders.tsx` does
38
+ `dynamic(() => import("./providers"), { ssr: false })`. base's provider tree
39
+ pulls a dep that touches `window` at module scope, which crashes a server
40
+ render. base is a client app shell — there is nothing to server-render — so
41
+ this is correct, not a hack. Keep it.
42
+ 2. **`config.matcher` in `middleware.ts` must be an inline literal**, not a value
43
+ read from a helper — Next's static analyzer can't parse an expression there.
44
+ 3. **In `globals.css`, `@import` must come before the `@tailwind` directives.**
45
+ Turbopack enforces the CSS spec strictly.
46
+ 4. **Build with Turbopack (the default) — do NOT pass `--webpack`.** In Next 16
47
+ the legacy `--webpack` flag silently produces nothing (exit 0, no output). If
48
+ you add a webpack alias, port it to `turbopack.resolveAlias` in `next.config.ts`.
49
+
50
+ ## Deriving your own app
51
+
52
+ 1. Rename the namespaced storage keys (`base:session`, `base-workspace`, …) so
53
+ two Synap apps don't share state in one browser. The README lists every key.
54
+ 2. Replace `app/(app)/home/page.tsx` with your dashboard.
55
+ 3. Pass your sidebar: `<AppShell leftNav={<MySidebar />}>`.
56
+ 4. Register app-specific Cmd+K commands with `useRegisterCommand([...])`.
57
+ 5. Register only the cells you need in `lib/registerAll.ts` (a lean template
58
+ registers fewer than the full set).
59
+
60
+ ## Auth flow (already wired)
61
+
62
+ `/login` → Synap Cloud sign-in (email + password or magic link) → optional TOTP →
63
+ pod picker → handshake → session stored → `/home`. Self-hosted pods fall back to
64
+ the pod's own Kratos at `/login?mode=kratos`. You don't build this; you theme it.