@xmtp/convos-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +572 -0
- package/bin/dev.js +4 -0
- package/bin/run.js +4 -0
- package/dist/baseCommand.d.ts +46 -0
- package/dist/baseCommand.js +171 -0
- package/dist/commands/agent/serve.d.ts +67 -0
- package/dist/commands/agent/serve.js +662 -0
- package/dist/commands/conversation/add-members.d.ts +19 -0
- package/dist/commands/conversation/add-members.js +39 -0
- package/dist/commands/conversation/consent-state.d.ts +18 -0
- package/dist/commands/conversation/consent-state.js +24 -0
- package/dist/commands/conversation/download-attachment.d.ts +28 -0
- package/dist/commands/conversation/download-attachment.js +164 -0
- package/dist/commands/conversation/explode.d.ts +24 -0
- package/dist/commands/conversation/explode.js +156 -0
- package/dist/commands/conversation/info.d.ts +22 -0
- package/dist/commands/conversation/info.js +79 -0
- package/dist/commands/conversation/invite.d.ts +26 -0
- package/dist/commands/conversation/invite.js +137 -0
- package/dist/commands/conversation/lock.d.ts +24 -0
- package/dist/commands/conversation/lock.js +98 -0
- package/dist/commands/conversation/members.d.ts +22 -0
- package/dist/commands/conversation/members.js +39 -0
- package/dist/commands/conversation/messages.d.ts +31 -0
- package/dist/commands/conversation/messages.js +141 -0
- package/dist/commands/conversation/permissions.d.ts +18 -0
- package/dist/commands/conversation/permissions.js +33 -0
- package/dist/commands/conversation/profiles.d.ts +22 -0
- package/dist/commands/conversation/profiles.js +80 -0
- package/dist/commands/conversation/remove-members.d.ts +19 -0
- package/dist/commands/conversation/remove-members.js +36 -0
- package/dist/commands/conversation/send-attachment.d.ts +30 -0
- package/dist/commands/conversation/send-attachment.js +187 -0
- package/dist/commands/conversation/send-reaction.d.ts +21 -0
- package/dist/commands/conversation/send-reaction.js +38 -0
- package/dist/commands/conversation/send-remote-attachment.d.ts +30 -0
- package/dist/commands/conversation/send-remote-attachment.js +96 -0
- package/dist/commands/conversation/send-reply.d.ts +32 -0
- package/dist/commands/conversation/send-reply.js +170 -0
- package/dist/commands/conversation/send-text.d.ts +24 -0
- package/dist/commands/conversation/send-text.js +64 -0
- package/dist/commands/conversation/stream.d.ts +24 -0
- package/dist/commands/conversation/stream.js +81 -0
- package/dist/commands/conversation/sync.d.ts +18 -0
- package/dist/commands/conversation/sync.js +25 -0
- package/dist/commands/conversation/update-consent.d.ts +19 -0
- package/dist/commands/conversation/update-consent.js +35 -0
- package/dist/commands/conversation/update-description.d.ts +19 -0
- package/dist/commands/conversation/update-description.js +28 -0
- package/dist/commands/conversation/update-name.d.ts +19 -0
- package/dist/commands/conversation/update-name.js +29 -0
- package/dist/commands/conversation/update-profile.d.ts +24 -0
- package/dist/commands/conversation/update-profile.js +97 -0
- package/dist/commands/conversations/create.d.ts +26 -0
- package/dist/commands/conversations/create.js +165 -0
- package/dist/commands/conversations/join.d.ts +27 -0
- package/dist/commands/conversations/join.js +232 -0
- package/dist/commands/conversations/list.d.ts +20 -0
- package/dist/commands/conversations/list.js +109 -0
- package/dist/commands/conversations/process-join-requests.d.ts +26 -0
- package/dist/commands/conversations/process-join-requests.js +261 -0
- package/dist/commands/conversations/sync.d.ts +19 -0
- package/dist/commands/conversations/sync.js +50 -0
- package/dist/commands/identity/create.d.ts +21 -0
- package/dist/commands/identity/create.js +56 -0
- package/dist/commands/identity/info.d.ts +22 -0
- package/dist/commands/identity/info.js +63 -0
- package/dist/commands/identity/list.d.ts +19 -0
- package/dist/commands/identity/list.js +59 -0
- package/dist/commands/identity/remove.d.ts +23 -0
- package/dist/commands/identity/remove.js +51 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.js +91 -0
- package/dist/commands/reset.d.ts +17 -0
- package/dist/commands/reset.js +93 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +31 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +15 -0
- package/dist/utils/client.d.ts +8 -0
- package/dist/utils/client.js +58 -0
- package/dist/utils/config.d.ts +15 -0
- package/dist/utils/config.js +1 -0
- package/dist/utils/identities.d.ts +49 -0
- package/dist/utils/identities.js +92 -0
- package/dist/utils/invite.d.ts +70 -0
- package/dist/utils/invite.js +339 -0
- package/dist/utils/metadata.d.ts +39 -0
- package/dist/utils/metadata.js +180 -0
- package/dist/utils/mime.d.ts +2 -0
- package/dist/utils/mime.js +42 -0
- package/dist/utils/random.d.ts +5 -0
- package/dist/utils/random.js +19 -0
- package/dist/utils/upload.d.ts +14 -0
- package/dist/utils/upload.js +51 -0
- package/dist/utils/xmtp.d.ts +45 -0
- package/dist/utils/xmtp.js +298 -0
- package/oclif.manifest.json +5562 -0
- package/package.json +124 -0
- package/skills/convos-cli/SKILL.md +588 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import { getAccountAddress, isGroup } from "../../utils/xmtp.js";
|
|
3
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
4
|
+
import { createClientForIdentity } from "../../utils/client.js";
|
|
5
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
6
|
+
export default class ConversationsList extends ConvosBaseCommand {
|
|
7
|
+
static description = `List all Convos conversations.
|
|
8
|
+
|
|
9
|
+
Lists conversations across all per-conversation identities.
|
|
10
|
+
Each conversation is associated with its own XMTP identity.
|
|
11
|
+
|
|
12
|
+
Use --sync to fetch the latest state from the network.`;
|
|
13
|
+
static examples = [
|
|
14
|
+
{
|
|
15
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
16
|
+
description: "List all conversations",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
command: "<%= config.bin %> <%= command.id %> --sync",
|
|
20
|
+
description: "Sync from network then list",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
command: "<%= config.bin %> <%= command.id %> --json",
|
|
24
|
+
description: "Output as JSON",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
static flags = {
|
|
28
|
+
...ConvosBaseCommand.baseFlags,
|
|
29
|
+
sync: Flags.boolean({
|
|
30
|
+
description: "Sync each identity from network before listing",
|
|
31
|
+
default: false,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { flags } = await this.parse(ConversationsList);
|
|
36
|
+
const config = this.getConvosConfig();
|
|
37
|
+
const store = createIdentityStore();
|
|
38
|
+
const allLinked = store.list().filter((i) => i.conversationId);
|
|
39
|
+
// Deduplicate: only use the oldest identity per conversation ID
|
|
40
|
+
const seen = new Map();
|
|
41
|
+
const identities = [];
|
|
42
|
+
// list() is sorted newest-first, so iterate in reverse to pick oldest first
|
|
43
|
+
for (let i = allLinked.length - 1; i >= 0; i--) {
|
|
44
|
+
const identity = allLinked[i];
|
|
45
|
+
const convId = identity.conversationId;
|
|
46
|
+
if (!seen.has(convId)) {
|
|
47
|
+
seen.set(convId, true);
|
|
48
|
+
identities.push(identity);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Warn about duplicate identities for the same conversation
|
|
52
|
+
const duplicates = new Map();
|
|
53
|
+
for (const identity of allLinked) {
|
|
54
|
+
const convId = identity.conversationId;
|
|
55
|
+
duplicates.set(convId, (duplicates.get(convId) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
for (const [convId, count] of duplicates) {
|
|
58
|
+
if (count > 1) {
|
|
59
|
+
this.warn(`${count} identities found for conversation ${convId}. ` +
|
|
60
|
+
`Run 'convos identity list' to review and remove duplicates.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (identities.length === 0) {
|
|
64
|
+
this.output({
|
|
65
|
+
message: "No conversations found. Create one with: convos conversations create",
|
|
66
|
+
count: 0,
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const output = [];
|
|
71
|
+
for (const identity of identities) {
|
|
72
|
+
try {
|
|
73
|
+
const client = await createClientForIdentity(identity, config);
|
|
74
|
+
if (flags.sync) {
|
|
75
|
+
await client.conversations.sync();
|
|
76
|
+
}
|
|
77
|
+
const conversations = await client.conversations.list();
|
|
78
|
+
for (const conversation of conversations) {
|
|
79
|
+
const members = await conversation.members();
|
|
80
|
+
let isLocked = false;
|
|
81
|
+
if (isGroup(conversation)) {
|
|
82
|
+
const { policySet } = conversation.permissions();
|
|
83
|
+
isLocked = policySet.addMemberPolicy === 1 /* PermissionPolicy.Deny */;
|
|
84
|
+
}
|
|
85
|
+
output.push({
|
|
86
|
+
conversationId: conversation.id,
|
|
87
|
+
identityId: identity.id,
|
|
88
|
+
label: identity.label ?? "",
|
|
89
|
+
profileName: identity.profileName ?? "",
|
|
90
|
+
address: getAccountAddress(identity.walletKey),
|
|
91
|
+
createdAt: conversation.createdAt.toISOString(),
|
|
92
|
+
memberCount: members.length,
|
|
93
|
+
isActive: conversation.isActive,
|
|
94
|
+
isLocked,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
output.push({
|
|
100
|
+
conversationId: identity.conversationId,
|
|
101
|
+
identityId: identity.id,
|
|
102
|
+
label: identity.label ?? "",
|
|
103
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.output(output);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class ProcessJoinRequests extends ConvosBaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
watch: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
conversation: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Attempt to process a single DM message as a join request.
|
|
22
|
+
* Returns the result if successfully processed, or undefined.
|
|
23
|
+
*/
|
|
24
|
+
private processMessage;
|
|
25
|
+
run(): Promise<void>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import { requireGroup } from "../../utils/xmtp.js";
|
|
3
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
4
|
+
import { createClientForIdentity } from "../../utils/client.js";
|
|
5
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
6
|
+
import { parseInvite, decryptConversationToken, verifyInvite, verifyInviteSignature } from "../../utils/invite.js";
|
|
7
|
+
import { parseAppData } from "../../utils/metadata.js";
|
|
8
|
+
export default class ProcessJoinRequests extends ConvosBaseCommand {
|
|
9
|
+
static description = `Process pending join requests for all conversations.
|
|
10
|
+
|
|
11
|
+
Scans DMs for each identity looking for join requests (text messages
|
|
12
|
+
containing signed invite slugs). When a valid join request is found:
|
|
13
|
+
|
|
14
|
+
1. Verify the invite signature
|
|
15
|
+
2. Decrypt the conversation token to get the conversation ID
|
|
16
|
+
3. Verify the creator inbox ID matches
|
|
17
|
+
4. Add the requester to the conversation
|
|
18
|
+
5. Block invalid/spam requests
|
|
19
|
+
|
|
20
|
+
Without --watch, processes all pending DMs and exits.
|
|
21
|
+
With --watch, streams DM messages in real-time and processes
|
|
22
|
+
join requests as they arrive (recommended for always-on usage).`;
|
|
23
|
+
static examples = [
|
|
24
|
+
{
|
|
25
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
26
|
+
description: "Process all pending join requests",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
command: "<%= config.bin %> <%= command.id %> --watch",
|
|
30
|
+
description: "Continuously stream and process join requests in real-time",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
command: "<%= config.bin %> <%= command.id %> --conversation <id>",
|
|
34
|
+
description: "Process join requests for a specific conversation only",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
static flags = {
|
|
38
|
+
...ConvosBaseCommand.baseFlags,
|
|
39
|
+
watch: Flags.boolean({
|
|
40
|
+
description: "Stream DM messages in real-time and process join requests as they arrive",
|
|
41
|
+
default: false,
|
|
42
|
+
}),
|
|
43
|
+
conversation: Flags.string({
|
|
44
|
+
description: "Only process requests for this conversation ID",
|
|
45
|
+
helpValue: "<id>",
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Attempt to process a single DM message as a join request.
|
|
50
|
+
* Returns the result if successfully processed, or undefined.
|
|
51
|
+
*/
|
|
52
|
+
async processMessage(message, client, identity) {
|
|
53
|
+
// Skip our own messages
|
|
54
|
+
if (message.senderInboxId === client.inboxId)
|
|
55
|
+
return;
|
|
56
|
+
// Try to parse as invite
|
|
57
|
+
const text = typeof message.content === "string" ? message.content : null;
|
|
58
|
+
if (!text)
|
|
59
|
+
return;
|
|
60
|
+
let invite;
|
|
61
|
+
try {
|
|
62
|
+
invite = parseInvite(text);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return; // Not an invite, skip
|
|
66
|
+
}
|
|
67
|
+
// Look up the DM conversation to manage consent
|
|
68
|
+
const dmConversation = await client.conversations.getConversationById(message.conversationId);
|
|
69
|
+
// Step 1: Verify structural validity and signature recoverability
|
|
70
|
+
if (!(await verifyInvite(invite))) {
|
|
71
|
+
this.log(`Invalid invite structure/signature from ${message.senderInboxId} — blocking DM`);
|
|
72
|
+
if (dmConversation)
|
|
73
|
+
dmConversation.updateConsentState(2 /* ConsentState.Denied */);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Step 2: Verify the signature was made by this identity's wallet key
|
|
77
|
+
if (!(await verifyInviteSignature(invite, identity.walletKey))) {
|
|
78
|
+
this.log(`Invite signature doesn't match our wallet key — blocking DM`);
|
|
79
|
+
if (dmConversation)
|
|
80
|
+
dmConversation.updateConsentState(2 /* ConsentState.Denied */);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Step 3: Verify creator inbox ID matches us
|
|
84
|
+
if (invite.creatorInboxId !== client.inboxId) {
|
|
85
|
+
this.log(`Invite not for this inbox — blocking DM`);
|
|
86
|
+
if (dmConversation)
|
|
87
|
+
dmConversation.updateConsentState(2 /* ConsentState.Denied */);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Step 4: Check expiration
|
|
91
|
+
if (invite.expiresAt && invite.expiresAt < new Date()) {
|
|
92
|
+
this.log(`Invite expired — skipping`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Step 5: Decrypt conversation token
|
|
96
|
+
let conversationId;
|
|
97
|
+
try {
|
|
98
|
+
conversationId = decryptConversationToken(invite.conversationToken, client.inboxId, Buffer.from(identity.walletKey.replace("0x", ""), "hex"));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
this.log(`Failed to decrypt conversation token — blocking DM`);
|
|
102
|
+
if (dmConversation)
|
|
103
|
+
dmConversation.updateConsentState(2 /* ConsentState.Denied */);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Step 6: Verify conversation exists
|
|
107
|
+
const conversation = await client.conversations.getConversationById(conversationId);
|
|
108
|
+
if (!conversation) {
|
|
109
|
+
this.log(`Conversation ${conversationId} not found — skipping`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const group = requireGroup(conversation);
|
|
113
|
+
// Step 7: Verify the invite tag matches the group's current tag
|
|
114
|
+
try {
|
|
115
|
+
const appData = group.appData ?? "";
|
|
116
|
+
const metadata = parseAppData(appData);
|
|
117
|
+
if (metadata.tag && invite.tag !== metadata.tag) {
|
|
118
|
+
this.log(`Invite tag mismatch (invite=${invite.tag}, group=${metadata.tag}) — rejecting`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// If we can't read appData, skip tag check but continue
|
|
124
|
+
}
|
|
125
|
+
// Step 8: Add the requester
|
|
126
|
+
this.log(`Adding ${message.senderInboxId} to conversation ${conversationId}`);
|
|
127
|
+
await group.addMembers([message.senderInboxId]);
|
|
128
|
+
// Mark DM as allowed so we don't re-process
|
|
129
|
+
if (dmConversation)
|
|
130
|
+
dmConversation.updateConsentState(1 /* ConsentState.Allowed */);
|
|
131
|
+
return {
|
|
132
|
+
conversationId,
|
|
133
|
+
joinerInboxId: message.senderInboxId,
|
|
134
|
+
identityId: identity.id,
|
|
135
|
+
tag: invite.tag,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async run() {
|
|
139
|
+
const { flags } = await this.parse(ProcessJoinRequests);
|
|
140
|
+
const config = this.getConvosConfig();
|
|
141
|
+
const store = createIdentityStore();
|
|
142
|
+
const identities = flags.conversation
|
|
143
|
+
? [store.getByConversationId(flags.conversation)].filter(Boolean)
|
|
144
|
+
: store.list().filter((i) => i.conversationId);
|
|
145
|
+
if (identities.length === 0) {
|
|
146
|
+
this.output({ message: "No linked identities found.", processed: 0 });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// ─── Batch mode: process all pending DMs ───
|
|
150
|
+
const allResults = [];
|
|
151
|
+
// Build clients for all identities (needed for both batch and watch)
|
|
152
|
+
const clientMap = new Map();
|
|
153
|
+
for (const identity of identities) {
|
|
154
|
+
if (!identity)
|
|
155
|
+
continue;
|
|
156
|
+
try {
|
|
157
|
+
const client = await createClientForIdentity(identity, config);
|
|
158
|
+
clientMap.set(identity.id, { client, identity });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
this.log(`Error initializing identity ${identity.id}: ${error instanceof Error ? error.message : "unknown"}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const [, { client, identity }] of clientMap) {
|
|
165
|
+
try {
|
|
166
|
+
await client.conversations.sync();
|
|
167
|
+
// List DMs with unknown consent (potential join requests)
|
|
168
|
+
const dms = await client.conversations.list({
|
|
169
|
+
conversationType: 0 /* ConversationType.Dm */,
|
|
170
|
+
consentStates: [0 /* ConsentState.Unknown */],
|
|
171
|
+
});
|
|
172
|
+
for (const dm of dms) {
|
|
173
|
+
try {
|
|
174
|
+
await dm.sync();
|
|
175
|
+
const messages = await dm.messages({ limit: 10 });
|
|
176
|
+
for (const message of messages) {
|
|
177
|
+
try {
|
|
178
|
+
const result = await this.processMessage(message, client, identity);
|
|
179
|
+
if (result) {
|
|
180
|
+
allResults.push(result);
|
|
181
|
+
break; // Only process first valid invite per DM
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.log(`Error processing message: ${error instanceof Error ? error.message : "unknown"}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.log(`Error processing DM: ${error instanceof Error ? error.message : "unknown"}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
this.log(`Error with identity ${identity.id}: ${error instanceof Error ? error.message : "unknown"}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!flags.watch) {
|
|
199
|
+
this.output({
|
|
200
|
+
processed: allResults.length,
|
|
201
|
+
results: allResults,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// ─── Watch mode: stream DM messages in real-time ───
|
|
206
|
+
if (allResults.length > 0) {
|
|
207
|
+
this.log(`\nProcessed ${allResults.length} pending request(s).`);
|
|
208
|
+
}
|
|
209
|
+
this.log("\nStreaming DM messages for join requests (Ctrl+C to stop)...\n");
|
|
210
|
+
// Build an inboxId → { client, identity } lookup so we can route
|
|
211
|
+
// streamed messages to the correct identity
|
|
212
|
+
const inboxMap = new Map();
|
|
213
|
+
for (const [, entry] of clientMap) {
|
|
214
|
+
inboxMap.set(entry.client.inboxId, entry);
|
|
215
|
+
}
|
|
216
|
+
// Start a DM message stream for each identity
|
|
217
|
+
const streams = [];
|
|
218
|
+
for (const [, { client, identity }] of clientMap) {
|
|
219
|
+
try {
|
|
220
|
+
const stream = await client.conversations.streamAllDmMessages();
|
|
221
|
+
streams.push(stream);
|
|
222
|
+
// Process messages from this stream in the background
|
|
223
|
+
(async () => {
|
|
224
|
+
try {
|
|
225
|
+
for await (const message of stream) {
|
|
226
|
+
try {
|
|
227
|
+
const result = await this.processMessage(message, client, identity);
|
|
228
|
+
if (result) {
|
|
229
|
+
this.streamOutput({
|
|
230
|
+
event: "join_request_accepted",
|
|
231
|
+
...result,
|
|
232
|
+
timestamp: new Date().toISOString(),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
this.log(`Error processing streamed message: ${error instanceof Error ? error.message : "unknown"}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this.log(`Stream ended for identity ${identity.id}: ${error instanceof Error ? error.message : "unknown"}`);
|
|
243
|
+
}
|
|
244
|
+
})();
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.log(`Failed to start stream for identity ${identity.id}: ${error instanceof Error ? error.message : "unknown"}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Keep running until interrupted
|
|
251
|
+
await new Promise((resolve) => {
|
|
252
|
+
process.on("SIGINT", () => {
|
|
253
|
+
this.log("\nStopping streams...");
|
|
254
|
+
for (const stream of streams) {
|
|
255
|
+
void stream.return();
|
|
256
|
+
}
|
|
257
|
+
resolve();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class ConversationsSync extends ConvosBaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
import { createClientForIdentity } from "../../utils/client.js";
|
|
3
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
4
|
+
export default class ConversationsSync extends ConvosBaseCommand {
|
|
5
|
+
static description = `Sync all conversations from the network.
|
|
6
|
+
|
|
7
|
+
Synchronizes every linked identity with the XMTP network.
|
|
8
|
+
Fetches new messages and membership changes for all conversations.`;
|
|
9
|
+
static examples = [
|
|
10
|
+
{
|
|
11
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
12
|
+
description: "Sync all conversations",
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...ConvosBaseCommand.baseFlags,
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const config = this.getConvosConfig();
|
|
20
|
+
const store = createIdentityStore();
|
|
21
|
+
const identities = store.list().filter((i) => i.conversationId);
|
|
22
|
+
const results = [];
|
|
23
|
+
for (const identity of identities) {
|
|
24
|
+
try {
|
|
25
|
+
const client = await createClientForIdentity(identity, config);
|
|
26
|
+
await client.conversations.sync();
|
|
27
|
+
results.push({
|
|
28
|
+
identityId: identity.id,
|
|
29
|
+
conversationId: identity.conversationId,
|
|
30
|
+
label: identity.label ?? "",
|
|
31
|
+
success: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
results.push({
|
|
36
|
+
identityId: identity.id,
|
|
37
|
+
conversationId: identity.conversationId,
|
|
38
|
+
label: identity.label ?? "",
|
|
39
|
+
success: false,
|
|
40
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.output({
|
|
45
|
+
synced: results.filter((r) => r.success).length,
|
|
46
|
+
failed: results.filter((r) => !r.success).length,
|
|
47
|
+
results,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class IdentityCreate extends ConvosBaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"profile-name": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
};
|
|
20
|
+
run(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import { getAccountAddress } from "../../utils/xmtp.js";
|
|
3
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
4
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
5
|
+
export default class IdentityCreate extends ConvosBaseCommand {
|
|
6
|
+
static description = `Create a new Convos identity.
|
|
7
|
+
|
|
8
|
+
Generates a new wallet key and database encryption key for use with
|
|
9
|
+
a single conversation. Each conversation gets its own identity for
|
|
10
|
+
maximum privacy (per ADR 002).
|
|
11
|
+
|
|
12
|
+
You can optionally set a label (local-only) and profile name (shared
|
|
13
|
+
with conversation members per ADR 005).`;
|
|
14
|
+
static examples = [
|
|
15
|
+
{
|
|
16
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
17
|
+
description: "Create a new identity",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
command: '<%= config.bin %> <%= command.id %> --label "Work Chat"',
|
|
21
|
+
description: "Create with a local label",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: '<%= config.bin %> <%= command.id %> --label "Team" --profile-name "Alice"',
|
|
25
|
+
description: "Create with label and profile name",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
static flags = {
|
|
29
|
+
...ConvosBaseCommand.baseFlags,
|
|
30
|
+
label: Flags.string({
|
|
31
|
+
description: "Local label for this identity (not shared)",
|
|
32
|
+
helpValue: "<label>",
|
|
33
|
+
}),
|
|
34
|
+
"profile-name": Flags.string({
|
|
35
|
+
description: "Profile display name (shared with conversation members)",
|
|
36
|
+
helpValue: "<name>",
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
async run() {
|
|
40
|
+
const { flags } = await this.parse(IdentityCreate);
|
|
41
|
+
const store = createIdentityStore();
|
|
42
|
+
const identity = store.create({
|
|
43
|
+
label: flags.label,
|
|
44
|
+
profileName: flags["profile-name"],
|
|
45
|
+
});
|
|
46
|
+
this.output({
|
|
47
|
+
id: identity.id,
|
|
48
|
+
address: getAccountAddress(identity.walletKey),
|
|
49
|
+
label: identity.label ?? "",
|
|
50
|
+
profileName: identity.profileName ?? "",
|
|
51
|
+
createdAt: identity.createdAt,
|
|
52
|
+
message: "Identity created. Use it with: convos conversations create --identity " +
|
|
53
|
+
identity.id,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class IdentityInfo extends ConvosBaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
static args: {
|
|
9
|
+
id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static flags: {
|
|
12
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
};
|
|
21
|
+
run(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { getAccountAddress, formatSections } from "../../utils/xmtp.js";
|
|
3
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
4
|
+
import { createClientForIdentity } from "../../utils/client.js";
|
|
5
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
6
|
+
export default class IdentityInfo extends ConvosBaseCommand {
|
|
7
|
+
static description = `Display detailed information about an identity.
|
|
8
|
+
|
|
9
|
+
Shows wallet address, XMTP inbox ID, linked conversation, profile
|
|
10
|
+
settings, and XMTP client details. Creates and registers the XMTP
|
|
11
|
+
client if not yet registered.`;
|
|
12
|
+
static examples = [
|
|
13
|
+
{
|
|
14
|
+
command: "<%= config.bin %> <%= command.id %> <identity-id>",
|
|
15
|
+
description: "Show identity details",
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
static args = {
|
|
19
|
+
id: Args.string({
|
|
20
|
+
description: "The identity ID",
|
|
21
|
+
required: true,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
static flags = {
|
|
25
|
+
...ConvosBaseCommand.baseFlags,
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { args } = await this.parse(IdentityInfo);
|
|
29
|
+
const config = this.getConvosConfig();
|
|
30
|
+
const store = createIdentityStore();
|
|
31
|
+
const identity = store.get(args.id);
|
|
32
|
+
if (!identity) {
|
|
33
|
+
this.error(`Identity not found: ${args.id}`);
|
|
34
|
+
}
|
|
35
|
+
const client = await createClientForIdentity(identity, config);
|
|
36
|
+
const properties = {
|
|
37
|
+
id: identity.id,
|
|
38
|
+
address: getAccountAddress(identity.walletKey),
|
|
39
|
+
inboxId: client.inboxId,
|
|
40
|
+
installationId: client.installationId,
|
|
41
|
+
isRegistered: client.isRegistered,
|
|
42
|
+
conversationId: identity.conversationId ?? "(unlinked)",
|
|
43
|
+
label: identity.label ?? "",
|
|
44
|
+
profileName: identity.profileName ?? "",
|
|
45
|
+
createdAt: identity.createdAt,
|
|
46
|
+
};
|
|
47
|
+
const clientInfo = {
|
|
48
|
+
env: config.env,
|
|
49
|
+
libxmtpVersion: client.libxmtpVersion,
|
|
50
|
+
appVersion: client.appVersion,
|
|
51
|
+
disableDeviceSync: true,
|
|
52
|
+
};
|
|
53
|
+
if (this.jsonOutput) {
|
|
54
|
+
this.output({ properties, client: clientInfo });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.log(formatSections([
|
|
58
|
+
{ title: "Identity", data: properties },
|
|
59
|
+
{ title: "Client", data: clientInfo },
|
|
60
|
+
], 2));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class IdentityList extends ConvosBaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|