@awiki/cli 0.0.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.github/workflows/release.yml +44 -0
  2. package/.goreleaser.yml +44 -0
  3. package/AGENTS.md +60 -0
  4. package/CLAUDE.md +192 -0
  5. package/README.md +2 -0
  6. package/docs/architecture/awiki-command-v2.md +955 -0
  7. package/docs/architecture/awiki-skill-architecture.md +475 -0
  8. package/docs/architecture/awiki-v2-architecture.md +1063 -0
  9. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
  10. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
  11. package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
  12. package/docs/harness/review-spec.md +474 -0
  13. package/docs/installation.md +372 -0
  14. package/docs/plan/awiki-v2-implementation-plan.md +903 -0
  15. package/docs/plan/phase-0/adr-index.md +56 -0
  16. package/docs/plan/phase-0/audit-findings.md +251 -0
  17. package/docs/plan/phase-0/capability-mapping.md +108 -0
  18. package/docs/plan/phase-0/implementation-constraints.md +363 -0
  19. package/docs/publish.md +169 -0
  20. package/go.mod +29 -0
  21. package/go.sum +73 -0
  22. package/internal/anpsdk/registry.go +63 -0
  23. package/internal/authsdk/session.go +351 -0
  24. package/internal/buildinfo/buildinfo.go +34 -0
  25. package/internal/cli/app.go +136 -0
  26. package/internal/cli/app_test.go +88 -0
  27. package/internal/cli/debug.go +104 -0
  28. package/internal/cli/group.go +263 -0
  29. package/internal/cli/id.go +473 -0
  30. package/internal/cli/init.go +134 -0
  31. package/internal/cli/msg.go +228 -0
  32. package/internal/cli/page.go +267 -0
  33. package/internal/cli/root.go +499 -0
  34. package/internal/cli/runtime.go +232 -0
  35. package/internal/cli/upgrade.go +60 -0
  36. package/internal/cmdmeta/catalog.go +203 -0
  37. package/internal/cmdmeta/catalog_test.go +21 -0
  38. package/internal/config/config.go +399 -0
  39. package/internal/config/config_test.go +104 -0
  40. package/internal/config/write.go +37 -0
  41. package/internal/content/service.go +314 -0
  42. package/internal/content/service_test.go +165 -0
  43. package/internal/content/types.go +44 -0
  44. package/internal/docs/topics.go +110 -0
  45. package/internal/doctor/doctor.go +306 -0
  46. package/internal/identity/client.go +267 -0
  47. package/internal/identity/did.go +85 -0
  48. package/internal/identity/did_test.go +50 -0
  49. package/internal/identity/layout.go +206 -0
  50. package/internal/identity/legacy.go +378 -0
  51. package/internal/identity/public.go +70 -0
  52. package/internal/identity/public_test.go +73 -0
  53. package/internal/identity/readiness.go +74 -0
  54. package/internal/identity/service.go +826 -0
  55. package/internal/identity/store.go +385 -0
  56. package/internal/identity/store_test.go +180 -0
  57. package/internal/identity/types.go +204 -0
  58. package/internal/message/auth.go +167 -0
  59. package/internal/message/group_service.go +838 -0
  60. package/internal/message/group_wire.go +350 -0
  61. package/internal/message/group_wire_test.go +67 -0
  62. package/internal/message/helpers.go +61 -0
  63. package/internal/message/http_client.go +334 -0
  64. package/internal/message/proof.go +156 -0
  65. package/internal/message/proof_test.go +61 -0
  66. package/internal/message/service.go +696 -0
  67. package/internal/message/service_test.go +97 -0
  68. package/internal/message/types.go +155 -0
  69. package/internal/message/wire.go +100 -0
  70. package/internal/message/wire_test.go +49 -0
  71. package/internal/message/ws_proxy_client.go +151 -0
  72. package/internal/output/output.go +350 -0
  73. package/internal/output/output_test.go +48 -0
  74. package/internal/runtime/config.go +117 -0
  75. package/internal/runtime/config_test.go +46 -0
  76. package/internal/runtime/listener/files.go +65 -0
  77. package/internal/runtime/listener/manager.go +142 -0
  78. package/internal/runtime/listener/server.go +983 -0
  79. package/internal/runtime/listener/server_test.go +319 -0
  80. package/internal/runtime/listener/sysproc_unix.go +17 -0
  81. package/internal/runtime/listener/sysproc_windows.go +13 -0
  82. package/internal/runtime/listener/types.go +21 -0
  83. package/internal/runtime/listener/wsclient.go +299 -0
  84. package/internal/runtime/listener/wsclient_test.go +41 -0
  85. package/internal/store/dao.go +632 -0
  86. package/internal/store/dao_test.go +87 -0
  87. package/internal/store/helpers.go +197 -0
  88. package/internal/store/import.go +499 -0
  89. package/internal/store/import_test.go +103 -0
  90. package/internal/store/open.go +71 -0
  91. package/internal/store/query.go +151 -0
  92. package/internal/store/schema.go +277 -0
  93. package/internal/store/schema_test.go +56 -0
  94. package/internal/store/types.go +177 -0
  95. package/internal/update/update.go +368 -0
  96. package/package.json +17 -0
  97. package/scripts/install.js +171 -0
  98. package/scripts/release/release-prerelease.sh +86 -0
  99. package/scripts/release/tag-release.sh +66 -0
  100. package/scripts/release/withdraw-release.sh +78 -0
  101. package/scripts/run.js +69 -0
  102. package/skills/README.md +32 -0
  103. package/skills/awiki-bundle/SKILL.md +76 -0
  104. package/skills/awiki-debug/SKILL.md +80 -0
  105. package/skills/awiki-group/SKILL.md +111 -0
  106. package/skills/awiki-id/SKILL.md +123 -0
  107. package/skills/awiki-msg/SKILL.md +131 -0
  108. package/skills/awiki-page/SKILL.md +93 -0
  109. package/skills/awiki-people/SKILL.md +66 -0
  110. package/skills/awiki-runtime/SKILL.md +137 -0
  111. package/skills/awiki-shared/SKILL.md +124 -0
  112. package/skills/awiki-workflow-discovery/SKILL.md +93 -0
  113. package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
  114. package/skills/manifests/skills.yaml +260 -0
  115. package/skills/templates/bundle-skill-template.md +42 -0
  116. package/skills/templates/debug-skill-template.md +44 -0
  117. package/skills/templates/domain-skill-template.md +56 -0
  118. package/skills/templates/shared-skill-template.md +46 -0
  119. package/skills/templates/workflow-skill-template.md +46 -0
