@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,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
|
+
}
|