@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,63 @@
1
+ package anpsdk
2
+
3
+ import (
4
+ anp "github.com/agent-network-protocol/anp/golang"
5
+ anpauth "github.com/agent-network-protocol/anp/golang/authentication"
6
+ directe2ee "github.com/agent-network-protocol/anp/golang/direct_e2ee"
7
+ anpproof "github.com/agent-network-protocol/anp/golang/proof"
8
+ )
9
+
10
+ const (
11
+ ModulePath = "github.com/agent-network-protocol/anp/golang"
12
+ ModuleVersion = "v0.7.2"
13
+ )
14
+
15
+ type (
16
+ KeyType = anp.KeyType
17
+ PrivateKeyMaterial = anp.PrivateKeyMaterial
18
+ PublicKeyMaterial = anp.PublicKeyMaterial
19
+ GeneratedKeyPairPEM = anp.GeneratedKeyPairPEM
20
+ DidDocumentBundle = anpauth.DidDocumentBundle
21
+ DidDocumentOptions = anpauth.DidDocumentOptions
22
+ DIDWbaAuthHeader = anpauth.DIDWbaAuthHeader
23
+ DidWbaVerifierConfig = anpauth.DidWbaVerifierConfig
24
+ AuthMode = anpauth.AuthMode
25
+ HttpSignatureOptions = anpauth.HttpSignatureOptions
26
+ MessageServiceE2EEClient = directe2ee.MessageServiceDirectE2eeClient
27
+ PrekeyBundle = directe2ee.PrekeyBundle
28
+ DirectSessionState = directe2ee.DirectSessionState
29
+ IMProof = anpproof.IMProof
30
+ IMGenerationOptions = anpproof.IMGenerationOptions
31
+ ParsedIMSignatureInput = anpproof.ParsedIMSignatureInput
32
+ )
33
+
34
+ var (
35
+ KeyTypeSecp256k1 = anp.KeyTypeSecp256k1
36
+ KeyTypeSecp256r1 = anp.KeyTypeSecp256r1
37
+ KeyTypeEd25519 = anp.KeyTypeEd25519
38
+ KeyTypeX25519 = anp.KeyTypeX25519
39
+ AuthModeHTTPSignatures = anpauth.AuthModeHTTPSignatures
40
+ AuthModeLegacyDidWba = anpauth.AuthModeLegacyDidWba
41
+ AuthModeAuto = anpauth.AuthModeAuto
42
+ GenerateKeyPairPEM = anp.GenerateKeyPairPEM
43
+ PrivateKeyFromPEM = anp.PrivateKeyFromPEM
44
+ PublicKeyFromPEM = anp.PublicKeyFromPEM
45
+ CreateDidWBADocument = anpauth.CreateDidWBADocument
46
+ CreateDidWBADocumentWithKeyBinding = anpauth.CreateDidWBADocumentWithKeyBinding
47
+ ResolveDidDocument = anpauth.ResolveDidDocument
48
+ ResolveDidDocumentWithOptions = anpauth.ResolveDidDocumentWithOptions
49
+ GenerateAuthHeader = anpauth.GenerateAuthHeader
50
+ GenerateHTTPSignatureHeaders = anpauth.GenerateHTTPSignatureHeaders
51
+ NewDIDWbaAuthHeader = anpauth.NewDIDWbaAuthHeader
52
+ NewDidWbaVerifier = anpauth.NewDidWbaVerifier
53
+ NewFileSessionStore = directe2ee.NewFileSessionStore
54
+ NewFileSignedPrekeyStore = directe2ee.NewFileSignedPrekeyStore
55
+ NewFilePendingOutboundStore = directe2ee.NewFilePendingOutboundStore
56
+ NewMessageServiceDirectE2eeClient = directe2ee.NewMessageServiceDirectE2eeClient
57
+ BuildIMContentDigest = anpproof.BuildIMContentDigest
58
+ BuildIMSignatureInput = anpproof.BuildIMSignatureInput
59
+ ParseIMSignatureInput = anpproof.ParseIMSignatureInput
60
+ EncodeIMSignature = anpproof.EncodeIMSignature
61
+ GenerateIMProof = anpproof.GenerateIMProof
62
+ VerifyIMProofWithDocument = anpproof.VerifyIMProofWithDocument
63
+ )
@@ -0,0 +1,351 @@
1
+ package authsdk
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/asn1"
7
+ "encoding/json"
8
+ "encoding/pem"
9
+ "fmt"
10
+ "io"
11
+ "net/http"
12
+ "os"
13
+ "path/filepath"
14
+ "strings"
15
+
16
+ "github.com/agentconnect/awiki-cli/internal/anpsdk"
17
+ )
18
+
19
+ type Session struct {
20
+ helper *anpsdk.DIDWbaAuthHeader
21
+ identityName string
22
+ did string
23
+ jwtToken string
24
+ persistToken func(string) error
25
+ }
26
+
27
+ func NewSession(didDocumentPath string, privateKeyPath string, identityName string, did string, jwtToken string, persistToken func(string) error) *Session {
28
+ normalizedKeyPath := privateKeyPath
29
+ if rewrittenPath, err := normalizeSecp256k1PrivateKeyPath(privateKeyPath); err == nil && strings.TrimSpace(rewrittenPath) != "" {
30
+ normalizedKeyPath = rewrittenPath
31
+ }
32
+ helper := anpsdk.NewDIDWbaAuthHeader(didDocumentPath, normalizedKeyPath, anpsdk.AuthModeHTTPSignatures)
33
+ session := &Session{
34
+ helper: helper,
35
+ identityName: identityName,
36
+ did: did,
37
+ jwtToken: jwtToken,
38
+ persistToken: persistToken,
39
+ }
40
+ if strings.TrimSpace(jwtToken) != "" {
41
+ headers := map[string]string{"Authorization": "Bearer " + jwtToken}
42
+ helper.UpdateToken("https://awiki.ai", headers)
43
+ }
44
+ return session
45
+ }
46
+
47
+ func (s *Session) SetBearer(serverURL string, token string) {
48
+ if s == nil || s.helper == nil || strings.TrimSpace(token) == "" {
49
+ return
50
+ }
51
+ s.helper.UpdateToken(serverURL, map[string]string{"Authorization": "Bearer " + token})
52
+ s.jwtToken = token
53
+ }
54
+
55
+ func (s *Session) CurrentJWT() string {
56
+ if s == nil {
57
+ return ""
58
+ }
59
+ return s.jwtToken
60
+ }
61
+
62
+ func (s *Session) Headers(serverURL string, method string, body []byte, forceNew bool) (map[string]string, error) {
63
+ if s == nil {
64
+ return nil, fmt.Errorf("auth session is not configured")
65
+ }
66
+ if strings.TrimSpace(s.jwtToken) != "" && !forceNew {
67
+ return map[string]string{
68
+ "Content-Type": "application/json",
69
+ "Authorization": "Bearer " + s.jwtToken,
70
+ }, nil
71
+ }
72
+ if s.helper == nil {
73
+ return nil, fmt.Errorf("auth session is not configured")
74
+ }
75
+ headers := map[string]string{"Content-Type": "application/json"}
76
+ return s.helper.GetAuthHeader(serverURL, forceNew, method, headers, body)
77
+ }
78
+
79
+ func (s *Session) ShouldRetryAfter401(headers http.Header) bool {
80
+ if s == nil || s.helper == nil {
81
+ return false
82
+ }
83
+ return s.helper.ShouldRetryAfter401(flattenHeaders(headers))
84
+ }
85
+
86
+ func (s *Session) ChallengeHeaders(serverURL string, headers http.Header, method string, body []byte) (map[string]string, error) {
87
+ if s == nil || s.helper == nil {
88
+ return nil, fmt.Errorf("auth session is not configured")
89
+ }
90
+ baseHeaders := map[string]string{"Content-Type": "application/json"}
91
+ return s.helper.GetChallengeAuthHeader(serverURL, flattenHeaders(headers), method, baseHeaders, body)
92
+ }
93
+
94
+ func (s *Session) ClearToken(serverURL string) {
95
+ if s == nil || s.helper == nil {
96
+ return
97
+ }
98
+ s.helper.ClearToken(serverURL)
99
+ s.jwtToken = ""
100
+ }
101
+
102
+ func (s *Session) CaptureToken(serverURL string, headers http.Header) string {
103
+ if s == nil || s.helper == nil {
104
+ return ""
105
+ }
106
+ token := s.helper.UpdateToken(serverURL, flattenHeaders(headers))
107
+ if token == "" {
108
+ return ""
109
+ }
110
+ s.jwtToken = token
111
+ if s.persistToken != nil {
112
+ _ = s.persistToken(token)
113
+ }
114
+ return token
115
+ }
116
+
117
+ func (s *Session) DoJSONRPC(ctx context.Context, client *http.Client, requestURL string, method string, rpcMethod string, params any, out any) error {
118
+ if client == nil {
119
+ client = http.DefaultClient
120
+ }
121
+ payload := map[string]any{"jsonrpc": "2.0", "id": "req-1", "method": rpcMethod, "params": params}
122
+ body, err := json.Marshal(payload)
123
+ if err != nil {
124
+ return err
125
+ }
126
+ response, err := s.doRequest(ctx, client, requestURL, method, body)
127
+ if err != nil {
128
+ return err
129
+ }
130
+ defer response.Body.Close()
131
+ raw, err := io.ReadAll(response.Body)
132
+ if err != nil {
133
+ return err
134
+ }
135
+ var decoded struct {
136
+ Result json.RawMessage `json:"result"`
137
+ Error *struct {
138
+ Code int `json:"code"`
139
+ Message string `json:"message"`
140
+ Data any `json:"data,omitempty"`
141
+ } `json:"error,omitempty"`
142
+ }
143
+ if err := json.Unmarshal(raw, &decoded); err != nil {
144
+ return err
145
+ }
146
+ if decoded.Error != nil {
147
+ return &RPCError{Code: decoded.Error.Code, Message: decoded.Error.Message, Data: decoded.Error.Data}
148
+ }
149
+ if out == nil {
150
+ return nil
151
+ }
152
+ return json.Unmarshal(decoded.Result, out)
153
+ }
154
+
155
+ func (s *Session) EnsureJWT(ctx context.Context, client *http.Client, requestURL string) (string, error) {
156
+ var result map[string]any
157
+ if err := s.DoJSONRPC(ctx, client, requestURL, http.MethodPost, "get_me", map[string]any{}, &result); err != nil {
158
+ return "", err
159
+ }
160
+ return s.jwtToken, nil
161
+ }
162
+
163
+ func (s *Session) DoJSON(ctx context.Context, client *http.Client, method string, requestURL string, payload any, out any) error {
164
+ if client == nil {
165
+ client = http.DefaultClient
166
+ }
167
+ body, err := json.Marshal(payload)
168
+ if err != nil {
169
+ return err
170
+ }
171
+ response, err := s.doRequest(ctx, client, requestURL, method, body)
172
+ if err != nil {
173
+ return err
174
+ }
175
+ defer response.Body.Close()
176
+ if out == nil {
177
+ _, _ = io.Copy(io.Discard, response.Body)
178
+ return nil
179
+ }
180
+ return json.NewDecoder(response.Body).Decode(out)
181
+ }
182
+
183
+ func (s *Session) doRequest(ctx context.Context, client *http.Client, requestURL string, method string, body []byte) (*http.Response, error) {
184
+ headers, err := s.Headers(requestURL, method, body, false)
185
+ if err != nil {
186
+ return nil, err
187
+ }
188
+ response, err := doHTTPRequest(ctx, client, method, requestURL, body, headers)
189
+ if err != nil {
190
+ return nil, err
191
+ }
192
+ if response.StatusCode == http.StatusUnauthorized {
193
+ defer response.Body.Close()
194
+ if s.ShouldRetryAfter401(response.Header) {
195
+ headers, err = s.ChallengeHeaders(requestURL, response.Header, method, body)
196
+ if err != nil {
197
+ return nil, err
198
+ }
199
+ } else if s.helper != nil {
200
+ s.ClearToken(requestURL)
201
+ headers, err = s.Headers(requestURL, method, body, true)
202
+ if err != nil {
203
+ return nil, err
204
+ }
205
+ } else {
206
+ raw, _ := io.ReadAll(response.Body)
207
+ return nil, &HTTPError{StatusCode: response.StatusCode, Message: strings.TrimSpace(string(raw))}
208
+ }
209
+ response, err = doHTTPRequest(ctx, client, method, requestURL, body, headers)
210
+ if err != nil {
211
+ return nil, err
212
+ }
213
+ }
214
+ if response.StatusCode >= 400 {
215
+ defer response.Body.Close()
216
+ raw, _ := io.ReadAll(response.Body)
217
+ return nil, &HTTPError{StatusCode: response.StatusCode, Message: strings.TrimSpace(string(raw))}
218
+ }
219
+ s.CaptureToken(requestURL, response.Header)
220
+ return response, nil
221
+ }
222
+
223
+ func doHTTPRequest(ctx context.Context, client *http.Client, method string, requestURL string, body []byte, headers map[string]string) (*http.Response, error) {
224
+ request, err := http.NewRequestWithContext(ctx, method, requestURL, bytes.NewReader(body))
225
+ if err != nil {
226
+ return nil, err
227
+ }
228
+ for key, value := range headers {
229
+ request.Header.Set(key, value)
230
+ }
231
+ return client.Do(request)
232
+ }
233
+
234
+ func flattenHeaders(headers http.Header) map[string]string {
235
+ values := make(map[string]string, len(headers))
236
+ for key, item := range headers {
237
+ if len(item) == 0 {
238
+ continue
239
+ }
240
+ values[key] = item[0]
241
+ }
242
+ return values
243
+ }
244
+
245
+ type pkcs8PrivateKeyInfo struct {
246
+ Version int
247
+ Algorithm pkixAlgorithmIdentifier
248
+ PrivateKey []byte
249
+ }
250
+
251
+ type pkixAlgorithmIdentifier struct {
252
+ Algorithm asn1.ObjectIdentifier
253
+ Parameters asn1.RawValue `asn1:"optional"`
254
+ }
255
+
256
+ type sec1ECPrivateKey struct {
257
+ Version int
258
+ PrivateKey []byte
259
+ }
260
+
261
+ func normalizeSecp256k1PrivateKeyPath(privateKeyPath string) (string, error) {
262
+ privateKeyPath = strings.TrimSpace(privateKeyPath)
263
+ if privateKeyPath == "" {
264
+ return "", nil
265
+ }
266
+ raw, err := os.ReadFile(filepath.Clean(privateKeyPath))
267
+ if err != nil {
268
+ return "", err
269
+ }
270
+ normalizedPEM, _, err := NormalizeSecp256k1PrivatePEM(string(raw))
271
+ if err != nil {
272
+ return "", err
273
+ }
274
+ if strings.TrimSpace(normalizedPEM) == strings.TrimSpace(string(raw)) {
275
+ return privateKeyPath, nil
276
+ }
277
+ dir := filepath.Dir(privateKeyPath)
278
+ target := filepath.Join(dir, ".awiki-cli-key-1-private-normalized.pem")
279
+ if writeErr := os.WriteFile(target, []byte(normalizedPEM), 0o600); writeErr != nil {
280
+ return "", writeErr
281
+ }
282
+ return target, nil
283
+ }
284
+
285
+ func NormalizeSecp256k1PrivatePEM(pemText string) (string, []byte, error) {
286
+ block, _ := pem.Decode([]byte(strings.TrimSpace(pemText)))
287
+ if block == nil {
288
+ return "", nil, fmt.Errorf("invalid key-1 private key pem")
289
+ }
290
+ switch block.Type {
291
+ case "ANP SECP256K1 PRIVATE KEY":
292
+ return strings.TrimSpace(pemText), append([]byte(nil), block.Bytes...), nil
293
+ case "PRIVATE KEY":
294
+ scalar, err := extractSecp256k1ScalarFromPKCS8(block.Bytes)
295
+ if err != nil {
296
+ return "", nil, err
297
+ }
298
+ normalized := pem.EncodeToMemory(&pem.Block{Type: "ANP SECP256K1 PRIVATE KEY", Bytes: scalar})
299
+ return string(normalized), scalar, nil
300
+ default:
301
+ return "", nil, fmt.Errorf("invalid key-1 private key label: %s", block.Type)
302
+ }
303
+ }
304
+
305
+ func extractSecp256k1ScalarFromPKCS8(der []byte) ([]byte, error) {
306
+ var info pkcs8PrivateKeyInfo
307
+ if _, err := asn1.Unmarshal(der, &info); err != nil {
308
+ return nil, fmt.Errorf("parse pkcs8 private key: %w", err)
309
+ }
310
+ var sec1 sec1ECPrivateKey
311
+ if _, err := asn1.Unmarshal(info.PrivateKey, &sec1); err != nil {
312
+ return nil, fmt.Errorf("parse embedded sec1 private key: %w", err)
313
+ }
314
+ if len(sec1.PrivateKey) == 0 {
315
+ return nil, fmt.Errorf("embedded sec1 private key is empty")
316
+ }
317
+ if len(sec1.PrivateKey) > 32 {
318
+ sec1.PrivateKey = sec1.PrivateKey[len(sec1.PrivateKey)-32:]
319
+ }
320
+ if len(sec1.PrivateKey) < 32 {
321
+ padded := make([]byte, 32)
322
+ copy(padded[32-len(sec1.PrivateKey):], sec1.PrivateKey)
323
+ sec1.PrivateKey = padded
324
+ }
325
+ return append([]byte(nil), sec1.PrivateKey...), nil
326
+ }
327
+
328
+ type HTTPError struct {
329
+ StatusCode int
330
+ Message string
331
+ }
332
+
333
+ func (e *HTTPError) Error() string {
334
+ if e == nil {
335
+ return ""
336
+ }
337
+ return fmt.Sprintf("http error %d: %s", e.StatusCode, e.Message)
338
+ }
339
+
340
+ type RPCError struct {
341
+ Code int
342
+ Message string
343
+ Data any
344
+ }
345
+
346
+ func (e *RPCError) Error() string {
347
+ if e == nil {
348
+ return ""
349
+ }
350
+ return fmt.Sprintf("rpc error %d: %s", e.Code, e.Message)
351
+ }
@@ -0,0 +1,34 @@
1
+ package buildinfo
2
+
3
+ import "runtime"
4
+
5
+ var (
6
+ Version = "dev"
7
+ Commit = "unknown"
8
+ BuildDate = "unknown"
9
+ CGOEnabled = "unknown"
10
+ )
11
+
12
+ type Info struct {
13
+ Version string `json:"version"`
14
+ Commit string `json:"commit"`
15
+ BuildDate string `json:"build_date"`
16
+ GoVersion string `json:"go_version"`
17
+ GOOS string `json:"goos"`
18
+ GOARCH string `json:"goarch"`
19
+ Compiler string `json:"compiler"`
20
+ CGOEnabled string `json:"cgo_enabled"`
21
+ }
22
+
23
+ func Current() Info {
24
+ return Info{
25
+ Version: Version,
26
+ Commit: Commit,
27
+ BuildDate: BuildDate,
28
+ GoVersion: runtime.Version(),
29
+ GOOS: runtime.GOOS,
30
+ GOARCH: runtime.GOARCH,
31
+ Compiler: runtime.Compiler,
32
+ CGOEnabled: CGOEnabled,
33
+ }
34
+ }
@@ -0,0 +1,136 @@
1
+ package cli
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "os"
7
+ "strings"
8
+
9
+ "github.com/agentconnect/awiki-cli/internal/buildinfo"
10
+ "github.com/agentconnect/awiki-cli/internal/cmdmeta"
11
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
12
+ docindex "github.com/agentconnect/awiki-cli/internal/docs"
13
+ "github.com/agentconnect/awiki-cli/internal/identity"
14
+ "github.com/agentconnect/awiki-cli/internal/output"
15
+ )
16
+
17
+ type GlobalOptions struct {
18
+ Format string
19
+ FormatChanged bool
20
+ JQ string
21
+ DryRun bool
22
+ Identity string
23
+ IdentityChanged bool
24
+ Verbose bool
25
+ }
26
+
27
+ type App struct {
28
+ globals GlobalOptions
29
+ catalog *cmdmeta.Catalog
30
+ docs *docindex.Index
31
+
32
+ updateWarning string
33
+ }
34
+
35
+ func Execute() int {
36
+ app := &App{
37
+ globals: GlobalOptions{Format: string(output.FormatJSON)},
38
+ catalog: cmdmeta.NewCatalog(),
39
+ docs: docindex.NewIndex(),
40
+ }
41
+ rootCmd := newRootCommand(app)
42
+ if err := rootCmd.Execute(); err != nil {
43
+ return app.handleError(err)
44
+ }
45
+ return 0
46
+ }
47
+
48
+ func (a *App) handleError(err error) int {
49
+ format := output.FormatJSON
50
+ if resolved, resolveErr := output.NormalizeFormat(a.globals.Format); resolveErr == nil {
51
+ format = resolved
52
+ }
53
+ detail := output.ErrorDetail{
54
+ Code: "internal_error",
55
+ Message: err.Error(),
56
+ Retryable: false,
57
+ }
58
+ exitCode := 1
59
+ var exitErr *output.ExitError
60
+ if errors.As(err, &exitErr) {
61
+ detail = exitErr.Detail
62
+ exitCode = exitErr.Code
63
+ }
64
+ detail.Details = identity.PublicValue(detail.Details)
65
+ envelope := output.ErrorEnvelope{
66
+ OK: false,
67
+ Error: detail,
68
+ Meta: output.Meta{
69
+ Version: buildinfo.Version,
70
+ DryRun: a.globals.DryRun,
71
+ Format: string(format),
72
+ },
73
+ }
74
+ if identity := a.identityMeta(); identity != nil {
75
+ envelope.Meta.Identity = identity
76
+ }
77
+ if renderErr := output.RenderError(os.Stderr, format, a.globals.JQ, envelope); renderErr != nil {
78
+ fmt.Fprintln(os.Stderr, err.Error())
79
+ }
80
+ return exitCode
81
+ }
82
+
83
+ func (a *App) renderSuccess(command string, format output.Format, jqExpr string, data any, summary string, warnings []string, identityMeta *output.IdentityMeta) error {
84
+ mergedWarnings := warnings
85
+ if a.updateWarning != "" {
86
+ mergedWarnings = append([]string{a.updateWarning}, warnings...)
87
+ }
88
+ envelope := output.SuccessEnvelope{
89
+ OK: true,
90
+ Command: command,
91
+ Data: identity.PublicValue(data),
92
+ Warnings: mergedWarnings,
93
+ Summary: summary,
94
+ Meta: output.Meta{
95
+ Version: buildinfo.Version,
96
+ Identity: identityMeta,
97
+ DryRun: a.globals.DryRun,
98
+ Format: string(format),
99
+ },
100
+ }
101
+ return output.RenderSuccess(os.Stdout, format, jqExpr, envelope)
102
+ }
103
+
104
+ func (a *App) resolveConfig() (*appconfig.Resolved, error) {
105
+ resolved, err := appconfig.Resolve(appconfig.Overrides{
106
+ Identity: a.globals.Identity,
107
+ IdentityChanged: a.globals.IdentityChanged,
108
+ Format: a.globals.Format,
109
+ FormatChanged: a.globals.FormatChanged,
110
+ })
111
+ if err != nil {
112
+ return nil, err
113
+ }
114
+ if strings.TrimSpace(resolved.ActiveIdentity) == "" {
115
+ manager := identity.NewManager(resolved.Paths)
116
+ current, currentErr := manager.Current()
117
+ if currentErr == nil && current != nil {
118
+ resolved.ActiveIdentity = current.IdentityName
119
+ if resolved.Sources == nil {
120
+ resolved.Sources = map[string]appconfig.ValueSource{}
121
+ }
122
+ resolved.Sources["active_identity"] = appconfig.ValueSource{
123
+ Source: "identity_index",
124
+ Value: current.IdentityName,
125
+ }
126
+ }
127
+ }
128
+ return resolved, nil
129
+ }
130
+
131
+ func (a *App) identityMeta() *output.IdentityMeta {
132
+ if a.globals.Identity == "" {
133
+ return nil
134
+ }
135
+ return &output.IdentityMeta{Name: a.globals.Identity}
136
+ }
@@ -0,0 +1,88 @@
1
+ package cli
2
+
3
+ import (
4
+ "errors"
5
+ "io"
6
+ "os"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/agentconnect/awiki-cli/internal/identity"
11
+ "github.com/agentconnect/awiki-cli/internal/output"
12
+ )
13
+
14
+ func TestRenderSuccessSanitizesInternalIdentityFields(t *testing.T) {
15
+ app := &App{}
16
+ rendered, err := captureStdout(func() error {
17
+ return app.renderSuccess(
18
+ "awiki-cli config show",
19
+ output.FormatJSON,
20
+ "",
21
+ map[string]any{
22
+ "default_identity": map[string]any{
23
+ "handle": "alice",
24
+ "user_id": "user-123",
25
+ },
26
+ },
27
+ "Resolved configuration",
28
+ nil,
29
+ nil,
30
+ )
31
+ })
32
+ if err != nil {
33
+ t.Fatalf("captureStdout(renderSuccess) error = %v", err)
34
+ }
35
+ if strings.Contains(rendered, "user_id") || strings.Contains(rendered, "user-123") {
36
+ t.Fatalf("rendered output %q still contains internal user_id fields", rendered)
37
+ }
38
+ if !strings.Contains(rendered, "alice") {
39
+ t.Fatalf("rendered output %q lost public fields", rendered)
40
+ }
41
+ }
42
+
43
+ func TestIdentityGatingUsesFrozenErrorCode(t *testing.T) {
44
+ t.Parallel()
45
+
46
+ app := &App{}
47
+ err := identity.UserRegistrationError("alice", identity.UserState{
48
+ RegistrationState: "local_identity",
49
+ ReadyForMessaging: false,
50
+ Missing: []string{"registration", "handle"},
51
+ })
52
+
53
+ for _, got := range []error{
54
+ app.messageExit(err, "hint"),
55
+ app.runtimeExit(err, "hint"),
56
+ } {
57
+ var exitErr *output.ExitError
58
+ if !errors.As(got, &exitErr) {
59
+ t.Fatalf("errors.As(%T, *output.ExitError) = false", got)
60
+ }
61
+ if exitErr.Detail.Code != "identity_required" {
62
+ t.Fatalf("exitErr.Detail.Code = %q, want %q", exitErr.Detail.Code, "identity_required")
63
+ }
64
+ if exitErr.Code != 3 {
65
+ t.Fatalf("exitErr.Code = %d, want 3", exitErr.Code)
66
+ }
67
+ }
68
+ }
69
+
70
+ func captureStdout(run func() error) (string, error) {
71
+ reader, writer, err := os.Pipe()
72
+ if err != nil {
73
+ return "", err
74
+ }
75
+ defer reader.Close()
76
+
77
+ originalStdout := os.Stdout
78
+ os.Stdout = writer
79
+ runErr := run()
80
+ _ = writer.Close()
81
+ os.Stdout = originalStdout
82
+
83
+ output, readErr := io.ReadAll(reader)
84
+ if readErr != nil {
85
+ return "", readErr
86
+ }
87
+ return string(output), runErr
88
+ }