@@ -0,0 +1,350 @@
1
+ package message
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ "time"
7
+
8
+ "github.com/agentconnect/awiki-cli/internal/identity"
9
+ )
10
+
11
+ func BuildGroupCreateRPCParams(record *identity.StoredIdentity, manager *identity.Manager, serviceDID string, request GroupCreateRequest) (map[string]any, error) {
12
+ if strings.TrimSpace(serviceDID) == "" {
13
+ return nil, fmt.Errorf("message service did is required")
14
+ }
15
+ auth, err := newAuthContext(record, manager)
16
+ if err != nil {
17
+ return nil, err
18
+ }
19
+ profile := buildGroupProfilePatch(request.Name, request.Description, request.Discoverability, request.Slug, request.Goal, request.Rules, request.MessagePrompt, request.DocURL)
20
+ policy := buildGroupPolicyPatch(request.AdmissionMode, request.AttachmentsAllowed, request.MaxMembers, request.MemberMaxMessages, request.MemberMaxTotalChars)
21
+ if _, ok := profile["display_name"]; !ok {
22
+ return nil, fmt.Errorf("group display name is required")
23
+ }
24
+ if len(policy) == 0 {
25
+ policy = buildGroupPolicyPatch("open-join", boolPtr(true), "500", nil, nil)
26
+ }
27
+ meta := map[string]any{
28
+ "anp_version": "1.0",
29
+ "profile": "anp.group.base.v1",
30
+ "security_profile": "transport-protected",
31
+ "sender_did": record.DID,
32
+ "target": map[string]any{
33
+ "kind": "service",
34
+ "did": serviceDID,
35
+ },
36
+ "operation_id": "op-" + generateOperationID(),
37
+ "created_at": nowRFC3339(),
38
+ "content_type": "application/json",
39
+ }
40
+ body := map[string]any{
41
+ "group_profile": profile,
42
+ "group_policy": policy,
43
+ }
44
+ payload := signedPayload{Method: "group.create", Meta: meta, Body: body}
45
+ actorProof, err := buildActorProof(auth, payload, "anp://service/"+strictPercentEncode(serviceDID)+"/group.create")
46
+ if err != nil {
47
+ return nil, err
48
+ }
49
+ return map[string]any{
50
+ "meta": meta,
51
+ "auth": map[string]any{"scheme": OriginProofScheme, "actor_proof": actorProof},
52
+ "body": body,
53
+ }, nil
54
+ }
55
+
56
+ func BuildGroupGetInfoRPCParams(record *identity.StoredIdentity, request GroupInfoRequest) (map[string]any, error) {
57
+ groupDID := strings.TrimSpace(request.Group)
58
+ if groupDID == "" {
59
+ return nil, ErrGroupRequired
60
+ }
61
+ body := map[string]any{}
62
+ if request.IncludePolicy {
63
+ body["include_policy"] = true
64
+ }
65
+ if request.IncludeMemberList {
66
+ body["include_member_list"] = true
67
+ }
68
+ return map[string]any{
69
+ "meta": map[string]any{
70
+ "anp_version": "1.0",
71
+ "profile": "anp.group.base.v1",
72
+ "security_profile": "transport-protected",
73
+ "sender_did": record.DID,
74
+ "target": map[string]any{
75
+ "kind": "group",
76
+ "did": groupDID,
77
+ },
78
+ },
79
+ "body": body,
80
+ }, nil
81
+ }
82
+
83
+ func BuildGroupJoinRPCParams(record *identity.StoredIdentity, manager *identity.Manager, request GroupJoinRequest) (map[string]any, error) {
84
+ body := map[string]any{}
85
+ if reason := strings.TrimSpace(request.ReasonText); reason != "" {
86
+ body["reason_text"] = reason
87
+ }
88
+ return buildGroupMutationRPCParams(record, manager, request.Group, "group.join", body)
89
+ }
90
+
91
+ func BuildGroupAddRPCParams(record *identity.StoredIdentity, manager *identity.Manager, request GroupMemberRequest) (map[string]any, error) {
92
+ memberDID := strings.TrimSpace(request.Member)
93
+ if memberDID == "" {
94
+ return nil, ErrMemberRequired
95
+ }
96
+ body := map[string]any{"member_did": memberDID}
97
+ if role := strings.TrimSpace(request.Role); role != "" {
98
+ body["role"] = role
99
+ }
100
+ if reason := strings.TrimSpace(request.ReasonText); reason != "" {
101
+ body["reason_text"] = reason
102
+ }
103
+ return buildGroupMutationRPCParams(record, manager, request.Group, "group.add", body)
104
+ }
105
+
106
+ func BuildGroupRemoveRPCParams(record *identity.StoredIdentity, manager *identity.Manager, request GroupMemberRequest) (map[string]any, error) {
107
+ memberDID := strings.TrimSpace(request.Member)
108
+ if memberDID == "" {
109
+ return nil, ErrMemberRequired
110
+ }
111
+ body := map[string]any{"member_did": memberDID}
112
+ if reason := strings.TrimSpace(request.ReasonText); reason != "" {
113
+ body["reason_text"] = reason
114
+ }
115
+ return buildGroupMutationRPCParams(record, manager, request.Group, "group.remove", body)
116
+ }
117
+
118
+ func BuildGroupLeaveRPCParams(record *identity.StoredIdentity, manager *identity.Manager, request GroupLeaveRequest) (map[string]any, error) {
119
+ return buildGroupMutationRPCParams(record, manager, request.Group, "group.leave", map[string]any{})
120
+ }
121
+
122
+ func BuildGroupUpdateProfileRPCParams(record *identity.StoredIdentity, manager *identity.Manager, groupDID string, patch map[string]any) (map[string]any, error) {
123
+ if len(patch) == 0 {
124
+ return nil, fmt.Errorf("group profile patch is required")
125
+ }
126
+ return buildGroupMutationRPCParams(record, manager, groupDID, "group.update_profile", map[string]any{"group_profile_patch": patch})
127
+ }
128
+
129
+ func BuildGroupUpdatePolicyRPCParams(record *identity.StoredIdentity, manager *identity.Manager, groupDID string, patch map[string]any) (map[string]any, error) {
130
+ if len(patch) == 0 {
131
+ return nil, fmt.Errorf("group policy patch is required")
132
+ }
133
+ return buildGroupMutationRPCParams(record, manager, groupDID, "group.update_policy", map[string]any{"group_policy_patch": patch})
134
+ }
135
+
136
+ func BuildGroupSendRPCParams(record *identity.StoredIdentity, manager *identity.Manager, groupDID string, text string, messageType string) (map[string]any, error) {
137
+ if strings.TrimSpace(groupDID) == "" {
138
+ return nil, ErrGroupRequired
139
+ }
140
+ if strings.TrimSpace(text) == "" {
141
+ return nil, ErrTextRequired
142
+ }
143
+ auth, err := newAuthContext(record, manager)
144
+ if err != nil {
145
+ return nil, err
146
+ }
147
+ contentType := contentTypeForMessageType(messageType)
148
+ meta := map[string]any{
149
+ "anp_version": "1.0",
150
+ "profile": "anp.group.base.v1",
151
+ "security_profile": "transport-protected",
152
+ "sender_did": record.DID,
153
+ "target": map[string]any{
154
+ "kind": "group",
155
+ "did": groupDID,
156
+ },
157
+ "operation_id": "op-" + generateOperationID(),
158
+ "message_id": "msg-" + generateOperationID(),
159
+ "created_at": time.Now().UTC().Format(time.RFC3339),
160
+ "content_type": contentType,
161
+ }
162
+ body := map[string]any{"text": text}
163
+ payload := signedPayload{Method: "group.send", Meta: meta, Body: body}
164
+ actorProof, err := buildActorProof(auth, payload, "anp://group/"+strictPercentEncode(groupDID))
165
+ if err != nil {
166
+ return nil, err
167
+ }
168
+ return map[string]any{
169
+ "meta": meta,
170
+ "auth": map[string]any{"scheme": OriginProofScheme, "actor_proof": actorProof},
171
+ "body": body,
172
+ }, nil
173
+ }
174
+
175
+ func BuildGroupGetRPCParams(record *identity.StoredIdentity, request GroupGetRequest) (map[string]any, error) {
176
+ groupDID := strings.TrimSpace(request.Group)
177
+ if groupDID == "" {
178
+ return nil, ErrGroupRequired
179
+ }
180
+ return map[string]any{
181
+ "meta": map[string]any{
182
+ "anp_version": "1.0",
183
+ "profile": "anp.group.local.v1",
184
+ "security_profile": "transport-protected",
185
+ "sender_did": record.DID,
186
+ "target": map[string]any{
187
+ "kind": "group",
188
+ "did": groupDID,
189
+ },
190
+ },
191
+ "body": map[string]any{"group_did": groupDID},
192
+ }, nil
193
+ }
194
+
195
+ func BuildGroupMembersRPCParams(record *identity.StoredIdentity, request GroupMembersRequest) (map[string]any, error) {
196
+ groupDID := strings.TrimSpace(request.Group)
197
+ if groupDID == "" {
198
+ return nil, ErrGroupRequired
199
+ }
200
+ limit := request.Limit
201
+ if limit <= 0 {
202
+ limit = 100
203
+ }
204
+ return map[string]any{
205
+ "meta": map[string]any{
206
+ "anp_version": "1.0",
207
+ "profile": "anp.group.local.v1",
208
+ "security_profile": "transport-protected",
209
+ "sender_did": record.DID,
210
+ "target": map[string]any{
211
+ "kind": "group",
212
+ "did": groupDID,
213
+ },
214
+ },
215
+ "body": map[string]any{"group_did": groupDID, "limit": limit},
216
+ }, nil
217
+ }
218
+
219
+ func BuildGroupMessagesRPCParams(record *identity.StoredIdentity, request GroupMessagesRequest) (map[string]any, error) {
220
+ groupDID := strings.TrimSpace(request.Group)
221
+ if groupDID == "" {
222
+ return nil, ErrGroupRequired
223
+ }
224
+ limit := request.Limit
225
+ if limit <= 0 {
226
+ limit = 50
227
+ }
228
+ body := map[string]any{"group_did": groupDID, "limit": limit}
229
+ if cursor := strings.TrimSpace(request.Cursor); cursor != "" {
230
+ body["since_seq"] = cursor
231
+ }
232
+ return map[string]any{
233
+ "meta": map[string]any{
234
+ "anp_version": "1.0",
235
+ "profile": "anp.group.local.v1",
236
+ "security_profile": "transport-protected",
237
+ "sender_did": record.DID,
238
+ "target": map[string]any{
239
+ "kind": "group",
240
+ "did": groupDID,
241
+ },
242
+ },
243
+ "body": body,
244
+ }, nil
245
+ }
246
+
247
+ func buildGroupMutationRPCParams(record *identity.StoredIdentity, manager *identity.Manager, groupDID string, method string, body map[string]any) (map[string]any, error) {
248
+ groupDID = strings.TrimSpace(groupDID)
249
+ if groupDID == "" {
250
+ return nil, ErrGroupRequired
251
+ }
252
+ auth, err := newAuthContext(record, manager)
253
+ if err != nil {
254
+ return nil, err
255
+ }
256
+ meta := map[string]any{
257
+ "anp_version": "1.0",
258
+ "profile": "anp.group.base.v1",
259
+ "security_profile": "transport-protected",
260
+ "sender_did": record.DID,
261
+ "target": map[string]any{
262
+ "kind": "group",
263
+ "did": groupDID,
264
+ },
265
+ "operation_id": "op-" + generateOperationID(),
266
+ "created_at": nowRFC3339(),
267
+ "content_type": "application/json",
268
+ }
269
+ payload := signedPayload{Method: method, Meta: meta, Body: body}
270
+ actorProof, err := buildActorProof(auth, payload, "anp://group/"+strictPercentEncode(groupDID))
271
+ if err != nil {
272
+ return nil, err
273
+ }
274
+ return map[string]any{
275
+ "meta": meta,
276
+ "auth": map[string]any{"scheme": OriginProofScheme, "actor_proof": actorProof},
277
+ "body": body,
278
+ }, nil
279
+ }
280
+
281
+ func buildGroupProfilePatch(name string, description string, discoverability string, slug string, goal string, rules string, messagePrompt string, docURL string) map[string]any {
282
+ patch := map[string]any{}
283
+ if value := strings.TrimSpace(name); value != "" {
284
+ patch["display_name"] = value
285
+ }
286
+ if value := strings.TrimSpace(description); value != "" {
287
+ patch["description"] = value
288
+ }
289
+ if value := strings.TrimSpace(discoverability); value != "" {
290
+ patch["discoverability"] = value
291
+ }
292
+ if value := strings.TrimSpace(slug); value != "" {
293
+ patch["slug"] = value
294
+ }
295
+ if value := strings.TrimSpace(goal); value != "" {
296
+ patch["goal"] = value
297
+ }
298
+ if value := strings.TrimSpace(rules); value != "" {
299
+ patch["rules"] = value
300
+ }
301
+ if value := strings.TrimSpace(messagePrompt); value != "" {
302
+ patch["message_prompt"] = value
303
+ }
304
+ if value := strings.TrimSpace(docURL); value != "" {
305
+ patch["doc_url"] = value
306
+ }
307
+ return patch
308
+ }
309
+
310
+ func buildGroupPolicyPatch(admissionMode string, attachmentsAllowed *bool, maxMembers string, memberMaxMessages *int64, memberMaxTotalChars *int64) map[string]any {
311
+ patch := map[string]any{}
312
+ if value := strings.TrimSpace(admissionMode); value != "" {
313
+ patch["admission_mode"] = value
314
+ }
315
+ if attachmentsAllowed != nil {
316
+ patch["attachments_allowed"] = *attachmentsAllowed
317
+ }
318
+ if value := strings.TrimSpace(maxMembers); value != "" {
319
+ patch["max_members"] = value
320
+ }
321
+ if memberMaxMessages != nil {
322
+ patch["member_max_messages"] = *memberMaxMessages
323
+ }
324
+ if memberMaxTotalChars != nil {
325
+ patch["member_max_total_chars"] = *memberMaxTotalChars
326
+ }
327
+ if len(patch) == 0 {
328
+ return patch
329
+ }
330
+ if _, ok := patch["message_security_profile"]; !ok {
331
+ patch["message_security_profile"] = "transport-protected"
332
+ }
333
+ if _, ok := patch["bootstrap_security_profile"]; !ok {
334
+ patch["bootstrap_security_profile"] = "transport-protected"
335
+ }
336
+ if _, ok := patch["permissions"]; !ok {
337
+ patch["permissions"] = map[string]any{
338
+ "send": "member",
339
+ "add": "admin",
340
+ "remove": "admin",
341
+ "update_profile": "admin",
342
+ "update_policy": "owner",
343
+ }
344
+ }
345
+ return patch
346
+ }
347
+
348
+ func boolPtr(value bool) *bool {
349
+ return &value
350
+ }
@@ -0,0 +1,67 @@
1
+ package message
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/agentconnect/awiki-cli/internal/identity"
7
+ )
8
+
9
+ func TestBuildGroupCreateRPCParamsUsesActorProofAndServiceTarget(t *testing.T) {
10
+ t.Parallel()
11
+
12
+ generated, err := identity.GenerateIdentity(identity.GenerateOptions{
13
+ Hostname: "awiki.ai",
14
+ PathPrefix: []string{"user"},
15
+ ProofDomain: "awiki.ai",
16
+ })
17
+ if err != nil {
18
+ t.Fatalf("GenerateIdentity() error = %v", err)
19
+ }
20
+ record := &identity.StoredIdentity{
21
+ IdentityName: "alice",
22
+ DID: generated.DID,
23
+ DIDDocument: generated.DIDDocument,
24
+ Key1PrivatePEM: generated.Key1PrivatePEM,
25
+ }
26
+
27
+ params, err := BuildGroupCreateRPCParams(record, nil, "did:wba:awiki.ai:services:message:e1_local", GroupCreateRequest{Name: "Protocol Review"})
28
+ if err != nil {
29
+ t.Fatalf("BuildGroupCreateRPCParams() error = %v", err)
30
+ }
31
+ auth, ok := params["auth"].(map[string]any)
32
+ if !ok {
33
+ t.Fatalf("params[auth] = %#v, want map", params["auth"])
34
+ }
35
+ if got := stringFromAny(auth["scheme"]); got != OriginProofScheme {
36
+ t.Fatalf("auth.scheme = %q, want %q", got, OriginProofScheme)
37
+ }
38
+ meta, ok := params["meta"].(map[string]any)
39
+ if !ok {
40
+ t.Fatalf("params[meta] = %#v, want map", params["meta"])
41
+ }
42
+ target, ok := meta["target"].(map[string]any)
43
+ if !ok {
44
+ t.Fatalf("meta[target] = %#v, want map", meta["target"])
45
+ }
46
+ if got := stringFromAny(target["kind"]); got != "service" {
47
+ t.Fatalf("meta.target.kind = %q, want service", got)
48
+ }
49
+ }
50
+
51
+ func TestBuildGroupMessagesRPCParamsUsesLocalProfile(t *testing.T) {
52
+ t.Parallel()
53
+
54
+ record := &identity.StoredIdentity{DID: "did:wba:awiki.ai:user:alice:e1_alice"}
55
+ params, err := BuildGroupMessagesRPCParams(record, GroupMessagesRequest{Group: "did:wba:awiki.ai:groups:demo:e1_group", Limit: 25, Cursor: "12"})
56
+ if err != nil {
57
+ t.Fatalf("BuildGroupMessagesRPCParams() error = %v", err)
58
+ }
59
+ meta, _ := params["meta"].(map[string]any)
60
+ if got := stringFromAny(meta["profile"]); got != "anp.group.local.v1" {
61
+ t.Fatalf("meta.profile = %q, want anp.group.local.v1", got)
62
+ }
63
+ body, _ := params["body"].(map[string]any)
64
+ if got := stringFromAny(body["since_seq"]); got != "12" {
65
+ t.Fatalf("body.since_seq = %q, want 12", got)
66
+ }
67
+ }
@@ -0,0 +1,61 @@
1
+ package message
2
+
3
+ import "strings"
4
+
5
+ func stringFromAny(value any) string {
6
+ text, _ := value.(string)
7
+ return text
8
+ }
9
+
10
+ func boolFromAny(value any) bool {
11
+ switch typed := value.(type) {
12
+ case bool:
13
+ return typed
14
+ case int:
15
+ return typed != 0
16
+ case int64:
17
+ return typed != 0
18
+ case float64:
19
+ return typed != 0
20
+ case string:
21
+ return typed == "1" || strings.EqualFold(typed, "true")
22
+ default:
23
+ return false
24
+ }
25
+ }
26
+
27
+ func int64PtrFromAny(value any) *int64 {
28
+ switch typed := value.(type) {
29
+ case int64:
30
+ value := typed
31
+ return &value
32
+ case int:
33
+ value := int64(typed)
34
+ return &value
35
+ case float64:
36
+ value := int64(typed)
37
+ return &value
38
+ default:
39
+ return nil
40
+ }
41
+ }
42
+
43
+ func intValueFromAny(value any, fallback int) int {
44
+ switch typed := value.(type) {
45
+ case int:
46
+ return typed
47
+ case int64:
48
+ return int(typed)
49
+ case float64:
50
+ return int(typed)
51
+ default:
52
+ return fallback
53
+ }
54
+ }
55
+
56
+ func defaultString(value string, fallback string) string {
57
+ if strings.TrimSpace(value) == "" {
58
+ return fallback
59
+ }
60
+ return value
61
+ }