@pingagent/sdk 0.1.7 → 0.1.9
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/bin/pingagent.js +24 -3
- package/dist/chunk-3OEFISNL.js +2433 -0
- package/dist/chunk-5Z6HZWDA.js +2603 -0
- package/dist/chunk-BSDY6AKB.js +2918 -0
- package/dist/chunk-PFABO4C7.js +2961 -0
- package/dist/chunk-QK2GMSWC.js +2959 -0
- package/dist/chunk-TCYDOFRQ.js +2085 -0
- package/dist/chunk-V7HHUQT6.js +1962 -0
- package/dist/index.d.ts +403 -5
- package/dist/index.js +41 -3
- package/dist/web-server.js +1151 -16
- package/package.json +11 -3
- package/__tests__/cli.test.ts +0 -225
- package/__tests__/identity.test.ts +0 -47
- package/__tests__/store.test.ts +0 -332
- package/src/a2a-adapter.ts +0 -159
- package/src/auth.ts +0 -50
- package/src/client.ts +0 -582
- package/src/contacts.ts +0 -210
- package/src/history.ts +0 -269
- package/src/identity.ts +0 -86
- package/src/index.ts +0 -25
- package/src/paths.ts +0 -52
- package/src/store.ts +0 -62
- package/src/transport.ts +0 -141
- package/src/web-server.ts +0 -1148
- package/src/ws-subscription.ts +0 -428
- package/tsconfig.json +0 -8
package/src/contacts.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import type { LocalStore } from './store.js';
|
|
2
|
-
|
|
3
|
-
export interface Contact {
|
|
4
|
-
did: string;
|
|
5
|
-
alias?: string;
|
|
6
|
-
display_name?: string;
|
|
7
|
-
notes?: string;
|
|
8
|
-
conversation_id?: string;
|
|
9
|
-
trusted: boolean;
|
|
10
|
-
added_at: number;
|
|
11
|
-
last_message_at?: number;
|
|
12
|
-
tags?: string[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface ContactRow {
|
|
16
|
-
did: string;
|
|
17
|
-
alias: string | null;
|
|
18
|
-
display_name: string | null;
|
|
19
|
-
notes: string | null;
|
|
20
|
-
conversation_id: string | null;
|
|
21
|
-
trusted: number;
|
|
22
|
-
added_at: number;
|
|
23
|
-
last_message_at: number | null;
|
|
24
|
-
tags: string | null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function rowToContact(row: ContactRow): Contact {
|
|
28
|
-
return {
|
|
29
|
-
did: row.did,
|
|
30
|
-
alias: row.alias ?? undefined,
|
|
31
|
-
display_name: row.display_name ?? undefined,
|
|
32
|
-
notes: row.notes ?? undefined,
|
|
33
|
-
conversation_id: row.conversation_id ?? undefined,
|
|
34
|
-
trusted: row.trusted === 1,
|
|
35
|
-
added_at: row.added_at,
|
|
36
|
-
last_message_at: row.last_message_at ?? undefined,
|
|
37
|
-
tags: row.tags ? JSON.parse(row.tags) : undefined,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class ContactManager {
|
|
42
|
-
private store: LocalStore;
|
|
43
|
-
|
|
44
|
-
constructor(store: LocalStore) {
|
|
45
|
-
this.store = store;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
add(contact: Omit<Contact, 'added_at'> & { added_at?: number }): Contact {
|
|
49
|
-
const db = this.store.getDb();
|
|
50
|
-
const now = contact.added_at ?? Date.now();
|
|
51
|
-
db.prepare(`
|
|
52
|
-
INSERT INTO contacts (did, alias, display_name, notes, conversation_id, trusted, added_at, last_message_at, tags)
|
|
53
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
-
ON CONFLICT(did) DO UPDATE SET
|
|
55
|
-
alias = COALESCE(excluded.alias, contacts.alias),
|
|
56
|
-
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
57
|
-
notes = COALESCE(excluded.notes, contacts.notes),
|
|
58
|
-
conversation_id = COALESCE(excluded.conversation_id, contacts.conversation_id),
|
|
59
|
-
trusted = excluded.trusted,
|
|
60
|
-
last_message_at = COALESCE(excluded.last_message_at, contacts.last_message_at),
|
|
61
|
-
tags = COALESCE(excluded.tags, contacts.tags)
|
|
62
|
-
`).run(
|
|
63
|
-
contact.did,
|
|
64
|
-
contact.alias ?? null,
|
|
65
|
-
contact.display_name ?? null,
|
|
66
|
-
contact.notes ?? null,
|
|
67
|
-
contact.conversation_id ?? null,
|
|
68
|
-
contact.trusted ? 1 : 0,
|
|
69
|
-
now,
|
|
70
|
-
contact.last_message_at ?? null,
|
|
71
|
-
contact.tags ? JSON.stringify(contact.tags) : null,
|
|
72
|
-
);
|
|
73
|
-
return { ...contact, added_at: now };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
remove(did: string): boolean {
|
|
77
|
-
const result = this.store.getDb().prepare('DELETE FROM contacts WHERE did = ?').run(did);
|
|
78
|
-
return result.changes > 0;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
get(did: string): Contact | null {
|
|
82
|
-
const row = this.store.getDb().prepare('SELECT * FROM contacts WHERE did = ?').get(did) as ContactRow | undefined;
|
|
83
|
-
return row ? rowToContact(row) : null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
update(did: string, updates: Partial<Omit<Contact, 'did'>>): Contact | null {
|
|
87
|
-
const existing = this.get(did);
|
|
88
|
-
if (!existing) return null;
|
|
89
|
-
|
|
90
|
-
const fields: string[] = [];
|
|
91
|
-
const values: any[] = [];
|
|
92
|
-
|
|
93
|
-
if (updates.alias !== undefined) { fields.push('alias = ?'); values.push(updates.alias); }
|
|
94
|
-
if (updates.display_name !== undefined) { fields.push('display_name = ?'); values.push(updates.display_name); }
|
|
95
|
-
if (updates.notes !== undefined) { fields.push('notes = ?'); values.push(updates.notes); }
|
|
96
|
-
if (updates.conversation_id !== undefined) { fields.push('conversation_id = ?'); values.push(updates.conversation_id); }
|
|
97
|
-
if (updates.trusted !== undefined) { fields.push('trusted = ?'); values.push(updates.trusted ? 1 : 0); }
|
|
98
|
-
if (updates.last_message_at !== undefined) { fields.push('last_message_at = ?'); values.push(updates.last_message_at); }
|
|
99
|
-
if (updates.tags !== undefined) { fields.push('tags = ?'); values.push(JSON.stringify(updates.tags)); }
|
|
100
|
-
|
|
101
|
-
if (fields.length === 0) return existing;
|
|
102
|
-
|
|
103
|
-
values.push(did);
|
|
104
|
-
this.store.getDb().prepare(`UPDATE contacts SET ${fields.join(', ')} WHERE did = ?`).run(...values);
|
|
105
|
-
return this.get(did);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
list(opts?: { tag?: string; trusted?: boolean; limit?: number; offset?: number }): Contact[] {
|
|
109
|
-
const conditions: string[] = [];
|
|
110
|
-
const params: any[] = [];
|
|
111
|
-
|
|
112
|
-
if (opts?.trusted !== undefined) {
|
|
113
|
-
conditions.push('trusted = ?');
|
|
114
|
-
params.push(opts.trusted ? 1 : 0);
|
|
115
|
-
}
|
|
116
|
-
if (opts?.tag) {
|
|
117
|
-
conditions.push("tags LIKE ?");
|
|
118
|
-
params.push(`%"${opts.tag}"%`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
122
|
-
const limit = opts?.limit ? `LIMIT ${opts.limit}` : '';
|
|
123
|
-
const offset = opts?.offset ? `OFFSET ${opts.offset}` : '';
|
|
124
|
-
|
|
125
|
-
const rows = this.store.getDb()
|
|
126
|
-
.prepare(`SELECT * FROM contacts ${where} ORDER BY added_at DESC ${limit} ${offset}`)
|
|
127
|
-
.all(...params) as ContactRow[];
|
|
128
|
-
return rows.map(rowToContact);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
search(query: string): Contact[] {
|
|
132
|
-
const pattern = `%${query}%`;
|
|
133
|
-
const rows = this.store.getDb()
|
|
134
|
-
.prepare(`
|
|
135
|
-
SELECT * FROM contacts
|
|
136
|
-
WHERE did LIKE ? OR alias LIKE ? OR display_name LIKE ? OR notes LIKE ?
|
|
137
|
-
ORDER BY added_at DESC
|
|
138
|
-
`)
|
|
139
|
-
.all(pattern, pattern, pattern, pattern) as ContactRow[];
|
|
140
|
-
return rows.map(rowToContact);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export(format: 'json' | 'csv' = 'json'): string {
|
|
144
|
-
const contacts = this.list();
|
|
145
|
-
if (format === 'csv') {
|
|
146
|
-
const header = 'did,alias,display_name,notes,conversation_id,trusted,added_at,last_message_at,tags';
|
|
147
|
-
const rows = contacts.map(c =>
|
|
148
|
-
[c.did, c.alias ?? '', c.display_name ?? '', c.notes ?? '', c.conversation_id ?? '',
|
|
149
|
-
c.trusted, c.added_at, c.last_message_at ?? '', c.tags ? JSON.stringify(c.tags) : '']
|
|
150
|
-
.map(v => `"${String(v).replace(/"/g, '""')}"`)
|
|
151
|
-
.join(','),
|
|
152
|
-
);
|
|
153
|
-
return [header, ...rows].join('\n');
|
|
154
|
-
}
|
|
155
|
-
return JSON.stringify(contacts, null, 2);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
import(data: string, format: 'json' | 'csv' = 'json'): { imported: number; skipped: number } {
|
|
159
|
-
let imported = 0;
|
|
160
|
-
let skipped = 0;
|
|
161
|
-
|
|
162
|
-
if (format === 'json') {
|
|
163
|
-
const contacts: Contact[] = JSON.parse(data);
|
|
164
|
-
for (const c of contacts) {
|
|
165
|
-
try { this.add(c); imported++; } catch { skipped++; }
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
const lines = data.split('\n').filter(l => l.trim());
|
|
169
|
-
for (let i = 1; i < lines.length; i++) {
|
|
170
|
-
try {
|
|
171
|
-
const cols = parseCsvLine(lines[i]);
|
|
172
|
-
this.add({
|
|
173
|
-
did: cols[0],
|
|
174
|
-
alias: cols[1] || undefined,
|
|
175
|
-
display_name: cols[2] || undefined,
|
|
176
|
-
notes: cols[3] || undefined,
|
|
177
|
-
conversation_id: cols[4] || undefined,
|
|
178
|
-
trusted: cols[5] === '1' || cols[5] === 'true',
|
|
179
|
-
added_at: parseInt(cols[6]) || Date.now(),
|
|
180
|
-
last_message_at: cols[7] ? parseInt(cols[7]) : undefined,
|
|
181
|
-
tags: cols[8] ? JSON.parse(cols[8]) : undefined,
|
|
182
|
-
});
|
|
183
|
-
imported++;
|
|
184
|
-
} catch { skipped++; }
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return { imported, skipped };
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function parseCsvLine(line: string): string[] {
|
|
193
|
-
const result: string[] = [];
|
|
194
|
-
let current = '';
|
|
195
|
-
let inQuotes = false;
|
|
196
|
-
for (let i = 0; i < line.length; i++) {
|
|
197
|
-
const ch = line[i];
|
|
198
|
-
if (inQuotes) {
|
|
199
|
-
if (ch === '"' && line[i + 1] === '"') { current += '"'; i++; }
|
|
200
|
-
else if (ch === '"') { inQuotes = false; }
|
|
201
|
-
else { current += ch; }
|
|
202
|
-
} else {
|
|
203
|
-
if (ch === '"') { inQuotes = true; }
|
|
204
|
-
else if (ch === ',') { result.push(current); current = ''; }
|
|
205
|
-
else { current += ch; }
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
result.push(current);
|
|
209
|
-
return result;
|
|
210
|
-
}
|
package/src/history.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import type { LocalStore } from './store.js';
|
|
2
|
-
import type { PingAgentClient, FetchResponse } from './client.js';
|
|
3
|
-
import type { ApiResponse } from '@pingagent/schemas';
|
|
4
|
-
|
|
5
|
-
export interface StoredMessage {
|
|
6
|
-
conversation_id: string;
|
|
7
|
-
message_id: string;
|
|
8
|
-
seq?: number;
|
|
9
|
-
sender_did: string;
|
|
10
|
-
schema: string;
|
|
11
|
-
payload: any;
|
|
12
|
-
ts_ms: number;
|
|
13
|
-
direction: 'sent' | 'received';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface MessageRow {
|
|
17
|
-
id: number;
|
|
18
|
-
conversation_id: string;
|
|
19
|
-
message_id: string;
|
|
20
|
-
seq: number | null;
|
|
21
|
-
sender_did: string;
|
|
22
|
-
schema: string;
|
|
23
|
-
payload: string;
|
|
24
|
-
ts_ms: number;
|
|
25
|
-
direction: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface ConversationSummary {
|
|
29
|
-
conversation_id: string;
|
|
30
|
-
message_count: number;
|
|
31
|
-
last_message_at: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function rowToMessage(row: MessageRow): StoredMessage {
|
|
35
|
-
return {
|
|
36
|
-
conversation_id: row.conversation_id,
|
|
37
|
-
message_id: row.message_id,
|
|
38
|
-
seq: row.seq ?? undefined,
|
|
39
|
-
sender_did: row.sender_did,
|
|
40
|
-
schema: row.schema,
|
|
41
|
-
payload: JSON.parse(row.payload),
|
|
42
|
-
ts_ms: row.ts_ms,
|
|
43
|
-
direction: row.direction as 'sent' | 'received',
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class HistoryManager {
|
|
48
|
-
private store: LocalStore;
|
|
49
|
-
|
|
50
|
-
constructor(store: LocalStore) {
|
|
51
|
-
this.store = store;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
save(messages: StoredMessage[]): number {
|
|
55
|
-
const db = this.store.getDb();
|
|
56
|
-
const stmt = db.prepare(`
|
|
57
|
-
INSERT INTO messages (conversation_id, message_id, seq, sender_did, schema, payload, ts_ms, direction)
|
|
58
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
|
-
ON CONFLICT(message_id) DO UPDATE SET
|
|
60
|
-
seq = COALESCE(excluded.seq, messages.seq),
|
|
61
|
-
payload = excluded.payload,
|
|
62
|
-
ts_ms = excluded.ts_ms
|
|
63
|
-
`);
|
|
64
|
-
|
|
65
|
-
let count = 0;
|
|
66
|
-
const tx = db.transaction(() => {
|
|
67
|
-
for (const msg of messages) {
|
|
68
|
-
stmt.run(
|
|
69
|
-
msg.conversation_id,
|
|
70
|
-
msg.message_id,
|
|
71
|
-
msg.seq ?? null,
|
|
72
|
-
msg.sender_did,
|
|
73
|
-
msg.schema,
|
|
74
|
-
JSON.stringify(msg.payload),
|
|
75
|
-
msg.ts_ms,
|
|
76
|
-
msg.direction,
|
|
77
|
-
);
|
|
78
|
-
count++;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
tx();
|
|
82
|
-
return count;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
list(
|
|
86
|
-
conversationId: string,
|
|
87
|
-
opts?: { limit?: number; beforeSeq?: number; afterSeq?: number; beforeTsMs?: number; afterTsMs?: number },
|
|
88
|
-
): StoredMessage[] {
|
|
89
|
-
const conditions = ['conversation_id = ?'];
|
|
90
|
-
const params: any[] = [conversationId];
|
|
91
|
-
|
|
92
|
-
if (opts?.beforeSeq !== undefined) {
|
|
93
|
-
conditions.push('seq < ?');
|
|
94
|
-
params.push(opts.beforeSeq);
|
|
95
|
-
}
|
|
96
|
-
if (opts?.afterSeq !== undefined) {
|
|
97
|
-
conditions.push('seq > ?');
|
|
98
|
-
params.push(opts.afterSeq);
|
|
99
|
-
}
|
|
100
|
-
if (opts?.beforeTsMs !== undefined) {
|
|
101
|
-
conditions.push('ts_ms < ?');
|
|
102
|
-
params.push(opts.beforeTsMs);
|
|
103
|
-
}
|
|
104
|
-
if (opts?.afterTsMs !== undefined) {
|
|
105
|
-
conditions.push('ts_ms > ?');
|
|
106
|
-
params.push(opts.afterTsMs);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const limit = opts?.limit ?? 100;
|
|
110
|
-
const rows = this.store.getDb()
|
|
111
|
-
.prepare(`SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY COALESCE(seq, ts_ms) ASC LIMIT ?`)
|
|
112
|
-
.all(...params, limit) as MessageRow[];
|
|
113
|
-
return rows.map(rowToMessage);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* List the most recent N messages *before* a seq (paging older history).
|
|
118
|
-
* Only compares rows where seq is not null.
|
|
119
|
-
*/
|
|
120
|
-
listBeforeSeq(conversationId: string, beforeSeq: number, limit: number): StoredMessage[] {
|
|
121
|
-
const rows = this.store.getDb()
|
|
122
|
-
.prepare(
|
|
123
|
-
`SELECT * FROM messages
|
|
124
|
-
WHERE conversation_id = ? AND seq IS NOT NULL AND seq < ?
|
|
125
|
-
ORDER BY seq DESC
|
|
126
|
-
LIMIT ?`,
|
|
127
|
-
)
|
|
128
|
-
.all(conversationId, beforeSeq, limit) as MessageRow[];
|
|
129
|
-
return rows.map(rowToMessage).reverse();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* List the most recent N messages *before* a timestamp (paging older history).
|
|
134
|
-
*/
|
|
135
|
-
listBeforeTs(conversationId: string, beforeTsMs: number, limit: number): StoredMessage[] {
|
|
136
|
-
const rows = this.store.getDb()
|
|
137
|
-
.prepare(
|
|
138
|
-
`SELECT * FROM messages
|
|
139
|
-
WHERE conversation_id = ? AND ts_ms < ?
|
|
140
|
-
ORDER BY ts_ms DESC
|
|
141
|
-
LIMIT ?`,
|
|
142
|
-
)
|
|
143
|
-
.all(conversationId, beforeTsMs, limit) as MessageRow[];
|
|
144
|
-
return rows.map(rowToMessage).reverse();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Returns the N most recent messages (chronological order, oldest to newest of those N). */
|
|
148
|
-
listRecent(conversationId: string, limit: number): StoredMessage[] {
|
|
149
|
-
const rows = this.store.getDb()
|
|
150
|
-
.prepare(
|
|
151
|
-
`SELECT * FROM messages WHERE conversation_id = ? ORDER BY COALESCE(seq, ts_ms) DESC LIMIT ?`,
|
|
152
|
-
)
|
|
153
|
-
.all(conversationId, limit) as MessageRow[];
|
|
154
|
-
return rows.map(rowToMessage).reverse();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
search(query: string, opts?: { conversationId?: string; limit?: number }): StoredMessage[] {
|
|
158
|
-
const conditions = ['payload LIKE ?'];
|
|
159
|
-
const params: any[] = [`%${query}%`];
|
|
160
|
-
|
|
161
|
-
if (opts?.conversationId) {
|
|
162
|
-
conditions.push('conversation_id = ?');
|
|
163
|
-
params.push(opts.conversationId);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const limit = opts?.limit ?? 50;
|
|
167
|
-
const rows = this.store.getDb()
|
|
168
|
-
.prepare(`SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY ts_ms DESC LIMIT ?`)
|
|
169
|
-
.all(...params, limit) as MessageRow[];
|
|
170
|
-
return rows.map(rowToMessage);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
delete(conversationId: string): number {
|
|
174
|
-
const result = this.store.getDb().prepare('DELETE FROM messages WHERE conversation_id = ?').run(conversationId);
|
|
175
|
-
this.store.getDb().prepare('DELETE FROM sync_state WHERE conversation_id = ?').run(conversationId);
|
|
176
|
-
return result.changes;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
listConversations(): ConversationSummary[] {
|
|
180
|
-
const rows = this.store.getDb()
|
|
181
|
-
.prepare(`
|
|
182
|
-
SELECT conversation_id, COUNT(*) as message_count, MAX(ts_ms) as last_message_at
|
|
183
|
-
FROM messages
|
|
184
|
-
GROUP BY conversation_id
|
|
185
|
-
ORDER BY last_message_at DESC
|
|
186
|
-
`)
|
|
187
|
-
.all() as Array<{ conversation_id: string; message_count: number; last_message_at: number }>;
|
|
188
|
-
return rows;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
getLastSyncedSeq(conversationId: string): number {
|
|
192
|
-
const row = this.store.getDb()
|
|
193
|
-
.prepare('SELECT last_synced_seq FROM sync_state WHERE conversation_id = ?')
|
|
194
|
-
.get(conversationId) as { last_synced_seq: number } | undefined;
|
|
195
|
-
return row?.last_synced_seq ?? 0;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
setLastSyncedSeq(conversationId: string, seq: number): void {
|
|
199
|
-
this.store.getDb().prepare(`
|
|
200
|
-
INSERT INTO sync_state (conversation_id, last_synced_seq, last_synced_at)
|
|
201
|
-
VALUES (?, ?, ?)
|
|
202
|
-
ON CONFLICT(conversation_id) DO UPDATE SET last_synced_seq = excluded.last_synced_seq, last_synced_at = excluded.last_synced_at
|
|
203
|
-
`).run(conversationId, seq, Date.now());
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export(opts?: { conversationId?: string; format?: 'json' | 'csv' }): string {
|
|
207
|
-
const format = opts?.format ?? 'json';
|
|
208
|
-
let messages: StoredMessage[];
|
|
209
|
-
|
|
210
|
-
if (opts?.conversationId) {
|
|
211
|
-
messages = this.list(opts.conversationId, { limit: 100_000 });
|
|
212
|
-
} else {
|
|
213
|
-
const rows = this.store.getDb()
|
|
214
|
-
.prepare('SELECT * FROM messages ORDER BY ts_ms ASC')
|
|
215
|
-
.all() as MessageRow[];
|
|
216
|
-
messages = rows.map(rowToMessage);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (format === 'csv') {
|
|
220
|
-
const header = 'conversation_id,message_id,seq,sender_did,schema,payload,ts_ms,direction';
|
|
221
|
-
const lines = messages.map(m =>
|
|
222
|
-
[m.conversation_id, m.message_id, m.seq ?? '', m.sender_did, m.schema,
|
|
223
|
-
JSON.stringify(m.payload), m.ts_ms, m.direction]
|
|
224
|
-
.map(v => `"${String(v).replace(/"/g, '""')}"`)
|
|
225
|
-
.join(','),
|
|
226
|
-
);
|
|
227
|
-
return [header, ...lines].join('\n');
|
|
228
|
-
}
|
|
229
|
-
return JSON.stringify(messages, null, 2);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async syncFromServer(
|
|
233
|
-
client: PingAgentClient,
|
|
234
|
-
conversationId: string,
|
|
235
|
-
opts?: { full?: boolean },
|
|
236
|
-
): Promise<{ synced: number }> {
|
|
237
|
-
const sinceSeq = opts?.full ? 0 : this.getLastSyncedSeq(conversationId);
|
|
238
|
-
let totalSynced = 0;
|
|
239
|
-
let currentSeq = sinceSeq;
|
|
240
|
-
|
|
241
|
-
while (true) {
|
|
242
|
-
const res: ApiResponse<FetchResponse> = await client.fetchInbox(conversationId, {
|
|
243
|
-
sinceSeq: currentSeq,
|
|
244
|
-
limit: 50,
|
|
245
|
-
});
|
|
246
|
-
if (!res.ok || !res.data || res.data.messages.length === 0) break;
|
|
247
|
-
|
|
248
|
-
const messages: StoredMessage[] = res.data.messages.map((msg: any) => ({
|
|
249
|
-
conversation_id: conversationId,
|
|
250
|
-
message_id: msg.message_id,
|
|
251
|
-
seq: msg.seq,
|
|
252
|
-
sender_did: msg.sender_did,
|
|
253
|
-
schema: msg.schema,
|
|
254
|
-
payload: msg.payload,
|
|
255
|
-
ts_ms: msg.ts_ms,
|
|
256
|
-
direction: 'received' as const,
|
|
257
|
-
}));
|
|
258
|
-
|
|
259
|
-
this.save(messages);
|
|
260
|
-
totalSynced += messages.length;
|
|
261
|
-
currentSeq = res.data.next_since_seq;
|
|
262
|
-
this.setLastSyncedSeq(conversationId, currentSeq);
|
|
263
|
-
|
|
264
|
-
if (!res.data.has_more) break;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return { synced: totalSynced };
|
|
268
|
-
}
|
|
269
|
-
}
|
package/src/identity.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { generateIdentity as genId, type Identity } from '@pingagent/protocol';
|
|
4
|
-
import { getIdentityPath } from './paths.js';
|
|
5
|
-
|
|
6
|
-
interface StoredIdentity {
|
|
7
|
-
public_key: string;
|
|
8
|
-
private_key: string;
|
|
9
|
-
did: string;
|
|
10
|
-
device_id: string;
|
|
11
|
-
server_url?: string;
|
|
12
|
-
access_token?: string;
|
|
13
|
-
token_expires_at?: number;
|
|
14
|
-
mode?: string;
|
|
15
|
-
alias?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function toBase64(bytes: Uint8Array): string {
|
|
19
|
-
let binary = '';
|
|
20
|
-
for (const b of bytes) binary += String.fromCharCode(b);
|
|
21
|
-
return btoa(binary);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function fromBase64(b64: string): Uint8Array {
|
|
25
|
-
const binary = atob(b64);
|
|
26
|
-
const bytes = new Uint8Array(binary.length);
|
|
27
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
28
|
-
return bytes;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function generateIdentity(): Identity {
|
|
32
|
-
return genId();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function identityExists(identityPath?: string): boolean {
|
|
36
|
-
return fs.existsSync(getIdentityPath(identityPath));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function loadIdentity(identityPath?: string): Identity & { serverUrl?: string; accessToken?: string; tokenExpiresAt?: number; mode?: string } {
|
|
40
|
-
const p = getIdentityPath(identityPath);
|
|
41
|
-
const data: StoredIdentity = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
42
|
-
return {
|
|
43
|
-
publicKey: fromBase64(data.public_key),
|
|
44
|
-
privateKey: fromBase64(data.private_key),
|
|
45
|
-
did: data.did,
|
|
46
|
-
deviceId: data.device_id,
|
|
47
|
-
serverUrl: data.server_url,
|
|
48
|
-
accessToken: data.access_token,
|
|
49
|
-
tokenExpiresAt: data.token_expires_at,
|
|
50
|
-
mode: data.mode,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function saveIdentity(
|
|
55
|
-
identity: Identity,
|
|
56
|
-
opts?: { serverUrl?: string; accessToken?: string; tokenExpiresAt?: number; mode?: string; alias?: string },
|
|
57
|
-
identityPath?: string,
|
|
58
|
-
) {
|
|
59
|
-
const p = getIdentityPath(identityPath);
|
|
60
|
-
const dir = path.dirname(p);
|
|
61
|
-
if (!fs.existsSync(dir)) {
|
|
62
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const stored: StoredIdentity = {
|
|
66
|
-
public_key: toBase64(identity.publicKey),
|
|
67
|
-
private_key: toBase64(identity.privateKey),
|
|
68
|
-
did: identity.did,
|
|
69
|
-
device_id: identity.deviceId,
|
|
70
|
-
server_url: opts?.serverUrl,
|
|
71
|
-
access_token: opts?.accessToken,
|
|
72
|
-
token_expires_at: opts?.tokenExpiresAt,
|
|
73
|
-
mode: opts?.mode,
|
|
74
|
-
alias: opts?.alias,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
fs.writeFileSync(p, JSON.stringify(stored, null, 2), { mode: 0o600 });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function updateStoredToken(accessToken: string, expiresAt: number, identityPath?: string) {
|
|
81
|
-
const p = getIdentityPath(identityPath);
|
|
82
|
-
const data: StoredIdentity = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
83
|
-
data.access_token = accessToken;
|
|
84
|
-
data.token_expires_at = expiresAt;
|
|
85
|
-
fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
86
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
PingAgentClient,
|
|
3
|
-
type ClientOptions,
|
|
4
|
-
type TaskResult,
|
|
5
|
-
type SendResponse,
|
|
6
|
-
type FetchResponse,
|
|
7
|
-
type SubscriptionResponse,
|
|
8
|
-
type SubscriptionUsage,
|
|
9
|
-
type ConversationEntry,
|
|
10
|
-
type ConversationListResponse,
|
|
11
|
-
type AgentProfile,
|
|
12
|
-
type DirectoryBrowseResponse,
|
|
13
|
-
type FeedPost,
|
|
14
|
-
type FeedPublicResponse,
|
|
15
|
-
type FeedByDidResponse,
|
|
16
|
-
} from './client.js';
|
|
17
|
-
export { generateIdentity, loadIdentity, saveIdentity, identityExists, updateStoredToken } from './identity.js';
|
|
18
|
-
export { ensureTokenValid } from './auth.js';
|
|
19
|
-
export { getIdentityPath, getStorePath, getRootDir, getProfile } from './paths.js';
|
|
20
|
-
export { HttpTransport, type TransportOptions } from './transport.js';
|
|
21
|
-
export { LocalStore } from './store.js';
|
|
22
|
-
export { ContactManager, type Contact } from './contacts.js';
|
|
23
|
-
export { HistoryManager, type StoredMessage } from './history.js';
|
|
24
|
-
export { A2AAdapter, type A2AAdapterOptions, type A2ATaskResult } from './a2a-adapter.js';
|
|
25
|
-
export { WsSubscription, type WsSubscriptionOptions } from './ws-subscription.js';
|
package/src/paths.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Shared path resolution for PingAgent identity and local store.
|
|
6
|
-
*
|
|
7
|
-
* Design:
|
|
8
|
-
* - ROOT_DIR: process.env.PINGAGENT_ROOT_DIR ?? ~/.pingagent
|
|
9
|
-
* - PROFILE: optional process.env.PINGAGENT_PROFILE
|
|
10
|
-
* - If PROFILE is set, identity and store live under:
|
|
11
|
-
* <ROOT_DIR>/profiles/<PROFILE>/
|
|
12
|
-
* Otherwise, they live directly under ROOT_DIR (backwards compatible).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const DEFAULT_ROOT_DIR = path.join(os.homedir(), '.pingagent');
|
|
16
|
-
|
|
17
|
-
export function getRootDir(): string {
|
|
18
|
-
return process.env.PINGAGENT_ROOT_DIR || DEFAULT_ROOT_DIR;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getProfile(): string | undefined {
|
|
22
|
-
const p = process.env.PINGAGENT_PROFILE;
|
|
23
|
-
if (!p) return undefined;
|
|
24
|
-
return p.trim() || undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getIdentityPath(explicitPath?: string): string {
|
|
28
|
-
const envPath = process.env.PINGAGENT_IDENTITY_PATH?.trim();
|
|
29
|
-
if (explicitPath) return explicitPath;
|
|
30
|
-
if (envPath) return path.resolve(envPath.replace(/^~(?=\/|$)/, os.homedir()));
|
|
31
|
-
|
|
32
|
-
const root = getRootDir();
|
|
33
|
-
const profile = getProfile();
|
|
34
|
-
if (!profile) {
|
|
35
|
-
return path.join(root, 'identity.json');
|
|
36
|
-
}
|
|
37
|
-
return path.join(root, 'profiles', profile, 'identity.json');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getStorePath(explicitPath?: string): string {
|
|
41
|
-
const envPath = process.env.PINGAGENT_STORE_PATH?.trim();
|
|
42
|
-
if (explicitPath) return explicitPath;
|
|
43
|
-
if (envPath) return path.resolve(envPath.replace(/^~(?=\/|$)/, os.homedir()));
|
|
44
|
-
|
|
45
|
-
const root = getRootDir();
|
|
46
|
-
const profile = getProfile();
|
|
47
|
-
if (!profile) {
|
|
48
|
-
return path.join(root, 'store.db');
|
|
49
|
-
}
|
|
50
|
-
return path.join(root, 'profiles', profile, 'store.db');
|
|
51
|
-
}
|
|
52
|
-
|
package/src/store.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import Database from 'better-sqlite3';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import { getStorePath } from './paths.js';
|
|
5
|
-
|
|
6
|
-
const SCHEMA_SQL = `
|
|
7
|
-
CREATE TABLE IF NOT EXISTS contacts (
|
|
8
|
-
did TEXT PRIMARY KEY,
|
|
9
|
-
alias TEXT,
|
|
10
|
-
display_name TEXT,
|
|
11
|
-
notes TEXT,
|
|
12
|
-
conversation_id TEXT,
|
|
13
|
-
trusted INTEGER NOT NULL DEFAULT 0,
|
|
14
|
-
added_at INTEGER NOT NULL,
|
|
15
|
-
last_message_at INTEGER,
|
|
16
|
-
tags TEXT
|
|
17
|
-
);
|
|
18
|
-
CREATE INDEX IF NOT EXISTS idx_contacts_alias ON contacts(alias);
|
|
19
|
-
|
|
20
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
21
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
-
conversation_id TEXT NOT NULL,
|
|
23
|
-
message_id TEXT NOT NULL UNIQUE,
|
|
24
|
-
seq INTEGER,
|
|
25
|
-
sender_did TEXT NOT NULL,
|
|
26
|
-
schema TEXT NOT NULL,
|
|
27
|
-
payload TEXT NOT NULL,
|
|
28
|
-
ts_ms INTEGER NOT NULL,
|
|
29
|
-
direction TEXT NOT NULL CHECK(direction IN ('sent', 'received'))
|
|
30
|
-
);
|
|
31
|
-
CREATE INDEX IF NOT EXISTS idx_messages_conv_seq ON messages(conversation_id, seq);
|
|
32
|
-
CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(ts_ms);
|
|
33
|
-
|
|
34
|
-
CREATE TABLE IF NOT EXISTS sync_state (
|
|
35
|
-
conversation_id TEXT PRIMARY KEY,
|
|
36
|
-
last_synced_seq INTEGER NOT NULL DEFAULT 0,
|
|
37
|
-
last_synced_at INTEGER
|
|
38
|
-
);
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
export class LocalStore {
|
|
42
|
-
private db: Database.Database;
|
|
43
|
-
|
|
44
|
-
constructor(dbPath?: string) {
|
|
45
|
-
const p = getStorePath(dbPath);
|
|
46
|
-
const dir = path.dirname(p);
|
|
47
|
-
if (!fs.existsSync(dir)) {
|
|
48
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
49
|
-
}
|
|
50
|
-
this.db = new Database(p);
|
|
51
|
-
this.db.pragma('journal_mode = WAL');
|
|
52
|
-
this.db.exec(SCHEMA_SQL);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
getDb(): Database.Database {
|
|
56
|
-
return this.db;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
close(): void {
|
|
60
|
-
this.db.close();
|
|
61
|
-
}
|
|
62
|
-
}
|