@oneie/sdk 0.13.2 → 0.13.3

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.
package/dist/receivers.js CHANGED
@@ -197,6 +197,22 @@ export const RECEIVERS = {
197
197
  request: z.object({ aid: z.string(), name: z.string().optional(), tags: z.array(z.string()).optional(), prompt: z.string().optional(), model: z.string().optional() }),
198
198
  response: ok, effect: "ask", auth: "manage_actors",
199
199
  }),
200
+ "visitor:promote": receiver({
201
+ receiver: "visitor:promote",
202
+ summary: "Promote a tracked anonymous visitor to a TypeDB actor (anon:<hash12>) so it surfaces in People; binds rung-1 visitor_hash→aid. Idempotent.",
203
+ request: z.object({ visitorHash: z.string() }),
204
+ response: z.object({
205
+ ok: z.boolean(),
206
+ aid: z.string().optional(),
207
+ promoted: z.boolean().optional(),
208
+ region: z.string().optional(),
209
+ reason: z.string().optional(),
210
+ }),
211
+ effect: "ask",
212
+ auth: "manage_actors",
213
+ idempotent: true,
214
+ examples: [{ visitorHash: "9f2a1c7b4e0d8a3f5c6b" }],
215
+ }),
200
216
  "world:update-contact": receiver({
201
217
  receiver: "world:update-contact",
202
218
  summary: "Update an actor's contact/profile attributes — identity, contact details, professional, personal, CRM metrics",
@@ -764,6 +780,130 @@ export const RECEIVERS = {
764
780
  response: z.object({ ok: z.boolean(), id: z.string().optional(), ts: z.number().optional(), to: z.string().optional(), from: z.string().optional(), error: z.string().optional() }),
765
781
  effect: "ask", idempotent: false,
766
782
  }),
783
+ "notify": receiver({
784
+ receiver: "notify",
785
+ summary: "Notify any actor (human or agent) by uid — channels routes the door (Telegram/Discord/peer inbox), mirrors into the web inbox, and nudges an open client live. One verb, both actor kinds.",
786
+ request: z.object({
787
+ receiver: z.string(),
788
+ kind: z.string().optional(),
789
+ body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
790
+ content: z.string().optional(),
791
+ }),
792
+ response: z.object({
793
+ ok: z.boolean(),
794
+ id: z.string().optional(),
795
+ ts: z.number().optional(),
796
+ to: z.string().optional(),
797
+ from: z.string().optional(),
798
+ kind: z.string().optional(),
799
+ delivered: z.array(z.string()).optional(),
800
+ error: z.string().optional(),
801
+ }),
802
+ effect: "ask", idempotent: false,
803
+ }),
804
+ "notify:count": receiver({
805
+ receiver: "notify:count",
806
+ summary: "Unread thread count for the caller's inbox (a thread is unread when its last message is newer than the caller's read marker).",
807
+ request: z.object({}),
808
+ response: z.object({ unread: z.number() }),
809
+ effect: "ask", idempotent: true,
810
+ }),
811
+ "notify:list": receiver({
812
+ receiver: "notify:list",
813
+ summary: "The caller's unread threads, newest first, each with a short preview.",
814
+ request: z.object({ limit: z.number().optional() }),
815
+ response: z.object({ unread: z.array(z.object({ id: z.string(), ts: z.number(), preview: z.string() })) }),
816
+ effect: "ask", idempotent: true,
817
+ }),
818
+ "notify:read": receiver({
819
+ receiver: "notify:read",
820
+ summary: "Mark one thread (threadId) or all of the caller's threads (all:true) as read; nudges the inbox live.",
821
+ request: z.object({ threadId: z.string().optional(), id: z.string().optional(), all: z.boolean().optional() }),
822
+ response: z.object({ ok: z.boolean(), ts: z.number().optional(), error: z.string().optional() }),
823
+ effect: "ask", idempotent: true,
824
+ }),
825
+ "thread:append": receiver({
826
+ receiver: "thread:append",
827
+ summary: "Gateway-callable write into web threads — carries tags into entity_tags so slug/in renders every channel by tag. Called by the channels bridge after every inbound signal.",
828
+ request: z.object({
829
+ slug: z.string(),
830
+ group: z.string(),
831
+ sender: z.string(),
832
+ label: z.string().optional(),
833
+ chatName: z.string().optional(),
834
+ content: z.string(),
835
+ ts: z.number().optional(),
836
+ tags: z.array(z.string()).optional(),
837
+ }),
838
+ response: z.object({
839
+ ok: z.boolean(),
840
+ threadId: z.string().optional(),
841
+ error: z.string().optional(),
842
+ }),
843
+ effect: "ask",
844
+ idempotent: false,
845
+ }),
846
+ "space:post": receiver({
847
+ receiver: "space:post",
848
+ summary: "Post a message to a Space — authenticated member or public Space; stamps space:<slug> tag so the message appears in the Space's thread on slug/in alongside every channel's conversation",
849
+ request: z.object({
850
+ space: z.string(),
851
+ content: z.string(),
852
+ tags: z.array(z.string()).optional(),
853
+ replyTo: z.string().optional(), // parent message id → cascading reply (channels persists the edge)
854
+ }),
855
+ response: z.object({
856
+ ok: z.boolean(),
857
+ id: z.string().optional(),
858
+ ts: z.number().optional(),
859
+ error: z.string().optional(),
860
+ }),
861
+ effect: "ask",
862
+ idempotent: false,
863
+ }),
864
+ "space:create": receiver({
865
+ receiver: "space:create",
866
+ summary: "Create a new Space — registers the channel_link inbox door in channels so every inbound signal is mirrored to the workspace thread read model; private flag restricts posting to members",
867
+ request: z.object({
868
+ slug: z.string(),
869
+ name: z.string().optional(),
870
+ private: z.boolean().optional(),
871
+ }),
872
+ response: z.object({
873
+ ok: z.boolean(),
874
+ space: z.string().optional(),
875
+ error: z.string().optional(),
876
+ }),
877
+ effect: "ask",
878
+ idempotent: false,
879
+ }),
880
+ "space:join": receiver({
881
+ receiver: "space:join",
882
+ summary: "Join a public Space — caller begins receiving its signals and their posts are stamped with the space tag",
883
+ request: z.object({ space: z.string() }),
884
+ response: z.object({
885
+ ok: z.boolean(),
886
+ error: z.string().optional(),
887
+ }),
888
+ effect: "ask",
889
+ idempotent: true,
890
+ }),
891
+ "space:invite": receiver({
892
+ receiver: "space:invite",
893
+ summary: "Invite an actor to a private Space — grants membership so they can read and post; requires the caller to be a member or admin of the Space",
894
+ request: z.object({
895
+ space: z.string(),
896
+ who: z.string(),
897
+ role: z.string().optional(),
898
+ }),
899
+ response: z.object({
900
+ ok: z.boolean(),
901
+ verdict: z.string().optional(),
902
+ error: z.string().optional(),
903
+ }),
904
+ effect: "ask",
905
+ idempotent: false,
906
+ }),
767
907
  "chat:send": receiver({
768
908
  receiver: "chat:send",
769
909
  summary: "Send a message to a group the caller is a member of — sender is always the authenticated caller",
@@ -776,7 +916,449 @@ export const RECEIVERS = {
776
916
  response: z.object({ ok: z.boolean(), reply: z.string().optional(), error: z.string().optional() }),
777
917
  effect: "ask", auth: "member",
778
918
  }),
919
+ // ── chat-context — the threaded #staff fabric: grade · context · harden · lens
920
+ // (text/chat-context.md). `space:post` (the post verb) is declared above. Handlers
921
+ // in resolvers/{messaging,entity}.ts; all IDOR-gated off ctx.ownerSlug. ──────────
922
+ // chat:context — the agent skin: a budget-bounded context pack for one message in
923
+ // a thread (spine + pins + budget-filled siblings + summary tail), returned both
924
+ // structured and serialized for a model. What an agent reads instead of the thread.
925
+ "chat:context": receiver({
926
+ receiver: "chat:context",
927
+ summary: "Assemble a budget-bounded context pack (spine + pins + siblings + summary) for a thread message — structured + serialized for an agent",
928
+ request: z.object({
929
+ entityId: z.string().optional(), // thread id (alias: id)
930
+ targetId: z.string().optional(), // anchor message (default: latest)
931
+ maxTokens: z.number().int().optional(), // budget, clamped 1..8000 (default 1500)
932
+ asActor: z.string().optional(), // serialize from this actor's vantage
933
+ }),
934
+ response: z.object({
935
+ ok: z.boolean(),
936
+ pack: z.object({
937
+ spine: z.array(z.unknown()),
938
+ pins: z.array(z.unknown()),
939
+ siblings: z.array(z.unknown()),
940
+ summary: z.string(),
941
+ }).optional(),
942
+ serialized: z.string().optional(),
943
+ error: z.string().optional(),
944
+ }),
945
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
946
+ }),
947
+ // chat:grade — reinforcement on a message (1..5). Persists the score and closes
948
+ // the loop onto the locked verbs: mark(>=4) strengthens the responder's path,
949
+ // warn(<=2) adds resistance, 3 neutral. Re-grading the same score is a no-op.
950
+ "chat:grade": receiver({
951
+ receiver: "chat:grade",
952
+ summary: "Grade a chat message 1..5 — mark(>=4)/warn(<=2) the responder's path so routing learns who answers well",
953
+ request: z.object({
954
+ messageId: z.string(), // alias: id
955
+ score: z.number().int().min(1).max(5),
956
+ }),
957
+ response: z.object({
958
+ ok: z.boolean(),
959
+ score: z.number().optional(),
960
+ reinforced: z.string().optional(), // 'mark' | 'warn' | 'unchanged' | undefined (neutral)
961
+ target: z.string().optional(),
962
+ error: z.string().optional(),
963
+ }),
964
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
965
+ }),
966
+ // chat:harden — crystallise a resolved thread (#decision or grade>=4 #answer) into
967
+ // a dim-6 learning. Composes assertHypothesis; marks the spine. Refuses unresolved.
968
+ "chat:harden": receiver({
969
+ receiver: "chat:harden",
970
+ summary: "Harden a resolved thread into a dim-6 learning — talk → grade → harden → knowledge; refuses an unresolved thread",
971
+ request: z.object({
972
+ entityId: z.string().optional(), // thread id (alias: id)
973
+ claim: z.string().optional(), // explicit statement; else derived from the thread
974
+ }),
975
+ response: z.object({
976
+ ok: z.boolean(),
977
+ hid: z.string().optional(), // hypothesis id
978
+ statement: z.string().optional(),
979
+ error: z.string().optional(), // 'not_resolved' if no decision/answer to harden
980
+ }),
981
+ effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
982
+ }),
983
+ // lens:run — browse a thread's forest by tag (AND / not) + recency or salience.
984
+ "lens:run": receiver({
985
+ receiver: "lens:run",
986
+ summary: "Browse a thread's forest by tag query (tags AND, not exclude) ordered by recency or salience",
987
+ request: z.object({
988
+ entityId: z.string().optional(), // thread id (alias: id)
989
+ tags: z.array(z.string()).optional(), // AND-match
990
+ not: z.array(z.string()).optional(), // exclude
991
+ order: z.enum(["recency", "salience"]).optional(),
992
+ limit: z.number().int().optional(),
993
+ }),
994
+ response: z.object({ ok: z.boolean(), count: z.number().optional(), messages: z.array(z.unknown()).optional(), error: z.string().optional() }),
995
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
996
+ }),
997
+ // lens:save — persist a named tag-query for the workspace (upsert by name), so a
998
+ // frequent browse becomes a one-name re-run / a feed to follow.
999
+ "lens:save": receiver({
1000
+ receiver: "lens:save",
1001
+ summary: "Save a named tag-query lens for the workspace (upsert by name) — a reusable browse / followable feed",
1002
+ request: z.object({
1003
+ name: z.string(),
1004
+ tags: z.array(z.string()).optional(),
1005
+ not: z.array(z.string()).optional(),
1006
+ order: z.enum(["recency", "salience"]).optional(),
1007
+ limit: z.number().int().optional(),
1008
+ }),
1009
+ response: z.object({ ok: z.boolean(), lens: z.unknown().optional(), count: z.number().optional(), error: z.string().optional() }),
1010
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1011
+ }),
1012
+ // lens:list — the workspace's saved lenses.
1013
+ "lens:list": receiver({
1014
+ receiver: "lens:list",
1015
+ summary: "List the workspace's saved lenses",
1016
+ request: z.object({}),
1017
+ response: z.object({ ok: z.boolean(), lenses: z.array(z.unknown()).optional(), error: z.string().optional() }),
1018
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1019
+ }),
1020
+ // lens:delete — remove a saved lens by name.
1021
+ "lens:delete": receiver({
1022
+ receiver: "lens:delete",
1023
+ summary: "Delete a saved lens by name",
1024
+ request: z.object({ name: z.string() }),
1025
+ response: z.object({ ok: z.boolean(), removed: z.number().optional(), error: z.string().optional() }),
1026
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
1027
+ }),
1028
+ // ── entity (inbox collaboration) — operate on one conversation/entity: reply,
1029
+ // tag/stage-move, profile meta, person link, rename, participants
1030
+ // (text/inbox-collaboration-plan.md). Handlers in resolvers/entity.ts; all
1031
+ // IDOR-gated off ctx.ownerSlug and re-check the row's workspace. ────────────────
1032
+ // entity:reply — the single outbound send. Persists an operator/agent reply, mirrors
1033
+ // it to the customer's channel when the thread isn't web, supersedes a pending draft.
1034
+ // kind='note' stays internal (no egress). replyTo cascades within the thread only.
1035
+ "entity:reply": receiver({
1036
+ receiver: "entity:reply",
1037
+ summary: "Reply to a conversation — persists the message, delivers to the customer's channel (telegram/discord) when not web, clears the pending draft; kind:'note' stays internal",
1038
+ request: z.object({
1039
+ entityId: z.string(), // conv/session/thread id (alias: id)
1040
+ content: z.string(),
1041
+ kind: z.enum(["send", "note"]).optional(), // 'send' (default, egresses) | 'note' (internal)
1042
+ author: z.enum(["staff", "agent"]).optional(), // who speaks (default 'staff')
1043
+ replyTo: z.string().optional(), // parent message id — must be in this thread
1044
+ tags: z.array(z.string()).optional(), // ride the message for the forest's salience axis (≤16)
1045
+ }),
1046
+ response: z.object({
1047
+ ok: z.boolean(),
1048
+ delivered: z.string().optional(), // 'web' | 'telegram' | 'discord' | 'failed'
1049
+ kind: z.string().optional(),
1050
+ error: z.string().optional(),
1051
+ }),
1052
+ effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
1053
+ }),
1054
+ // entity:tag — add/remove free tags, or move an entity between lifecycle stages
1055
+ // (the single stage_transition writer — emits move/rejected/failed receipts).
1056
+ "entity:tag": receiver({
1057
+ receiver: "entity:tag",
1058
+ summary: "Tag a conversation/entity (add/remove) or move it between lifecycle stages — the sole stage_transition writer",
1059
+ request: z.object({
1060
+ id: z.string(),
1061
+ add: z.array(z.string()).optional(),
1062
+ remove: z.array(z.string()).optional(),
1063
+ move: z.object({
1064
+ lifecycle: z.string(),
1065
+ from: z.string().optional(),
1066
+ to: z.string(),
1067
+ by: z.string().optional(),
1068
+ source: z.string().optional(),
1069
+ }).optional(),
1070
+ }),
1071
+ response: z.object({
1072
+ ok: z.boolean(),
1073
+ tags: z.array(z.string()).optional(),
1074
+ moved: z.object({ lifecycle: z.string(), from: z.string().optional(), to: z.string() }).optional(),
1075
+ error: z.string().optional(),
1076
+ }),
1077
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1078
+ }),
1079
+ // entity:meta — editable profile fields for an entity (website, industry, location, …).
1080
+ "entity:meta": receiver({
1081
+ receiver: "entity:meta",
1082
+ summary: "Patch an entity's profile fields (website, industry, location, city, country, size, description, phone, email) — empty value clears the field",
1083
+ request: z.object({ id: z.string(), meta: z.record(z.string(), z.unknown()) }),
1084
+ response: z.object({ ok: z.boolean(), meta: z.record(z.string(), z.string()).optional(), error: z.string().optional() }),
1085
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1086
+ }),
1087
+ // entity:connect — link/unlink an inbox entity to one known person (omit person → unlink).
1088
+ "entity:connect": receiver({
1089
+ receiver: "entity:connect",
1090
+ summary: "Link an inbox conversation to a known person (by uid or label); omit both to unlink",
1091
+ request: z.object({ id: z.string(), personUid: z.string().optional(), personLabel: z.string().optional() }),
1092
+ response: z.object({ ok: z.boolean(), personUid: z.string().nullable().optional(), personLabel: z.string().optional(), error: z.string().optional() }),
1093
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1094
+ }),
1095
+ // entity:rename — set/clear an inbox entity's subject line (empty → clear).
1096
+ "entity:rename": receiver({
1097
+ receiver: "entity:rename",
1098
+ summary: "Set or clear a conversation's subject line (≤140 chars; empty clears)",
1099
+ request: z.object({ id: z.string(), subject: z.string().optional() }),
1100
+ response: z.object({ ok: z.boolean(), subject: z.string().nullable().optional(), error: z.string().optional() }),
1101
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1102
+ }),
1103
+ // entity:participant — add/remove an actor on one conversation (remove by uid).
1104
+ "entity:participant": receiver({
1105
+ receiver: "entity:participant",
1106
+ summary: "Add or remove an actor on a conversation (pass remove:<uid> to remove, or uid/kind/label to add)",
1107
+ request: z.object({
1108
+ id: z.string(),
1109
+ uid: z.string().optional(),
1110
+ kind: z.string().optional(),
1111
+ label: z.string().optional(),
1112
+ remove: z.string().optional(),
1113
+ }),
1114
+ response: z.object({
1115
+ ok: z.boolean(),
1116
+ participant: z.object({ uid: z.string(), kind: z.string(), label: z.string() }).optional(),
1117
+ removed: z.string().optional(),
1118
+ error: z.string().optional(),
1119
+ }),
1120
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1121
+ }),
1122
+ // ── Deliberately NOT contracted (internal plumbing, not an agent capability):
1123
+ // reply:web · reply:telegram · reply:discord — channel egress fan-out, driven by
1124
+ // entity:reply, never called directly by an agent.
1125
+ // conversation:autonomy — read by the channels worker per inbound turn (send vs
1126
+ // draft), not an operator/agent verb.
1127
+ // member:invited — an internal event hook, not a callable capability.
1128
+ // ui:campaigns:compile · ui:campaigns:approve — UI build-pipeline steps.
1129
+ // These have live resolvers but no RECEIVERS entry by design — keep it that way.
1130
+ // ── commerce (storefront products) — handlers in resolvers/commerce.ts ─────────
1131
+ "products:create": receiver({
1132
+ receiver: "products:create",
1133
+ summary: "Create a storefront product with one or more prices",
1134
+ request: z.object({
1135
+ slug: z.string(),
1136
+ name: z.string(),
1137
+ prices: z.array(z.object({
1138
+ unit_amount: z.number(),
1139
+ currency: z.string().optional(),
1140
+ price_type: z.string().optional(),
1141
+ interval: z.string().optional(),
1142
+ })),
1143
+ description: z.string().optional(),
1144
+ images: z.array(z.string()).optional(),
1145
+ product_type: z.string().optional(), // default 'one_time'
1146
+ collection: z.string().optional(),
1147
+ }),
1148
+ response: z.object({ pid: z.string().optional(), ppid: z.string().optional(), name: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
1149
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1150
+ }),
1151
+ "products:update": receiver({
1152
+ receiver: "products:update",
1153
+ summary: "Update a product's name, description, images, or collection",
1154
+ request: z.object({ slug: z.string(), pid: z.string(), name: z.string().optional(), description: z.string().optional(), images: z.array(z.string()).optional(), collection: z.string().optional() }),
1155
+ response: z.object({ pid: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
1156
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
1157
+ }),
1158
+ "products:list": receiver({
1159
+ receiver: "products:list",
1160
+ summary: "List a workspace's storefront products with their prices (public)",
1161
+ request: z.object({ slug: z.string().optional() }),
1162
+ response: z.object({ products: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
1163
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
1164
+ }),
1165
+ "products:archive": receiver({
1166
+ receiver: "products:archive",
1167
+ summary: "Archive a product (reversible via products:update)",
1168
+ request: z.object({ slug: z.string(), pid: z.string() }),
1169
+ response: z.object({ pid: z.string().optional(), workspace: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
1170
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1171
+ }),
1172
+ "storefront:stats": receiver({
1173
+ receiver: "storefront:stats",
1174
+ summary: "Storefront funnel stats — views, checkouts, purchases, revenue, per-product breakdown",
1175
+ request: z.object({ slug: z.string().optional() }),
1176
+ response: z.object({
1177
+ views: z.number().optional(),
1178
+ checkouts: z.number().optional(),
1179
+ purchases: z.number().optional(),
1180
+ revenue_cents: z.number().optional(),
1181
+ byProduct: z.array(z.record(z.string(), z.unknown())).optional(),
1182
+ error: z.string().optional(),
1183
+ }),
1184
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1185
+ }),
1186
+ // ── pages + workspace settings — handlers in resolvers/pages.ts ────────────────
1187
+ "pages:create": receiver({
1188
+ receiver: "pages:create",
1189
+ summary: "Create a workspace page (draft) from a title + sections",
1190
+ request: z.object({ slug: z.string(), title: z.string(), sections: z.record(z.string(), z.unknown()), pageSlug: z.string().optional() }),
1191
+ response: z.object({ ok: z.boolean(), slug: z.string().optional(), title: z.string().optional(), url: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
1192
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1193
+ }),
1194
+ "pages:edit": receiver({
1195
+ receiver: "pages:edit",
1196
+ summary: "Edit a workspace page's title or sections",
1197
+ request: z.object({ slug: z.string(), page: z.string(), title: z.string().optional(), sections: z.record(z.string(), z.unknown()).optional() }),
1198
+ response: z.object({ ok: z.boolean(), slug: z.string().optional(), title: z.string().optional(), url: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
1199
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1200
+ }),
1201
+ "pages:list": receiver({
1202
+ receiver: "pages:list",
1203
+ summary: "List a workspace's pages with status",
1204
+ request: z.object({ slug: z.string() }),
1205
+ response: z.object({ pages: z.array(z.object({ slug: z.string(), title: z.string(), status: z.string() })) }),
1206
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1207
+ }),
1208
+ "pages:publish": receiver({
1209
+ receiver: "pages:publish",
1210
+ summary: "Publish a workspace page",
1211
+ request: z.object({ slug: z.string(), page: z.string() }),
1212
+ response: z.object({ ok: z.boolean(), slug: z.string().optional(), title: z.string().optional(), url: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
1213
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1214
+ }),
1215
+ "settings:write": receiver({
1216
+ receiver: "settings:write",
1217
+ summary: "Write a workspace setting value (caller or authorized descendant)",
1218
+ request: z.object({ slug: z.string().optional(), name: z.string().optional(), description: z.string().optional(), value: z.string() }),
1219
+ response: z.object({ ok: z.boolean(), slug: z.string().optional(), message: z.string().optional(), error: z.string().optional() }),
1220
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1221
+ }),
1222
+ "domain:set": receiver({
1223
+ receiver: "domain:set",
1224
+ summary: "Bind a custom domain to a workspace — returns the verification TXT/CNAME records to add",
1225
+ request: z.object({ slug: z.string(), actorId: z.string(), host: z.string() }),
1226
+ response: z.object({ ok: z.boolean(), intent: z.string().optional(), host: z.string().optional(), verifyToken: z.string().optional(), txtRecord: z.string().optional(), cnameTarget: z.string().optional(), reason: z.string().optional() }),
1227
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "verify_domain",
1228
+ }),
1229
+ // ── courses + playbook — handlers in resolvers/courses.ts ──────────────────────
1230
+ "course:save": receiver({
1231
+ receiver: "course:save",
1232
+ summary: "Create or update a course (upsert by courseId)",
1233
+ request: z.object({ courseId: z.string(), title: z.string().optional(), description: z.string().optional(), published: z.boolean().optional(), price_cents: z.number().optional() }),
1234
+ response: z.object({ ok: z.boolean(), courseId: z.string().optional() }),
1235
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
1236
+ }),
1237
+ "course:get": receiver({
1238
+ receiver: "course:get",
1239
+ summary: "Get a course with its lessons (lesson bodies redacted unless enrolled or owner)",
1240
+ request: z.object({ workspace: z.string().optional(), slug: z.string().optional(), courseId: z.string() }),
1241
+ response: z.object({ course: z.record(z.string(), z.unknown()).optional(), lessons: z.array(z.record(z.string(), z.unknown())).optional(), enrolled: z.boolean().optional() }),
1242
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
1243
+ }),
1244
+ "course:list": receiver({
1245
+ receiver: "course:list",
1246
+ summary: "List a workspace's courses (published for others, all for the owner)",
1247
+ request: z.object({ workspace: z.string().optional(), slug: z.string().optional() }),
1248
+ response: z.object({ courses: z.array(z.record(z.string(), z.unknown())).optional() }),
1249
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
1250
+ }),
1251
+ "course:enroll": receiver({
1252
+ receiver: "course:enroll",
1253
+ summary: "Enroll the caller in a course (payment_required if priced and not comped)",
1254
+ request: z.object({ workspace: z.string().optional(), slug: z.string().optional(), courseId: z.string() }),
1255
+ response: z.object({ ok: z.boolean(), enrolled: z.boolean().optional() }),
1256
+ effect: "ask", cost: "variable", reversible: false, idempotent: true, auth: "member",
1257
+ }),
1258
+ "course:lessons-save": receiver({
1259
+ receiver: "course:lessons-save",
1260
+ summary: "Upsert one or more lessons on a course",
1261
+ request: z.object({ courseId: z.string(), lessons: z.union([z.array(z.record(z.string(), z.unknown())), z.record(z.string(), z.unknown())]) }),
1262
+ response: z.object({ ok: z.boolean(), saved: z.number().optional() }),
1263
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
1264
+ }),
1265
+ "playbook:save": receiver({
1266
+ receiver: "playbook:save",
1267
+ summary: "Save a learner's playbook entry for a course lesson (upsert)",
1268
+ request: z.object({ course: z.string(), lesson: z.string(), content: z.string().optional() }),
1269
+ response: z.object({ ok: z.boolean() }),
1270
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
1271
+ }),
1272
+ "playbook:get": receiver({
1273
+ receiver: "playbook:get",
1274
+ summary: "Get the caller's playbook entries (optionally scoped to one course)",
1275
+ request: z.object({ course: z.string().optional() }),
1276
+ response: z.object({ entries: z.array(z.record(z.string(), z.unknown())).optional() }),
1277
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
1278
+ }),
1279
+ "playbook:promote": receiver({
1280
+ receiver: "playbook:promote",
1281
+ summary: "Promote a completed playbook into a reusable skill in the workspace's content repo",
1282
+ request: z.object({ course: z.string() }),
1283
+ response: z.object({ ok: z.boolean(), skillRef: z.string().optional() }),
1284
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1285
+ }),
1286
+ // ── social + crawl — handlers in resolvers/social.ts ───────────────────────────
1287
+ "social:publish": receiver({
1288
+ receiver: "social:publish",
1289
+ summary: "Publish a drafted social post to its platform",
1290
+ request: z.object({ slug: z.string(), actorId: z.string(), id: z.string() }),
1291
+ response: z.object({ ok: z.boolean(), id: z.string().optional(), status: z.string().optional(), reason: z.string().optional() }),
1292
+ effect: "ask", cost: "variable", reversible: false, idempotent: false, auth: "update_group",
1293
+ }),
1294
+ "social:news-to-drafts": receiver({
1295
+ receiver: "social:news-to-drafts",
1296
+ summary: "Turn fresh news items into platform-specific social post drafts",
1297
+ request: z.object({ groupId: z.string().optional(), maxItems: z.number().int().max(10).optional(), platforms: z.array(z.string()).optional() }),
1298
+ response: z.object({ ok: z.boolean(), draftsCreated: z.number().optional(), total: z.number().optional(), fresh: z.number().optional() }),
1299
+ effect: "ask", cost: "variable", reversible: false, idempotent: false, auth: "member",
1300
+ }),
1301
+ "web:crawl": receiver({
1302
+ receiver: "web:crawl",
1303
+ summary: "Fetch a URL and extract domain, name, description, and readable text (public)",
1304
+ request: z.object({ url: z.string() }),
1305
+ response: z.object({ ok: z.boolean(), url: z.string().optional(), domain: z.string().optional(), name: z.string().optional(), description: z.string().optional(), text: z.string().optional(), error: z.string().optional() }),
1306
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
1307
+ }),
1308
+ // ── people / companies / conversation — handlers in resolvers/groups.ts ────────
1309
+ "people:create": receiver({
1310
+ receiver: "people:create",
1311
+ summary: "Create a person actor in the workspace",
1312
+ request: z.object({ name: z.string(), email: z.string().optional() }),
1313
+ response: z.object({ ok: z.boolean(), aid: z.string().optional(), name: z.string().optional(), error: z.string().optional() }),
1314
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1315
+ }),
1316
+ "companies:create": receiver({
1317
+ receiver: "companies:create",
1318
+ summary: "Create a company group in the workspace",
1319
+ request: z.object({ name: z.string() }),
1320
+ response: z.object({ ok: z.boolean(), gid: z.string().optional(), name: z.string().optional(), error: z.string().optional() }),
1321
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
1322
+ }),
1323
+ "conversation:start": receiver({
1324
+ receiver: "conversation:start",
1325
+ summary: "Start a new conversation thread (optionally seeded with a message, function lens, or recipient)",
1326
+ request: z.object({ message: z.string().optional(), fn: z.string().optional(), actorId: z.string().optional(), to: z.string().optional() }),
1327
+ response: z.object({ ok: z.boolean(), threadId: z.string().optional(), error: z.string().optional() }),
1328
+ effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
1329
+ }),
1330
+ // ── billing — handlers in resolvers/market.ts ──────────────────────────────────
1331
+ "billing:plan": receiver({
1332
+ receiver: "billing:plan",
1333
+ summary: "Change a workspace's plan — returns a checkout URL to complete the upgrade",
1334
+ request: z.object({ slug: z.string(), actorId: z.string(), plan: z.enum(["pro", "agency", "scale"]) }),
1335
+ response: z.object({ ok: z.boolean(), intent: z.string().optional(), checkoutUrl: z.string().optional(), plan: z.string().optional(), reason: z.string().optional() }),
1336
+ effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "update_group",
1337
+ }),
1338
+ "billing:topup": receiver({
1339
+ receiver: "billing:topup",
1340
+ summary: "Top up workspace AI credits — returns a checkout URL for the amount",
1341
+ request: z.object({ slug: z.string(), actorId: z.string(), amount: z.number(), currency: z.string().optional() }),
1342
+ response: z.object({ ok: z.boolean(), intent: z.string().optional(), checkoutUrl: z.string().optional(), amount: z.number().optional(), currency: z.string().optional(), reason: z.string().optional() }),
1343
+ effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "update_group",
1344
+ }),
1345
+ // ── video — handler in resolvers/video.ts ──────────────────────────────────────
1346
+ "video:ensure-defaults": receiver({
1347
+ receiver: "video:ensure-defaults",
1348
+ summary: "Ensure a workspace's default video rooms exist — returns which were created vs already present",
1349
+ request: z.object({ workspace: z.string().optional(), slug: z.string().optional() }),
1350
+ response: z.object({ ok: z.boolean(), created: z.array(z.string()).optional(), existing: z.array(z.string()).optional(), error: z.string().optional() }),
1351
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
1352
+ }),
779
1353
  // ── booking — one calendar, two doors (text/booking-plan.md §5) ───────────────
1354
+ // booking:confirm-payment (public) finalises a paid booking by payment ref.
1355
+ "booking:confirm-payment": receiver({
1356
+ receiver: "booking:confirm-payment",
1357
+ summary: "Confirm a booking after payment settles, by payment reference (public)",
1358
+ request: z.object({ paymentRef: z.string() }),
1359
+ response: z.object({ ok: z.boolean(), error: z.string().optional() }),
1360
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
1361
+ }),
780
1362
  // Both public-bookable: a customer with no account books a slot. The provider
781
1363
  // side (configure a service) is the separate, gated booking:configure.
782
1364
  "booking:list-slots": receiver({