@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,151 @@
1
+ package store
2
+
3
+ import (
4
+ "context"
5
+ "database/sql"
6
+ "fmt"
7
+ "strings"
8
+ )
9
+
10
+ func ListInboxMessages(ctx context.Context, db *sql.DB, ownerDID string, limit int, peerDID string, unreadOnly bool) ([]map[string]any, error) {
11
+ if limit <= 0 {
12
+ limit = 20
13
+ }
14
+ query := `
15
+ SELECT *
16
+ FROM messages
17
+ WHERE owner_did = ?
18
+ AND direction = 0`
19
+ args := []any{normalizeOwnerDID(ownerDID)}
20
+ if unreadOnly {
21
+ query += " AND is_read = 0"
22
+ }
23
+ if strings.TrimSpace(peerDID) != "" {
24
+ query += " AND (sender_did = ? OR receiver_did = ?)"
25
+ args = append(args, peerDID, peerDID)
26
+ }
27
+ query += " ORDER BY COALESCE(sent_at, stored_at) DESC LIMIT ?"
28
+ args = append(args, limit)
29
+ return queryMaps(ctx, db, query, args...)
30
+ }
31
+
32
+ func ListThreadMessages(ctx context.Context, db *sql.DB, ownerDID string, threadID string, limit int) ([]map[string]any, error) {
33
+ if strings.TrimSpace(threadID) == "" {
34
+ return nil, fmt.Errorf("thread_id is required")
35
+ }
36
+ if limit <= 0 {
37
+ limit = 50
38
+ }
39
+ return queryMaps(ctx, db, `
40
+ SELECT *
41
+ FROM messages
42
+ WHERE owner_did = ? AND thread_id = ?
43
+ ORDER BY COALESCE(sent_at, stored_at) DESC
44
+ LIMIT ?`, normalizeOwnerDID(ownerDID), threadID, limit)
45
+ }
46
+
47
+ func ListGroupInboxMessages(ctx context.Context, db *sql.DB, ownerDID string, limit int, groupID string, unreadOnly bool) ([]map[string]any, error) {
48
+ if limit <= 0 {
49
+ limit = 20
50
+ }
51
+ query := `
52
+ SELECT *
53
+ FROM messages
54
+ WHERE owner_did = ?
55
+ AND direction = 0
56
+ AND COALESCE(group_did, group_id) IS NOT NULL`
57
+ args := []any{normalizeOwnerDID(ownerDID)}
58
+ if unreadOnly {
59
+ query += " AND is_read = 0"
60
+ }
61
+ if strings.TrimSpace(groupID) != "" {
62
+ query += " AND (group_did = ? OR group_id = ?)"
63
+ args = append(args, groupID, groupID)
64
+ }
65
+ query += " ORDER BY COALESCE(sent_at, stored_at) DESC LIMIT ?"
66
+ args = append(args, limit)
67
+ return queryMaps(ctx, db, query, args...)
68
+ }
69
+
70
+ func ListGroupMessages(ctx context.Context, db *sql.DB, ownerDID string, groupID string, limit int, sinceSeq *int64) ([]map[string]any, error) {
71
+ if strings.TrimSpace(groupID) == "" {
72
+ return nil, fmt.Errorf("group_id is required")
73
+ }
74
+ if limit <= 0 {
75
+ limit = 50
76
+ }
77
+ query := `
78
+ SELECT *
79
+ FROM messages
80
+ WHERE owner_did = ?
81
+ AND (group_did = ? OR group_id = ?)`
82
+ args := []any{normalizeOwnerDID(ownerDID), groupID, groupID}
83
+ if sinceSeq != nil {
84
+ query += " AND COALESCE(server_seq, 0) > ?"
85
+ args = append(args, *sinceSeq)
86
+ }
87
+ query += " ORDER BY COALESCE(server_seq, 0) DESC, COALESCE(sent_at, stored_at) DESC LIMIT ?"
88
+ args = append(args, limit)
89
+ return queryMaps(ctx, db, query, args...)
90
+ }
91
+
92
+ func GetGroupSnapshot(ctx context.Context, db *sql.DB, ownerDID string, groupID string) (map[string]any, error) {
93
+ return queryOneMap(ctx, db, `
94
+ SELECT *
95
+ FROM groups
96
+ WHERE owner_did = ? AND (group_id = ? OR group_did = ?)`,
97
+ normalizeOwnerDID(ownerDID), groupID, groupID,
98
+ )
99
+ }
100
+
101
+ func ListCachedGroupMembers(ctx context.Context, db *sql.DB, ownerDID string, groupID string, limit int) ([]map[string]any, error) {
102
+ if strings.TrimSpace(groupID) == "" {
103
+ return nil, fmt.Errorf("group_id is required")
104
+ }
105
+ if limit <= 0 {
106
+ limit = 100
107
+ }
108
+ return queryMaps(ctx, db, `
109
+ SELECT *
110
+ FROM group_members
111
+ WHERE owner_did = ? AND group_id = ?
112
+ ORDER BY role ASC, member_handle ASC, member_did ASC
113
+ LIMIT ?`, normalizeOwnerDID(ownerDID), groupID, limit)
114
+ }
115
+
116
+ func ListMessagesByIDs(ctx context.Context, db *sql.DB, ownerDID string, messageIDs []string) ([]map[string]any, error) {
117
+ if len(messageIDs) == 0 {
118
+ return nil, nil
119
+ }
120
+ placeholders := make([]string, 0, len(messageIDs))
121
+ args := make([]any, 0, len(messageIDs)+1)
122
+ args = append(args, normalizeOwnerDID(ownerDID))
123
+ for _, id := range messageIDs {
124
+ placeholders = append(placeholders, "?")
125
+ args = append(args, id)
126
+ }
127
+ query := fmt.Sprintf(`SELECT * FROM messages WHERE owner_did = ? AND msg_id IN (%s)`, strings.Join(placeholders, ","))
128
+ return queryMaps(ctx, db, query, args...)
129
+ }
130
+
131
+ func MarkMessagesRead(ctx context.Context, db *sql.DB, ownerDID string, messageIDs []string) (int64, error) {
132
+ if len(messageIDs) == 0 {
133
+ return 0, nil
134
+ }
135
+ placeholders := make([]string, 0, len(messageIDs))
136
+ args := make([]any, 0, len(messageIDs)+1)
137
+ args = append(args, normalizeOwnerDID(ownerDID))
138
+ for _, id := range messageIDs {
139
+ placeholders = append(placeholders, "?")
140
+ args = append(args, id)
141
+ }
142
+ query := fmt.Sprintf(
143
+ `UPDATE messages SET is_read = 1 WHERE owner_did = ? AND msg_id IN (%s)`,
144
+ strings.Join(placeholders, ","),
145
+ )
146
+ result, err := db.ExecContext(ctx, query, args...)
147
+ if err != nil {
148
+ return 0, err
149
+ }
150
+ return result.RowsAffected()
151
+ }
@@ -0,0 +1,277 @@
1
+ package store
2
+
3
+ import (
4
+ "context"
5
+ "database/sql"
6
+ "fmt"
7
+ )
8
+
9
+ const v6TablesSQL = `
10
+ CREATE TABLE IF NOT EXISTS contacts (
11
+ owner_did TEXT NOT NULL DEFAULT '',
12
+ did TEXT NOT NULL,
13
+ name TEXT,
14
+ handle TEXT,
15
+ nick_name TEXT,
16
+ bio TEXT,
17
+ profile_md TEXT,
18
+ tags TEXT,
19
+ relationship TEXT,
20
+ source_type TEXT,
21
+ source_name TEXT,
22
+ source_group_id TEXT,
23
+ connected_at TEXT,
24
+ recommended_reason TEXT,
25
+ followed INTEGER NOT NULL DEFAULT 0,
26
+ messaged INTEGER NOT NULL DEFAULT 0,
27
+ note TEXT,
28
+ first_seen_at TEXT,
29
+ last_seen_at TEXT,
30
+ metadata TEXT,
31
+ PRIMARY KEY (owner_did, did)
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS messages (
35
+ msg_id TEXT NOT NULL,
36
+ owner_did TEXT NOT NULL DEFAULT '',
37
+ thread_id TEXT NOT NULL,
38
+ direction INTEGER NOT NULL DEFAULT 0,
39
+ sender_did TEXT,
40
+ receiver_did TEXT,
41
+ group_id TEXT,
42
+ group_did TEXT,
43
+ content_type TEXT DEFAULT 'text',
44
+ content TEXT,
45
+ title TEXT,
46
+ server_seq INTEGER,
47
+ sent_at TEXT,
48
+ stored_at TEXT NOT NULL,
49
+ is_e2ee INTEGER DEFAULT 0,
50
+ is_read INTEGER DEFAULT 0,
51
+ sender_name TEXT,
52
+ metadata TEXT,
53
+ credential_name TEXT NOT NULL DEFAULT '',
54
+ PRIMARY KEY (msg_id, owner_did)
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS e2ee_outbox (
58
+ outbox_id TEXT PRIMARY KEY,
59
+ owner_did TEXT NOT NULL DEFAULT '',
60
+ peer_did TEXT NOT NULL,
61
+ session_id TEXT,
62
+ original_type TEXT NOT NULL DEFAULT 'text',
63
+ plaintext TEXT NOT NULL,
64
+ local_status TEXT NOT NULL DEFAULT 'queued',
65
+ attempt_count INTEGER NOT NULL DEFAULT 0,
66
+ sent_msg_id TEXT,
67
+ sent_server_seq INTEGER,
68
+ last_error_code TEXT,
69
+ retry_hint TEXT,
70
+ failed_msg_id TEXT,
71
+ failed_server_seq INTEGER,
72
+ metadata TEXT,
73
+ last_attempt_at TEXT,
74
+ created_at TEXT NOT NULL,
75
+ updated_at TEXT NOT NULL,
76
+ credential_name TEXT NOT NULL DEFAULT ''
77
+ );
78
+ `
79
+
80
+ const v7TablesSQL = `
81
+ CREATE TABLE IF NOT EXISTS groups (
82
+ owner_did TEXT NOT NULL DEFAULT '',
83
+ group_id TEXT NOT NULL,
84
+ group_did TEXT,
85
+ name TEXT,
86
+ group_mode TEXT NOT NULL DEFAULT 'general',
87
+ slug TEXT,
88
+ description TEXT,
89
+ goal TEXT,
90
+ rules TEXT,
91
+ message_prompt TEXT,
92
+ doc_url TEXT,
93
+ group_owner_did TEXT,
94
+ group_owner_handle TEXT,
95
+ my_role TEXT,
96
+ membership_status TEXT NOT NULL DEFAULT 'active',
97
+ join_enabled INTEGER,
98
+ join_code TEXT,
99
+ join_code_expires_at TEXT,
100
+ member_count INTEGER,
101
+ last_synced_seq INTEGER,
102
+ last_read_seq INTEGER,
103
+ last_message_at TEXT,
104
+ remote_created_at TEXT,
105
+ remote_updated_at TEXT,
106
+ stored_at TEXT NOT NULL,
107
+ metadata TEXT,
108
+ credential_name TEXT NOT NULL DEFAULT '',
109
+ PRIMARY KEY (owner_did, group_id)
110
+ );
111
+
112
+ CREATE TABLE IF NOT EXISTS group_members (
113
+ owner_did TEXT NOT NULL DEFAULT '',
114
+ group_id TEXT NOT NULL,
115
+ user_id TEXT NOT NULL,
116
+ member_did TEXT,
117
+ member_handle TEXT,
118
+ profile_url TEXT,
119
+ role TEXT,
120
+ status TEXT NOT NULL DEFAULT 'active',
121
+ joined_at TEXT,
122
+ sent_message_count INTEGER NOT NULL DEFAULT 0,
123
+ last_synced_at TEXT NOT NULL,
124
+ metadata TEXT,
125
+ credential_name TEXT NOT NULL DEFAULT '',
126
+ PRIMARY KEY (owner_did, group_id, user_id)
127
+ );
128
+ `
129
+
130
+ const v8TablesSQL = `
131
+ CREATE TABLE IF NOT EXISTS relationship_events (
132
+ event_id TEXT PRIMARY KEY,
133
+ owner_did TEXT NOT NULL DEFAULT '',
134
+ target_did TEXT NOT NULL,
135
+ target_handle TEXT,
136
+ event_type TEXT NOT NULL,
137
+ source_type TEXT,
138
+ source_name TEXT,
139
+ source_group_id TEXT,
140
+ reason TEXT,
141
+ score REAL,
142
+ status TEXT NOT NULL DEFAULT 'pending',
143
+ created_at TEXT NOT NULL,
144
+ updated_at TEXT NOT NULL,
145
+ metadata TEXT,
146
+ credential_name TEXT NOT NULL DEFAULT ''
147
+ );
148
+ `
149
+
150
+ const v11TablesSQL = `
151
+ CREATE TABLE IF NOT EXISTS e2ee_sessions (
152
+ owner_did TEXT NOT NULL DEFAULT '',
153
+ peer_did TEXT NOT NULL,
154
+ session_id TEXT NOT NULL,
155
+ is_initiator INTEGER NOT NULL DEFAULT 0,
156
+ send_chain_key TEXT NOT NULL,
157
+ recv_chain_key TEXT NOT NULL,
158
+ send_seq INTEGER NOT NULL DEFAULT 0,
159
+ recv_seq INTEGER NOT NULL DEFAULT 0,
160
+ expires_at REAL,
161
+ created_at TEXT NOT NULL,
162
+ active_at TEXT,
163
+ peer_confirmed INTEGER NOT NULL DEFAULT 0,
164
+ credential_name TEXT NOT NULL DEFAULT '',
165
+ updated_at TEXT NOT NULL,
166
+ PRIMARY KEY (owner_did, peer_did),
167
+ UNIQUE (owner_did, session_id)
168
+ );
169
+ `
170
+
171
+ var indexStatements = []string{
172
+ `CREATE INDEX IF NOT EXISTS idx_contacts_owner ON contacts(owner_did, last_seen_at DESC)`,
173
+ `CREATE INDEX IF NOT EXISTS idx_messages_owner_thread ON messages(owner_did, thread_id, sent_at)`,
174
+ `CREATE INDEX IF NOT EXISTS idx_messages_owner_thread_seq ON messages(owner_did, thread_id, server_seq)`,
175
+ `CREATE INDEX IF NOT EXISTS idx_messages_owner_direction ON messages(owner_did, direction)`,
176
+ `CREATE INDEX IF NOT EXISTS idx_messages_owner_sender ON messages(owner_did, sender_did)`,
177
+ `CREATE INDEX IF NOT EXISTS idx_messages_owner ON messages(owner_did)`,
178
+ `CREATE INDEX IF NOT EXISTS idx_messages_credential ON messages(credential_name)`,
179
+ `CREATE INDEX IF NOT EXISTS idx_e2ee_outbox_owner_status ON e2ee_outbox(owner_did, local_status, updated_at DESC)`,
180
+ `CREATE INDEX IF NOT EXISTS idx_e2ee_outbox_owner_sent_msg ON e2ee_outbox(owner_did, sent_msg_id)`,
181
+ `CREATE INDEX IF NOT EXISTS idx_e2ee_outbox_owner_sent_seq ON e2ee_outbox(owner_did, peer_did, sent_server_seq)`,
182
+ `CREATE INDEX IF NOT EXISTS idx_e2ee_outbox_credential ON e2ee_outbox(credential_name)`,
183
+ `CREATE INDEX IF NOT EXISTS idx_groups_owner_status_last_message ON groups(owner_did, membership_status, last_message_at DESC)`,
184
+ `CREATE INDEX IF NOT EXISTS idx_groups_owner_slug ON groups(owner_did, slug)`,
185
+ `CREATE INDEX IF NOT EXISTS idx_groups_owner_updated ON groups(owner_did, remote_updated_at DESC)`,
186
+ `CREATE INDEX IF NOT EXISTS idx_group_members_owner_group_role ON group_members(owner_did, group_id, role)`,
187
+ `CREATE INDEX IF NOT EXISTS idx_group_members_owner_group_status ON group_members(owner_did, group_id, status)`,
188
+ `CREATE INDEX IF NOT EXISTS idx_contacts_owner_source_group ON contacts(owner_did, source_group_id)`,
189
+ `CREATE INDEX IF NOT EXISTS idx_relationship_events_owner_target_time ON relationship_events(owner_did, target_did, created_at DESC)`,
190
+ `CREATE INDEX IF NOT EXISTS idx_relationship_events_owner_status_time ON relationship_events(owner_did, status, created_at DESC)`,
191
+ `CREATE INDEX IF NOT EXISTS idx_relationship_events_owner_group ON relationship_events(owner_did, source_group_id)`,
192
+ `CREATE INDEX IF NOT EXISTS idx_e2ee_sessions_owner_updated ON e2ee_sessions(owner_did, updated_at DESC)`,
193
+ `CREATE INDEX IF NOT EXISTS idx_e2ee_sessions_credential ON e2ee_sessions(credential_name)`,
194
+ }
195
+
196
+ var viewStatements = []string{
197
+ `CREATE VIEW threads AS
198
+ SELECT
199
+ owner_did,
200
+ thread_id,
201
+ COUNT(*) AS message_count,
202
+ SUM(CASE WHEN is_read = 0 AND direction = 0 THEN 1 ELSE 0 END) AS unread_count,
203
+ MAX(COALESCE(sent_at, stored_at)) AS last_message_at,
204
+ (SELECT m2.content FROM messages m2
205
+ WHERE m2.owner_did = m.owner_did
206
+ AND m2.thread_id = m.thread_id
207
+ ORDER BY COALESCE(m2.sent_at, m2.stored_at) DESC
208
+ LIMIT 1) AS last_content
209
+ FROM messages m
210
+ GROUP BY owner_did, thread_id`,
211
+ `CREATE VIEW inbox AS
212
+ SELECT * FROM messages WHERE direction = 0
213
+ ORDER BY owner_did, COALESCE(sent_at, stored_at) DESC`,
214
+ `CREATE VIEW outbox AS
215
+ SELECT * FROM messages WHERE direction = 1
216
+ ORDER BY owner_did, COALESCE(sent_at, stored_at) DESC`,
217
+ }
218
+
219
+ func EnsureSchema(ctx context.Context, db *sql.DB) error {
220
+ version, err := schemaVersion(ctx, db)
221
+ if err != nil {
222
+ return fmt.Errorf("read sqlite schema version: %w", err)
223
+ }
224
+ if version == 0 {
225
+ if err := createSchema(ctx, db); err != nil {
226
+ return err
227
+ }
228
+ return setSchemaVersion(ctx, db, SchemaVersion)
229
+ }
230
+ if version > SchemaVersion {
231
+ return fmt.Errorf("sqlite schema version %d is newer than supported %d", version, SchemaVersion)
232
+ }
233
+ if version < 6 {
234
+ return fmt.Errorf("sqlite schema version %d is too old for in-place upgrade", version)
235
+ }
236
+ if err := createSchema(ctx, db); err != nil {
237
+ return err
238
+ }
239
+ return setSchemaVersion(ctx, db, SchemaVersion)
240
+ }
241
+
242
+ func CurrentSchemaVersion(db *sql.DB) (int, error) {
243
+ return schemaVersion(context.Background(), db)
244
+ }
245
+
246
+ func createSchema(ctx context.Context, db *sql.DB) error {
247
+ scripts := []string{v6TablesSQL, v7TablesSQL, v8TablesSQL, v11TablesSQL}
248
+ for _, script := range scripts {
249
+ if _, err := db.ExecContext(ctx, script); err != nil {
250
+ return fmt.Errorf("apply sqlite schema: %w", err)
251
+ }
252
+ }
253
+ for _, statement := range indexStatements {
254
+ if _, err := db.ExecContext(ctx, statement); err != nil {
255
+ return fmt.Errorf("apply sqlite index: %w", err)
256
+ }
257
+ }
258
+ for _, view := range []string{"threads", "inbox", "outbox"} {
259
+ if _, err := db.ExecContext(ctx, fmt.Sprintf("DROP VIEW IF EXISTS %s", view)); err != nil {
260
+ return fmt.Errorf("drop sqlite view %s: %w", view, err)
261
+ }
262
+ }
263
+ for _, statement := range viewStatements {
264
+ if _, err := db.ExecContext(ctx, statement); err != nil {
265
+ return fmt.Errorf("apply sqlite view: %w", err)
266
+ }
267
+ }
268
+ return nil
269
+ }
270
+
271
+ func setSchemaVersion(ctx context.Context, db *sql.DB, version int) error {
272
+ _, err := db.ExecContext(ctx, fmt.Sprintf("PRAGMA user_version = %d", version))
273
+ if err != nil {
274
+ return fmt.Errorf("set sqlite schema version: %w", err)
275
+ }
276
+ return nil
277
+ }
@@ -0,0 +1,56 @@
1
+ package store
2
+
3
+ import (
4
+ "context"
5
+ "database/sql"
6
+ "path/filepath"
7
+ "testing"
8
+
9
+ appconfig "github.com/agentconnect/awiki-cli/internal/config"
10
+ )
11
+
12
+ func openTestDB(t *testing.T) *sql.DB {
13
+ t.Helper()
14
+ root := t.TempDir()
15
+ db, err := Open(appconfig.Paths{DatabaseFile: filepath.Join(root, "awiki-cli.db")})
16
+ if err != nil {
17
+ t.Fatalf("Open() error = %v", err)
18
+ }
19
+ t.Cleanup(func() { _ = db.Close() })
20
+ return db
21
+ }
22
+
23
+ func TestEnsureSchemaCreatesVersionAndTables(t *testing.T) {
24
+ t.Parallel()
25
+
26
+ db := openTestDB(t)
27
+ ctx := context.Background()
28
+ if err := EnsureSchema(ctx, db); err != nil {
29
+ t.Fatalf("EnsureSchema() error = %v", err)
30
+ }
31
+ version, err := CurrentSchemaVersion(db)
32
+ if err != nil {
33
+ t.Fatalf("CurrentSchemaVersion() error = %v", err)
34
+ }
35
+ if version != SchemaVersion {
36
+ t.Fatalf("schema version mismatch: got=%d want=%d", version, SchemaVersion)
37
+ }
38
+ for _, table := range []string{"contacts", "messages", "e2ee_outbox", "groups", "group_members", "relationship_events", "e2ee_sessions"} {
39
+ exists, err := tableExists(ctx, db, table)
40
+ if err != nil {
41
+ t.Fatalf("tableExists(%s) error = %v", table, err)
42
+ }
43
+ if !exists {
44
+ t.Fatalf("expected table %s to exist", table)
45
+ }
46
+ }
47
+ for _, view := range []string{"threads", "inbox", "outbox"} {
48
+ exists, err := viewExists(ctx, db, view)
49
+ if err != nil {
50
+ t.Fatalf("viewExists(%s) error = %v", view, err)
51
+ }
52
+ if !exists {
53
+ t.Fatalf("expected view %s to exist", view)
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,177 @@
1
+ package store
2
+
3
+ import "errors"
4
+
5
+ const (
6
+ SchemaVersion = 11
7
+ )
8
+
9
+ var (
10
+ ErrUnsupportedLegacySchema = errors.New("unsupported legacy sqlite schema version")
11
+ ErrLegacyDatabaseNotFound = errors.New("legacy sqlite database not found")
12
+ ErrUnsafeSQL = errors.New("unsafe sql statement")
13
+ )
14
+
15
+ type OpenOptions struct {
16
+ ReadOnly bool
17
+ }
18
+
19
+ type LegacyScan struct {
20
+ Path string `json:"path"`
21
+ Exists bool `json:"exists"`
22
+ SchemaVersion int `json:"schema_version"`
23
+ Tables []string `json:"tables,omitempty"`
24
+ }
25
+
26
+ type ImportReport struct {
27
+ SourcePath string `json:"source_path"`
28
+ SourceSchemaVersion int `json:"source_schema_version"`
29
+ ImportedRows map[string]int `json:"imported_rows"`
30
+ SkippedTables []string `json:"skipped_tables,omitempty"`
31
+ Warnings []string `json:"warnings,omitempty"`
32
+ }
33
+
34
+ type MessageRecord struct {
35
+ MsgID string
36
+ OwnerDID string
37
+ ThreadID string
38
+ Direction int
39
+ SenderDID string
40
+ ReceiverDID string
41
+ GroupID string
42
+ GroupDID string
43
+ ContentType string
44
+ Content string
45
+ Title string
46
+ ServerSeq *int64
47
+ SentAt string
48
+ IsE2EE bool
49
+ IsRead bool
50
+ SenderName string
51
+ Metadata string
52
+ CredentialName string
53
+ }
54
+
55
+ type ContactRecord struct {
56
+ OwnerDID string
57
+ DID string
58
+ Name string
59
+ Handle string
60
+ NickName string
61
+ Bio string
62
+ ProfileMD string
63
+ Tags string
64
+ Relationship string
65
+ SourceType string
66
+ SourceName string
67
+ SourceGroupID string
68
+ ConnectedAt string
69
+ RecommendedReason string
70
+ Followed *bool
71
+ Messaged *bool
72
+ Note string
73
+ FirstSeenAt string
74
+ LastSeenAt string
75
+ Metadata string
76
+ }
77
+
78
+ type RelationshipEventRecord struct {
79
+ EventID string
80
+ OwnerDID string
81
+ TargetDID string
82
+ TargetHandle string
83
+ EventType string
84
+ SourceType string
85
+ SourceName string
86
+ SourceGroupID string
87
+ Reason string
88
+ Score *float64
89
+ Status string
90
+ CreatedAt string
91
+ UpdatedAt string
92
+ Metadata string
93
+ CredentialName string
94
+ }
95
+
96
+ type GroupRecord struct {
97
+ OwnerDID string
98
+ GroupID string
99
+ GroupDID string
100
+ Name string
101
+ GroupMode string
102
+ Slug string
103
+ Description string
104
+ Goal string
105
+ Rules string
106
+ MessagePrompt string
107
+ DocURL string
108
+ GroupOwnerDID string
109
+ GroupOwnerHandle string
110
+ MyRole string
111
+ MembershipStatus string
112
+ JoinEnabled *bool
113
+ JoinCode string
114
+ JoinCodeExpiresAt string
115
+ MemberCount *int64
116
+ LastSyncedSeq *int64
117
+ LastReadSeq *int64
118
+ LastMessageAt string
119
+ RemoteCreatedAt string
120
+ RemoteUpdatedAt string
121
+ Metadata string
122
+ CredentialName string
123
+ }
124
+
125
+ type GroupMemberRecord struct {
126
+ OwnerDID string
127
+ GroupID string
128
+ UserID string
129
+ MemberDID string
130
+ MemberHandle string
131
+ ProfileURL string
132
+ Role string
133
+ Status string
134
+ JoinedAt string
135
+ SentMessageCount *int64
136
+ Metadata string
137
+ CredentialName string
138
+ }
139
+
140
+ type E2EEOutboxRecord struct {
141
+ OutboxID string
142
+ OwnerDID string
143
+ PeerDID string
144
+ SessionID string
145
+ OriginalType string
146
+ Plaintext string
147
+ LocalStatus string
148
+ AttemptCount int
149
+ SentMsgID string
150
+ SentServerSeq *int64
151
+ LastErrorCode string
152
+ RetryHint string
153
+ FailedMsgID string
154
+ FailedServerSeq *int64
155
+ Metadata string
156
+ LastAttemptAt string
157
+ CreatedAt string
158
+ UpdatedAt string
159
+ CredentialName string
160
+ }
161
+
162
+ type E2EESessionRecord struct {
163
+ OwnerDID string
164
+ PeerDID string
165
+ SessionID string
166
+ IsInitiator bool
167
+ SendChainKey string
168
+ RecvChainKey string
169
+ SendSeq int
170
+ RecvSeq int
171
+ ExpiresAt *float64
172
+ CreatedAt string
173
+ ActiveAt string
174
+ PeerConfirmed bool
175
+ UpdatedAt string
176
+ CredentialName string
177
+ }