@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.
- package/.github/workflows/release.yml +44 -0
- package/.goreleaser.yml +44 -0
- package/AGENTS.md +60 -0
- package/CLAUDE.md +192 -0
- package/README.md +2 -0
- package/docs/architecture/awiki-command-v2.md +955 -0
- package/docs/architecture/awiki-skill-architecture.md +475 -0
- package/docs/architecture/awiki-v2-architecture.md +1063 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/cli-init.md +1008 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/output-format.md +407 -0
- package/docs/architecture//345/217/202/350/200/203/346/226/207/346/241/243/overall-init.md +741 -0
- package/docs/harness/review-spec.md +474 -0
- package/docs/installation.md +372 -0
- package/docs/plan/awiki-v2-implementation-plan.md +903 -0
- package/docs/plan/phase-0/adr-index.md +56 -0
- package/docs/plan/phase-0/audit-findings.md +251 -0
- package/docs/plan/phase-0/capability-mapping.md +108 -0
- package/docs/plan/phase-0/implementation-constraints.md +363 -0
- package/docs/publish.md +169 -0
- package/go.mod +29 -0
- package/go.sum +73 -0
- package/internal/anpsdk/registry.go +63 -0
- package/internal/authsdk/session.go +351 -0
- package/internal/buildinfo/buildinfo.go +34 -0
- package/internal/cli/app.go +136 -0
- package/internal/cli/app_test.go +88 -0
- package/internal/cli/debug.go +104 -0
- package/internal/cli/group.go +263 -0
- package/internal/cli/id.go +473 -0
- package/internal/cli/init.go +134 -0
- package/internal/cli/msg.go +228 -0
- package/internal/cli/page.go +267 -0
- package/internal/cli/root.go +499 -0
- package/internal/cli/runtime.go +232 -0
- package/internal/cli/upgrade.go +60 -0
- package/internal/cmdmeta/catalog.go +203 -0
- package/internal/cmdmeta/catalog_test.go +21 -0
- package/internal/config/config.go +399 -0
- package/internal/config/config_test.go +104 -0
- package/internal/config/write.go +37 -0
- package/internal/content/service.go +314 -0
- package/internal/content/service_test.go +165 -0
- package/internal/content/types.go +44 -0
- package/internal/docs/topics.go +110 -0
- package/internal/doctor/doctor.go +306 -0
- package/internal/identity/client.go +267 -0
- package/internal/identity/did.go +85 -0
- package/internal/identity/did_test.go +50 -0
- package/internal/identity/layout.go +206 -0
- package/internal/identity/legacy.go +378 -0
- package/internal/identity/public.go +70 -0
- package/internal/identity/public_test.go +73 -0
- package/internal/identity/readiness.go +74 -0
- package/internal/identity/service.go +826 -0
- package/internal/identity/store.go +385 -0
- package/internal/identity/store_test.go +180 -0
- package/internal/identity/types.go +204 -0
- package/internal/message/auth.go +167 -0
- package/internal/message/group_service.go +838 -0
- package/internal/message/group_wire.go +350 -0
- package/internal/message/group_wire_test.go +67 -0
- package/internal/message/helpers.go +61 -0
- package/internal/message/http_client.go +334 -0
- package/internal/message/proof.go +156 -0
- package/internal/message/proof_test.go +61 -0
- package/internal/message/service.go +696 -0
- package/internal/message/service_test.go +97 -0
- package/internal/message/types.go +155 -0
- package/internal/message/wire.go +100 -0
- package/internal/message/wire_test.go +49 -0
- package/internal/message/ws_proxy_client.go +151 -0
- package/internal/output/output.go +350 -0
- package/internal/output/output_test.go +48 -0
- package/internal/runtime/config.go +117 -0
- package/internal/runtime/config_test.go +46 -0
- package/internal/runtime/listener/files.go +65 -0
- package/internal/runtime/listener/manager.go +142 -0
- package/internal/runtime/listener/server.go +983 -0
- package/internal/runtime/listener/server_test.go +319 -0
- package/internal/runtime/listener/sysproc_unix.go +17 -0
- package/internal/runtime/listener/sysproc_windows.go +13 -0
- package/internal/runtime/listener/types.go +21 -0
- package/internal/runtime/listener/wsclient.go +299 -0
- package/internal/runtime/listener/wsclient_test.go +41 -0
- package/internal/store/dao.go +632 -0
- package/internal/store/dao_test.go +87 -0
- package/internal/store/helpers.go +197 -0
- package/internal/store/import.go +499 -0
- package/internal/store/import_test.go +103 -0
- package/internal/store/open.go +71 -0
- package/internal/store/query.go +151 -0
- package/internal/store/schema.go +277 -0
- package/internal/store/schema_test.go +56 -0
- package/internal/store/types.go +177 -0
- package/internal/update/update.go +368 -0
- package/package.json +17 -0
- package/scripts/install.js +171 -0
- package/scripts/release/release-prerelease.sh +86 -0
- package/scripts/release/tag-release.sh +66 -0
- package/scripts/release/withdraw-release.sh +78 -0
- package/scripts/run.js +69 -0
- package/skills/README.md +32 -0
- package/skills/awiki-bundle/SKILL.md +76 -0
- package/skills/awiki-debug/SKILL.md +80 -0
- package/skills/awiki-group/SKILL.md +111 -0
- package/skills/awiki-id/SKILL.md +123 -0
- package/skills/awiki-msg/SKILL.md +131 -0
- package/skills/awiki-page/SKILL.md +93 -0
- package/skills/awiki-people/SKILL.md +66 -0
- package/skills/awiki-runtime/SKILL.md +137 -0
- package/skills/awiki-shared/SKILL.md +124 -0
- package/skills/awiki-workflow-discovery/SKILL.md +93 -0
- package/skills/awiki-workflow-onboarding/SKILL.md +119 -0
- package/skills/manifests/skills.yaml +260 -0
- package/skills/templates/bundle-skill-template.md +42 -0
- package/skills/templates/debug-skill-template.md +44 -0
- package/skills/templates/domain-skill-template.md +56 -0
- package/skills/templates/shared-skill-template.md +46 -0
- package/skills/templates/workflow-skill-template.md +46 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
package store
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"database/sql"
|
|
6
|
+
"fmt"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"sort"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
appconfig "github.com/agentconnect/awiki-cli/internal/config"
|
|
12
|
+
"github.com/agentconnect/awiki-cli/internal/identity"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func ScanLegacyDatabase(ctx context.Context, paths appconfig.Paths) (*LegacyScan, error) {
|
|
16
|
+
legacyPath := strings.TrimSpace(paths.LegacyDataDir)
|
|
17
|
+
if strings.HasSuffix(strings.ToLower(legacyPath), ".db") {
|
|
18
|
+
legacyPath = legacyPath
|
|
19
|
+
} else {
|
|
20
|
+
legacyPath = filepath.Join(paths.LegacyDataDir, "database", "awiki.db")
|
|
21
|
+
}
|
|
22
|
+
scan := &LegacyScan{
|
|
23
|
+
Path: legacyPath,
|
|
24
|
+
}
|
|
25
|
+
db, err := OpenReadOnly(scan.Path)
|
|
26
|
+
if err != nil {
|
|
27
|
+
if strings.Contains(err.Error(), "no such file") || strings.Contains(err.Error(), "unable to open database file") {
|
|
28
|
+
return scan, nil
|
|
29
|
+
}
|
|
30
|
+
return nil, err
|
|
31
|
+
}
|
|
32
|
+
defer db.Close()
|
|
33
|
+
scan.Exists = true
|
|
34
|
+
version, err := schemaVersion(ctx, db)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
scan.SchemaVersion = version
|
|
39
|
+
rows, err := queryMaps(ctx, db, `SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return nil, err
|
|
42
|
+
}
|
|
43
|
+
for _, row := range rows {
|
|
44
|
+
if name, ok := row["name"].(string); ok {
|
|
45
|
+
scan.Tables = append(scan.Tables, name)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return scan, nil
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func ImportLegacyDatabase(ctx context.Context, targetDB *sql.DB, paths appconfig.Paths, manager *identity.Manager) (*ImportReport, error) {
|
|
52
|
+
scan, err := ScanLegacyDatabase(ctx, paths)
|
|
53
|
+
if err != nil {
|
|
54
|
+
return nil, err
|
|
55
|
+
}
|
|
56
|
+
if !scan.Exists {
|
|
57
|
+
return nil, ErrLegacyDatabaseNotFound
|
|
58
|
+
}
|
|
59
|
+
legacyDB, err := OpenReadOnly(scan.Path)
|
|
60
|
+
if err != nil {
|
|
61
|
+
return nil, err
|
|
62
|
+
}
|
|
63
|
+
defer legacyDB.Close()
|
|
64
|
+
if err := EnsureSchema(ctx, targetDB); err != nil {
|
|
65
|
+
return nil, err
|
|
66
|
+
}
|
|
67
|
+
report := &ImportReport{
|
|
68
|
+
SourcePath: scan.Path,
|
|
69
|
+
SourceSchemaVersion: scan.SchemaVersion,
|
|
70
|
+
ImportedRows: map[string]int{},
|
|
71
|
+
}
|
|
72
|
+
ownerByCredential := map[string]string{}
|
|
73
|
+
defaultOwner := ""
|
|
74
|
+
if manager != nil {
|
|
75
|
+
if identities, listErr := manager.List(); listErr == nil {
|
|
76
|
+
for _, summary := range identities {
|
|
77
|
+
ownerByCredential[summary.IdentityName] = summary.DID
|
|
78
|
+
if summary.IsDefault {
|
|
79
|
+
defaultOwner = summary.DID
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if defaultOwner == "" && len(identities) == 1 {
|
|
83
|
+
defaultOwner = identities[0].DID
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if scan.SchemaVersion > 0 && scan.SchemaVersion < 6 && defaultOwner == "" {
|
|
88
|
+
return nil, fmt.Errorf("%w: legacy schema < 6 requires at least one imported identity so owner_did can be inferred", ErrUnsupportedLegacySchema)
|
|
89
|
+
}
|
|
90
|
+
importers := []struct {
|
|
91
|
+
name string
|
|
92
|
+
fn func(context.Context, *sql.DB, *sql.DB, map[string]string, string) (int, error)
|
|
93
|
+
}{
|
|
94
|
+
{name: "messages", fn: importMessages},
|
|
95
|
+
{name: "e2ee_outbox", fn: importE2EEOutbox},
|
|
96
|
+
{name: "contacts", fn: importContacts},
|
|
97
|
+
{name: "groups", fn: importGroups},
|
|
98
|
+
{name: "group_members", fn: importGroupMembers},
|
|
99
|
+
{name: "relationship_events", fn: importRelationshipEvents},
|
|
100
|
+
{name: "e2ee_sessions", fn: importE2EESessions},
|
|
101
|
+
}
|
|
102
|
+
for _, importer := range importers {
|
|
103
|
+
count, importErr := importer.fn(ctx, legacyDB, targetDB, ownerByCredential, defaultOwner)
|
|
104
|
+
if importErr != nil {
|
|
105
|
+
if errorsIsMissingTable(importErr) {
|
|
106
|
+
report.SkippedTables = append(report.SkippedTables, importer.name)
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
return nil, fmt.Errorf("import %s: %w", importer.name, importErr)
|
|
110
|
+
}
|
|
111
|
+
report.ImportedRows[importer.name] = count
|
|
112
|
+
}
|
|
113
|
+
sort.Strings(report.SkippedTables)
|
|
114
|
+
return report, nil
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func inferOwnerDID(row map[string]any, ownerByCredential map[string]string, defaultOwner string) string {
|
|
118
|
+
if owner, ok := row["owner_did"].(string); ok && strings.TrimSpace(owner) != "" {
|
|
119
|
+
return owner
|
|
120
|
+
}
|
|
121
|
+
if credential, ok := row["credential_name"].(string); ok && credential != "" {
|
|
122
|
+
if owner, ok := ownerByCredential[credential]; ok {
|
|
123
|
+
return owner
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return defaultOwner
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func importMessages(ctx context.Context, src, dst *sql.DB, ownerByCredential map[string]string, defaultOwner string) (int, error) {
|
|
130
|
+
rows, err := queryMaps(ctx, src, `SELECT * FROM messages`)
|
|
131
|
+
if err != nil {
|
|
132
|
+
return 0, err
|
|
133
|
+
}
|
|
134
|
+
count := 0
|
|
135
|
+
for _, row := range rows {
|
|
136
|
+
msgID := stringFromAny(row["msg_id"])
|
|
137
|
+
if msgID == "" {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
threadID := stringFromAny(row["thread_id"])
|
|
141
|
+
if threadID == "" {
|
|
142
|
+
threadID = MakeThreadID(inferOwnerDID(row, ownerByCredential, defaultOwner), stringFromAny(row["sender_did"]), stringFromAny(row["group_id"]))
|
|
143
|
+
}
|
|
144
|
+
record := MessageRecord{
|
|
145
|
+
MsgID: msgID,
|
|
146
|
+
OwnerDID: inferOwnerDID(row, ownerByCredential, defaultOwner),
|
|
147
|
+
ThreadID: threadID,
|
|
148
|
+
Direction: intFromAny(row["direction"]),
|
|
149
|
+
SenderDID: stringFromAny(row["sender_did"]),
|
|
150
|
+
ReceiverDID: stringFromAny(row["receiver_did"]),
|
|
151
|
+
GroupID: stringFromAny(row["group_id"]),
|
|
152
|
+
GroupDID: stringFromAny(row["group_did"]),
|
|
153
|
+
ContentType: defaultString(stringFromAny(row["content_type"]), "text"),
|
|
154
|
+
Content: stringFromAny(row["content"]),
|
|
155
|
+
Title: stringFromAny(row["title"]),
|
|
156
|
+
ServerSeq: int64PtrFromAny(row["server_seq"]),
|
|
157
|
+
SentAt: stringFromAny(row["sent_at"]),
|
|
158
|
+
IsE2EE: boolFromAny(row["is_e2ee"]),
|
|
159
|
+
IsRead: boolFromAny(row["is_read"]),
|
|
160
|
+
SenderName: stringFromAny(row["sender_name"]),
|
|
161
|
+
Metadata: metadataFromAny(row["metadata"]),
|
|
162
|
+
CredentialName: stringFromAny(row["credential_name"]),
|
|
163
|
+
}
|
|
164
|
+
if err := StoreMessage(ctx, dst, record); err != nil {
|
|
165
|
+
return 0, err
|
|
166
|
+
}
|
|
167
|
+
count++
|
|
168
|
+
}
|
|
169
|
+
return count, nil
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func importE2EEOutbox(ctx context.Context, src, dst *sql.DB, ownerByCredential map[string]string, defaultOwner string) (int, error) {
|
|
173
|
+
rows, err := queryMaps(ctx, src, `SELECT * FROM e2ee_outbox`)
|
|
174
|
+
if err != nil {
|
|
175
|
+
return 0, err
|
|
176
|
+
}
|
|
177
|
+
count := 0
|
|
178
|
+
for _, row := range rows {
|
|
179
|
+
record := E2EEOutboxRecord{
|
|
180
|
+
OutboxID: stringFromAny(row["outbox_id"]),
|
|
181
|
+
OwnerDID: inferOwnerDID(row, ownerByCredential, defaultOwner),
|
|
182
|
+
PeerDID: stringFromAny(row["peer_did"]),
|
|
183
|
+
SessionID: stringFromAny(row["session_id"]),
|
|
184
|
+
OriginalType: defaultString(stringFromAny(row["original_type"]), "text"),
|
|
185
|
+
Plaintext: stringFromAny(row["plaintext"]),
|
|
186
|
+
LocalStatus: defaultString(stringFromAny(row["local_status"]), "queued"),
|
|
187
|
+
AttemptCount: intFromAny(row["attempt_count"]),
|
|
188
|
+
SentMsgID: stringFromAny(row["sent_msg_id"]),
|
|
189
|
+
SentServerSeq: int64PtrFromAny(row["sent_server_seq"]),
|
|
190
|
+
LastErrorCode: stringFromAny(row["last_error_code"]),
|
|
191
|
+
RetryHint: stringFromAny(row["retry_hint"]),
|
|
192
|
+
FailedMsgID: stringFromAny(row["failed_msg_id"]),
|
|
193
|
+
FailedServerSeq: int64PtrFromAny(row["failed_server_seq"]),
|
|
194
|
+
Metadata: metadataFromAny(row["metadata"]),
|
|
195
|
+
LastAttemptAt: stringFromAny(row["last_attempt_at"]),
|
|
196
|
+
CreatedAt: stringFromAny(row["created_at"]),
|
|
197
|
+
UpdatedAt: stringFromAny(row["updated_at"]),
|
|
198
|
+
CredentialName: stringFromAny(row["credential_name"]),
|
|
199
|
+
}
|
|
200
|
+
if _, err := QueueE2EEOutbox(ctx, dst, record); err != nil {
|
|
201
|
+
return 0, err
|
|
202
|
+
}
|
|
203
|
+
count++
|
|
204
|
+
}
|
|
205
|
+
return count, nil
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
func importContacts(ctx context.Context, src, dst *sql.DB, ownerByCredential map[string]string, defaultOwner string) (int, error) {
|
|
209
|
+
rows, err := queryMaps(ctx, src, `SELECT * FROM contacts`)
|
|
210
|
+
if err != nil {
|
|
211
|
+
return 0, err
|
|
212
|
+
}
|
|
213
|
+
count := 0
|
|
214
|
+
for _, row := range rows {
|
|
215
|
+
did := stringFromAny(row["did"])
|
|
216
|
+
if did == "" {
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
record := ContactRecord{
|
|
220
|
+
OwnerDID: inferOwnerDID(row, ownerByCredential, defaultOwner),
|
|
221
|
+
DID: did,
|
|
222
|
+
Name: stringFromAny(row["name"]),
|
|
223
|
+
Handle: stringFromAny(row["handle"]),
|
|
224
|
+
NickName: stringFromAny(row["nick_name"]),
|
|
225
|
+
Bio: stringFromAny(row["bio"]),
|
|
226
|
+
ProfileMD: stringFromAny(row["profile_md"]),
|
|
227
|
+
Tags: stringFromAny(row["tags"]),
|
|
228
|
+
Relationship: stringFromAny(row["relationship"]),
|
|
229
|
+
SourceType: stringFromAny(row["source_type"]),
|
|
230
|
+
SourceName: stringFromAny(row["source_name"]),
|
|
231
|
+
SourceGroupID: stringFromAny(row["source_group_id"]),
|
|
232
|
+
ConnectedAt: stringFromAny(row["connected_at"]),
|
|
233
|
+
RecommendedReason: stringFromAny(row["recommended_reason"]),
|
|
234
|
+
Followed: boolPtrFromAny(row["followed"]),
|
|
235
|
+
Messaged: boolPtrFromAny(row["messaged"]),
|
|
236
|
+
Note: stringFromAny(row["note"]),
|
|
237
|
+
FirstSeenAt: stringFromAny(row["first_seen_at"]),
|
|
238
|
+
LastSeenAt: stringFromAny(row["last_seen_at"]),
|
|
239
|
+
Metadata: metadataFromAny(row["metadata"]),
|
|
240
|
+
}
|
|
241
|
+
if err := UpsertContact(ctx, dst, record); err != nil {
|
|
242
|
+
return 0, err
|
|
243
|
+
}
|
|
244
|
+
count++
|
|
245
|
+
}
|
|
246
|
+
return count, nil
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func importGroups(ctx context.Context, src, dst *sql.DB, ownerByCredential map[string]string, defaultOwner string) (int, error) {
|
|
250
|
+
rows, err := queryMaps(ctx, src, `SELECT * FROM groups`)
|
|
251
|
+
if err != nil {
|
|
252
|
+
return 0, err
|
|
253
|
+
}
|
|
254
|
+
count := 0
|
|
255
|
+
for _, row := range rows {
|
|
256
|
+
groupID := stringFromAny(row["group_id"])
|
|
257
|
+
if groupID == "" {
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
record := GroupRecord{
|
|
261
|
+
OwnerDID: inferOwnerDID(row, ownerByCredential, defaultOwner),
|
|
262
|
+
GroupID: groupID,
|
|
263
|
+
GroupDID: stringFromAny(row["group_did"]),
|
|
264
|
+
Name: stringFromAny(row["name"]),
|
|
265
|
+
GroupMode: defaultString(stringFromAny(row["group_mode"]), "general"),
|
|
266
|
+
Slug: stringFromAny(row["slug"]),
|
|
267
|
+
Description: stringFromAny(row["description"]),
|
|
268
|
+
Goal: stringFromAny(row["goal"]),
|
|
269
|
+
Rules: stringFromAny(row["rules"]),
|
|
270
|
+
MessagePrompt: stringFromAny(row["message_prompt"]),
|
|
271
|
+
DocURL: stringFromAny(row["doc_url"]),
|
|
272
|
+
GroupOwnerDID: stringFromAny(row["group_owner_did"]),
|
|
273
|
+
GroupOwnerHandle: stringFromAny(row["group_owner_handle"]),
|
|
274
|
+
MyRole: stringFromAny(row["my_role"]),
|
|
275
|
+
MembershipStatus: defaultString(stringFromAny(row["membership_status"]), "active"),
|
|
276
|
+
JoinEnabled: boolPtrFromAny(row["join_enabled"]),
|
|
277
|
+
JoinCode: stringFromAny(row["join_code"]),
|
|
278
|
+
JoinCodeExpiresAt: stringFromAny(row["join_code_expires_at"]),
|
|
279
|
+
MemberCount: int64PtrFromAny(row["member_count"]),
|
|
280
|
+
LastSyncedSeq: int64PtrFromAny(row["last_synced_seq"]),
|
|
281
|
+
LastReadSeq: int64PtrFromAny(row["last_read_seq"]),
|
|
282
|
+
LastMessageAt: stringFromAny(row["last_message_at"]),
|
|
283
|
+
RemoteCreatedAt: stringFromAny(row["remote_created_at"]),
|
|
284
|
+
RemoteUpdatedAt: stringFromAny(row["remote_updated_at"]),
|
|
285
|
+
Metadata: metadataFromAny(row["metadata"]),
|
|
286
|
+
CredentialName: stringFromAny(row["credential_name"]),
|
|
287
|
+
}
|
|
288
|
+
if err := UpsertGroup(ctx, dst, record); err != nil {
|
|
289
|
+
return 0, err
|
|
290
|
+
}
|
|
291
|
+
count++
|
|
292
|
+
}
|
|
293
|
+
return count, nil
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
func importGroupMembers(ctx context.Context, src, dst *sql.DB, ownerByCredential map[string]string, defaultOwner string) (int, error) {
|
|
297
|
+
rows, err := queryMaps(ctx, src, `SELECT * FROM group_members`)
|
|
298
|
+
if err != nil {
|
|
299
|
+
return 0, err
|
|
300
|
+
}
|
|
301
|
+
count := 0
|
|
302
|
+
for _, row := range rows {
|
|
303
|
+
record := GroupMemberRecord{
|
|
304
|
+
OwnerDID: inferOwnerDID(row, ownerByCredential, defaultOwner),
|
|
305
|
+
GroupID: stringFromAny(row["group_id"]),
|
|
306
|
+
UserID: stringFromAny(row["user_id"]),
|
|
307
|
+
MemberDID: stringFromAny(row["member_did"]),
|
|
308
|
+
MemberHandle: stringFromAny(row["member_handle"]),
|
|
309
|
+
ProfileURL: stringFromAny(row["profile_url"]),
|
|
310
|
+
Role: stringFromAny(row["role"]),
|
|
311
|
+
Status: defaultString(stringFromAny(row["status"]), "active"),
|
|
312
|
+
JoinedAt: stringFromAny(row["joined_at"]),
|
|
313
|
+
SentMessageCount: int64PtrFromAny(row["sent_message_count"]),
|
|
314
|
+
Metadata: metadataFromAny(row["metadata"]),
|
|
315
|
+
CredentialName: stringFromAny(row["credential_name"]),
|
|
316
|
+
}
|
|
317
|
+
if record.GroupID == "" || record.UserID == "" {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
if err := UpsertGroupMember(ctx, dst, record); err != nil {
|
|
321
|
+
return 0, err
|
|
322
|
+
}
|
|
323
|
+
count++
|
|
324
|
+
}
|
|
325
|
+
return count, nil
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func importRelationshipEvents(ctx context.Context, src, dst *sql.DB, ownerByCredential map[string]string, defaultOwner string) (int, error) {
|
|
329
|
+
rows, err := queryMaps(ctx, src, `SELECT * FROM relationship_events`)
|
|
330
|
+
if err != nil {
|
|
331
|
+
return 0, err
|
|
332
|
+
}
|
|
333
|
+
count := 0
|
|
334
|
+
for _, row := range rows {
|
|
335
|
+
record := RelationshipEventRecord{
|
|
336
|
+
EventID: stringFromAny(row["event_id"]),
|
|
337
|
+
OwnerDID: inferOwnerDID(row, ownerByCredential, defaultOwner),
|
|
338
|
+
TargetDID: stringFromAny(row["target_did"]),
|
|
339
|
+
TargetHandle: stringFromAny(row["target_handle"]),
|
|
340
|
+
EventType: stringFromAny(row["event_type"]),
|
|
341
|
+
SourceType: stringFromAny(row["source_type"]),
|
|
342
|
+
SourceName: stringFromAny(row["source_name"]),
|
|
343
|
+
SourceGroupID: stringFromAny(row["source_group_id"]),
|
|
344
|
+
Reason: stringFromAny(row["reason"]),
|
|
345
|
+
Score: float64PtrFromAny(row["score"]),
|
|
346
|
+
Status: defaultString(stringFromAny(row["status"]), "pending"),
|
|
347
|
+
CreatedAt: stringFromAny(row["created_at"]),
|
|
348
|
+
UpdatedAt: stringFromAny(row["updated_at"]),
|
|
349
|
+
Metadata: metadataFromAny(row["metadata"]),
|
|
350
|
+
CredentialName: stringFromAny(row["credential_name"]),
|
|
351
|
+
}
|
|
352
|
+
if record.TargetDID == "" || record.EventType == "" {
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
if _, err := AppendRelationshipEvent(ctx, dst, record); err != nil {
|
|
356
|
+
return 0, err
|
|
357
|
+
}
|
|
358
|
+
count++
|
|
359
|
+
}
|
|
360
|
+
return count, nil
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
func importE2EESessions(ctx context.Context, src, dst *sql.DB, ownerByCredential map[string]string, defaultOwner string) (int, error) {
|
|
364
|
+
rows, err := queryMaps(ctx, src, `SELECT * FROM e2ee_sessions`)
|
|
365
|
+
if err != nil {
|
|
366
|
+
return 0, err
|
|
367
|
+
}
|
|
368
|
+
count := 0
|
|
369
|
+
for _, row := range rows {
|
|
370
|
+
_, err := dst.ExecContext(ctx, `
|
|
371
|
+
INSERT OR REPLACE INTO e2ee_sessions
|
|
372
|
+
(owner_did, peer_did, session_id, is_initiator, send_chain_key, recv_chain_key,
|
|
373
|
+
send_seq, recv_seq, expires_at, created_at, active_at, peer_confirmed, credential_name, updated_at)
|
|
374
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
375
|
+
inferOwnerDID(row, ownerByCredential, defaultOwner),
|
|
376
|
+
stringFromAny(row["peer_did"]),
|
|
377
|
+
stringFromAny(row["session_id"]),
|
|
378
|
+
boolToInt(boolFromAny(row["is_initiator"])),
|
|
379
|
+
stringFromAny(row["send_chain_key"]),
|
|
380
|
+
stringFromAny(row["recv_chain_key"]),
|
|
381
|
+
intFromAny(row["send_seq"]),
|
|
382
|
+
intFromAny(row["recv_seq"]),
|
|
383
|
+
normalizeOptionalFloat64(float64PtrFromAny(row["expires_at"])),
|
|
384
|
+
stringFromAny(row["created_at"]),
|
|
385
|
+
normalizeOptionalString(stringFromAny(row["active_at"])),
|
|
386
|
+
boolToInt(boolFromAny(row["peer_confirmed"])),
|
|
387
|
+
stringFromAny(row["credential_name"]),
|
|
388
|
+
defaultString(stringFromAny(row["updated_at"]), nowUTC()),
|
|
389
|
+
)
|
|
390
|
+
if err != nil {
|
|
391
|
+
return 0, err
|
|
392
|
+
}
|
|
393
|
+
count++
|
|
394
|
+
}
|
|
395
|
+
return count, nil
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
func errorsIsMissingTable(err error) bool {
|
|
399
|
+
if err == nil {
|
|
400
|
+
return false
|
|
401
|
+
}
|
|
402
|
+
return strings.Contains(err.Error(), "no such table")
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func boolFromAny(value any) bool {
|
|
406
|
+
switch typed := value.(type) {
|
|
407
|
+
case int64:
|
|
408
|
+
return typed != 0
|
|
409
|
+
case int:
|
|
410
|
+
return typed != 0
|
|
411
|
+
case float64:
|
|
412
|
+
return typed != 0
|
|
413
|
+
case bool:
|
|
414
|
+
return typed
|
|
415
|
+
case string:
|
|
416
|
+
return typed == "1" || strings.EqualFold(typed, "true")
|
|
417
|
+
default:
|
|
418
|
+
return false
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
func boolPtrFromAny(value any) *bool {
|
|
423
|
+
switch value.(type) {
|
|
424
|
+
case nil:
|
|
425
|
+
return nil
|
|
426
|
+
}
|
|
427
|
+
converted := boolFromAny(value)
|
|
428
|
+
return &converted
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
func intFromAny(value any) int {
|
|
432
|
+
switch typed := value.(type) {
|
|
433
|
+
case int:
|
|
434
|
+
return typed
|
|
435
|
+
case int64:
|
|
436
|
+
return int(typed)
|
|
437
|
+
case float64:
|
|
438
|
+
return int(typed)
|
|
439
|
+
case string:
|
|
440
|
+
if typed == "" {
|
|
441
|
+
return 0
|
|
442
|
+
}
|
|
443
|
+
var out int
|
|
444
|
+
fmt.Sscanf(typed, "%d", &out)
|
|
445
|
+
return out
|
|
446
|
+
default:
|
|
447
|
+
return 0
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
func int64PtrFromAny(value any) *int64 {
|
|
452
|
+
switch typed := value.(type) {
|
|
453
|
+
case nil:
|
|
454
|
+
return nil
|
|
455
|
+
case int64:
|
|
456
|
+
v := typed
|
|
457
|
+
return &v
|
|
458
|
+
case int:
|
|
459
|
+
v := int64(typed)
|
|
460
|
+
return &v
|
|
461
|
+
case float64:
|
|
462
|
+
v := int64(typed)
|
|
463
|
+
return &v
|
|
464
|
+
case string:
|
|
465
|
+
if strings.TrimSpace(typed) == "" {
|
|
466
|
+
return nil
|
|
467
|
+
}
|
|
468
|
+
var out int64
|
|
469
|
+
fmt.Sscanf(typed, "%d", &out)
|
|
470
|
+
return &out
|
|
471
|
+
default:
|
|
472
|
+
return nil
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func float64PtrFromAny(value any) *float64 {
|
|
477
|
+
switch typed := value.(type) {
|
|
478
|
+
case nil:
|
|
479
|
+
return nil
|
|
480
|
+
case float64:
|
|
481
|
+
v := typed
|
|
482
|
+
return &v
|
|
483
|
+
case int64:
|
|
484
|
+
v := float64(typed)
|
|
485
|
+
return &v
|
|
486
|
+
case int:
|
|
487
|
+
v := float64(typed)
|
|
488
|
+
return &v
|
|
489
|
+
case string:
|
|
490
|
+
if strings.TrimSpace(typed) == "" {
|
|
491
|
+
return nil
|
|
492
|
+
}
|
|
493
|
+
var out float64
|
|
494
|
+
fmt.Sscanf(typed, "%f", &out)
|
|
495
|
+
return &out
|
|
496
|
+
default:
|
|
497
|
+
return nil
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package store
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
appconfig "github.com/agentconnect/awiki-cli/internal/config"
|
|
11
|
+
"github.com/agentconnect/awiki-cli/internal/identity"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func pathOverridePaths(path string) appconfig.Paths {
|
|
15
|
+
return appconfig.Paths{DatabaseFile: path}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func TestImportLegacyDatabaseV11(t *testing.T) {
|
|
19
|
+
t.Parallel()
|
|
20
|
+
|
|
21
|
+
root := t.TempDir()
|
|
22
|
+
targetPaths := appconfig.Paths{
|
|
23
|
+
DatabaseFile: filepath.Join(root, "current.db"),
|
|
24
|
+
IdentityDir: filepath.Join(root, "identities"),
|
|
25
|
+
LegacyCredentialsDir: filepath.Join(root, "legacy-credentials"),
|
|
26
|
+
LegacyDataDir: filepath.Join(root, "legacy-data"),
|
|
27
|
+
}
|
|
28
|
+
legacyDBPath := filepath.Join(targetPaths.LegacyDataDir, "database", "awiki.db")
|
|
29
|
+
if err := os.MkdirAll(filepath.Dir(legacyDBPath), 0o700); err != nil {
|
|
30
|
+
t.Fatalf("MkdirAll() error = %v", err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
legacyDB, err := Open(pathOverridePaths(legacyDBPath))
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatalf("Open(legacy) error = %v", err)
|
|
36
|
+
}
|
|
37
|
+
defer legacyDB.Close()
|
|
38
|
+
if err := EnsureSchema(context.Background(), legacyDB); err != nil {
|
|
39
|
+
t.Fatalf("EnsureSchema(legacy) error = %v", err)
|
|
40
|
+
}
|
|
41
|
+
if err := StoreMessage(context.Background(), legacyDB, MessageRecord{
|
|
42
|
+
MsgID: "legacy-msg",
|
|
43
|
+
OwnerDID: "did:wba:awiki.ai:user:legacy",
|
|
44
|
+
ThreadID: MakeThreadID("did:wba:awiki.ai:user:legacy", "did:wba:awiki.ai:user:peer", ""),
|
|
45
|
+
Direction: 0,
|
|
46
|
+
SenderDID: "did:wba:awiki.ai:user:peer",
|
|
47
|
+
ReceiverDID: "did:wba:awiki.ai:user:legacy",
|
|
48
|
+
ContentType: "text",
|
|
49
|
+
Content: "legacy hello",
|
|
50
|
+
CredentialName: "legacy",
|
|
51
|
+
}); err != nil {
|
|
52
|
+
t.Fatalf("StoreMessage(legacy) error = %v", err)
|
|
53
|
+
}
|
|
54
|
+
if err := UpsertContact(context.Background(), legacyDB, ContactRecord{
|
|
55
|
+
OwnerDID: "did:wba:awiki.ai:user:legacy",
|
|
56
|
+
DID: "did:wba:awiki.ai:user:peer",
|
|
57
|
+
Name: "Legacy Peer",
|
|
58
|
+
}); err != nil {
|
|
59
|
+
t.Fatalf("UpsertContact(legacy) error = %v", err)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
manager := identity.NewManager(targetPaths)
|
|
63
|
+
generated, err := identity.GenerateIdentity(identity.GenerateOptions{
|
|
64
|
+
Hostname: "awiki.ai",
|
|
65
|
+
PathPrefix: []string{"user"},
|
|
66
|
+
ProofDomain: "awiki.ai",
|
|
67
|
+
})
|
|
68
|
+
if err != nil {
|
|
69
|
+
t.Fatalf("GenerateIdentity() error = %v", err)
|
|
70
|
+
}
|
|
71
|
+
if _, err := manager.Save(identity.SaveInput{
|
|
72
|
+
IdentityName: "legacy",
|
|
73
|
+
DID: "did:wba:awiki.ai:user:legacy",
|
|
74
|
+
UniqueID: generated.UniqueID,
|
|
75
|
+
DisplayName: "Legacy",
|
|
76
|
+
DIDDocument: generated.DIDDocument,
|
|
77
|
+
Key1PrivatePEM: generated.Key1PrivatePEM,
|
|
78
|
+
Key1PublicPEM: generated.Key1PublicPEM,
|
|
79
|
+
}); err != nil {
|
|
80
|
+
t.Fatalf("Save() error = %v", err)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
targetDB, err := Open(targetPaths)
|
|
84
|
+
if err != nil {
|
|
85
|
+
t.Fatalf("Open(target) error = %v", err)
|
|
86
|
+
}
|
|
87
|
+
defer targetDB.Close()
|
|
88
|
+
report, err := ImportLegacyDatabase(context.Background(), targetDB, targetPaths, manager)
|
|
89
|
+
if err != nil {
|
|
90
|
+
t.Fatalf("ImportLegacyDatabase() error = %v", err)
|
|
91
|
+
}
|
|
92
|
+
if report.ImportedRows["messages"] != 1 || report.ImportedRows["contacts"] != 1 {
|
|
93
|
+
raw, _ := json.MarshalIndent(report, "", " ")
|
|
94
|
+
t.Fatalf("unexpected import report: %s", raw)
|
|
95
|
+
}
|
|
96
|
+
row, err := GetMessageByID(context.Background(), targetDB, "legacy-msg", "did:wba:awiki.ai:user:legacy", "")
|
|
97
|
+
if err != nil {
|
|
98
|
+
t.Fatalf("GetMessageByID(target) error = %v", err)
|
|
99
|
+
}
|
|
100
|
+
if row["content"] != "legacy hello" {
|
|
101
|
+
t.Fatalf("unexpected imported message: %#v", row)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package store
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"database/sql"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
appconfig "github.com/agentconnect/awiki-cli/internal/config"
|
|
12
|
+
_ "modernc.org/sqlite"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const sqliteDriverName = "sqlite"
|
|
16
|
+
|
|
17
|
+
func Open(paths appconfig.Paths) (*sql.DB, error) {
|
|
18
|
+
return openDatabase(paths.DatabaseFile, OpenOptions{})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func OpenReadOnly(path string) (*sql.DB, error) {
|
|
22
|
+
return openDatabase(path, OpenOptions{ReadOnly: true})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func openDatabase(path string, options OpenOptions) (*sql.DB, error) {
|
|
26
|
+
if path == "" {
|
|
27
|
+
return nil, fmt.Errorf("sqlite path is required")
|
|
28
|
+
}
|
|
29
|
+
if !options.ReadOnly {
|
|
30
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
31
|
+
return nil, fmt.Errorf("create sqlite dir: %w", err)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
dsn := path
|
|
35
|
+
if options.ReadOnly {
|
|
36
|
+
dsn = fmt.Sprintf("file:%s?mode=ro", path)
|
|
37
|
+
}
|
|
38
|
+
db, err := sql.Open(sqliteDriverName, dsn)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return nil, fmt.Errorf("open sqlite database: %w", err)
|
|
41
|
+
}
|
|
42
|
+
db.SetMaxOpenConns(1)
|
|
43
|
+
db.SetConnMaxIdleTime(time.Minute)
|
|
44
|
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
45
|
+
defer cancel()
|
|
46
|
+
if err := db.PingContext(ctx); err != nil {
|
|
47
|
+
_ = db.Close()
|
|
48
|
+
return nil, fmt.Errorf("ping sqlite database: %w", err)
|
|
49
|
+
}
|
|
50
|
+
if !options.ReadOnly {
|
|
51
|
+
if err := configureDatabase(ctx, db); err != nil {
|
|
52
|
+
_ = db.Close()
|
|
53
|
+
return nil, err
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return db, nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func configureDatabase(ctx context.Context, db *sql.DB) error {
|
|
60
|
+
pragmas := []string{
|
|
61
|
+
"PRAGMA journal_mode=WAL;",
|
|
62
|
+
"PRAGMA foreign_keys=ON;",
|
|
63
|
+
"PRAGMA busy_timeout=5000;",
|
|
64
|
+
}
|
|
65
|
+
for _, pragma := range pragmas {
|
|
66
|
+
if _, err := db.ExecContext(ctx, pragma); err != nil {
|
|
67
|
+
return fmt.Errorf("configure sqlite (%s): %w", pragma, err)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return nil
|
|
71
|
+
}
|