@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,39 @@
|
|
|
1
|
+
import { Args } 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
|
+
export default class ConversationAddMembers extends ConvosBaseCommand {
|
|
7
|
+
static description = `Add members to a conversation by inbox ID.
|
|
8
|
+
|
|
9
|
+
Low-level member addition. In Convos, members typically join via
|
|
10
|
+
invite links instead. Requires super admin permissions.`;
|
|
11
|
+
static strict = false;
|
|
12
|
+
static args = {
|
|
13
|
+
id: Args.string({ description: "The conversation ID", required: true }),
|
|
14
|
+
};
|
|
15
|
+
static flags = { ...ConvosBaseCommand.baseFlags };
|
|
16
|
+
async run() {
|
|
17
|
+
const { args, argv } = await this.parse(ConversationAddMembers);
|
|
18
|
+
const inboxIds = argv.slice(1);
|
|
19
|
+
if (inboxIds.length === 0)
|
|
20
|
+
this.error("At least one inbox ID is required");
|
|
21
|
+
const config = this.getConvosConfig();
|
|
22
|
+
const store = createIdentityStore();
|
|
23
|
+
const identity = store.getByConversationId(args.id);
|
|
24
|
+
if (!identity)
|
|
25
|
+
this.error(`No identity found for conversation: ${args.id}`);
|
|
26
|
+
const client = await createClientForIdentity(identity, config);
|
|
27
|
+
const conversation = await client.conversations.getConversationById(args.id);
|
|
28
|
+
if (!conversation)
|
|
29
|
+
this.error(`Conversation not found: ${args.id}`);
|
|
30
|
+
const group = requireGroup(conversation);
|
|
31
|
+
await group.addMembers(inboxIds);
|
|
32
|
+
this.output({
|
|
33
|
+
success: true,
|
|
34
|
+
conversationId: args.id,
|
|
35
|
+
addedInboxIds: inboxIds,
|
|
36
|
+
count: inboxIds.length,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class ConversationConsentState extends ConvosBaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static args: {
|
|
5
|
+
id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static flags: {
|
|
8
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
3
|
+
import { createClientForIdentity } from "../../utils/client.js";
|
|
4
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
5
|
+
export default class ConversationConsentState extends ConvosBaseCommand {
|
|
6
|
+
static description = `Get the consent state of a conversation.`;
|
|
7
|
+
static args = {
|
|
8
|
+
id: Args.string({ description: "The conversation ID", required: true }),
|
|
9
|
+
};
|
|
10
|
+
static flags = { ...ConvosBaseCommand.baseFlags };
|
|
11
|
+
async run() {
|
|
12
|
+
const { args } = await this.parse(ConversationConsentState);
|
|
13
|
+
const config = this.getConvosConfig();
|
|
14
|
+
const store = createIdentityStore();
|
|
15
|
+
const identity = store.getByConversationId(args.id);
|
|
16
|
+
if (!identity)
|
|
17
|
+
this.error(`No identity found for conversation: ${args.id}`);
|
|
18
|
+
const client = await createClientForIdentity(identity, config);
|
|
19
|
+
const conversation = await client.conversations.getConversationById(args.id);
|
|
20
|
+
if (!conversation)
|
|
21
|
+
this.error(`Conversation not found: ${args.id}`);
|
|
22
|
+
this.output({ conversationId: args.id, consentState: conversation.consentState() });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class ConversationDownloadAttachment 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
|
+
"message-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
static flags: {
|
|
13
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
raw: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
24
|
+
};
|
|
25
|
+
run(): Promise<void>;
|
|
26
|
+
private downloadInline;
|
|
27
|
+
private downloadRemote;
|
|
28
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { writeFile as fsWriteFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { cwd } from "node:process";
|
|
4
|
+
import { Args, Flags } from "@oclif/core";
|
|
5
|
+
import { decryptAttachment, } from "@xmtp/node-sdk";
|
|
6
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
7
|
+
import { createClientForIdentity } from "../../utils/client.js";
|
|
8
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
9
|
+
import { getExtension } from "../../utils/mime.js";
|
|
10
|
+
export default class ConversationDownloadAttachment extends ConvosBaseCommand {
|
|
11
|
+
static description = `Download an attachment from a message.
|
|
12
|
+
|
|
13
|
+
Downloads an attachment message and saves it to disk. Handles both
|
|
14
|
+
inline attachments and remote (encrypted) attachments transparently.
|
|
15
|
+
|
|
16
|
+
For inline attachments, the content is written directly to disk.
|
|
17
|
+
|
|
18
|
+
For remote attachments, the encrypted payload is fetched from the URL
|
|
19
|
+
embedded in the message, decrypted using the keys in the message, and
|
|
20
|
+
the decrypted content is saved to disk.
|
|
21
|
+
|
|
22
|
+
The output filename is determined by (in order of priority):
|
|
23
|
+
1. The --output flag (explicit path)
|
|
24
|
+
2. The filename from the message metadata
|
|
25
|
+
3. Auto-generated from message ID + MIME type extension
|
|
26
|
+
|
|
27
|
+
Use --raw to save the encrypted payload without decrypting (remote
|
|
28
|
+
attachments only).`;
|
|
29
|
+
static examples = [
|
|
30
|
+
{
|
|
31
|
+
command: "<%= config.bin %> <%= command.id %> <conversation-id> <message-id>",
|
|
32
|
+
description: "Download attachment (auto-names from message metadata)",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
command: "<%= config.bin %> <%= command.id %> <conversation-id> <message-id> --output ./photo.jpg",
|
|
36
|
+
description: "Download to a specific path",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
command: "<%= config.bin %> <%= command.id %> <conversation-id> <message-id> --raw",
|
|
40
|
+
description: "Save the encrypted payload without decrypting (remote only)",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
static args = {
|
|
44
|
+
id: Args.string({
|
|
45
|
+
description: "The conversation ID",
|
|
46
|
+
required: true,
|
|
47
|
+
}),
|
|
48
|
+
"message-id": Args.string({
|
|
49
|
+
description: "The message ID of the attachment",
|
|
50
|
+
required: true,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
static flags = {
|
|
54
|
+
...ConvosBaseCommand.baseFlags,
|
|
55
|
+
output: Flags.string({
|
|
56
|
+
char: "o",
|
|
57
|
+
description: "Output file path (default: auto-generated from metadata)",
|
|
58
|
+
helpValue: "<path>",
|
|
59
|
+
}),
|
|
60
|
+
raw: Flags.boolean({
|
|
61
|
+
description: "Save encrypted payload without decrypting (remote attachments only)",
|
|
62
|
+
default: false,
|
|
63
|
+
}),
|
|
64
|
+
sync: Flags.boolean({
|
|
65
|
+
description: "Sync conversation from network before downloading",
|
|
66
|
+
default: false,
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
async run() {
|
|
70
|
+
const { args, flags } = await this.parse(ConversationDownloadAttachment);
|
|
71
|
+
const config = this.getConvosConfig();
|
|
72
|
+
const store = createIdentityStore();
|
|
73
|
+
const identity = store.getByConversationId(args.id);
|
|
74
|
+
if (!identity) {
|
|
75
|
+
this.error(`No identity found for conversation: ${args.id}\nUse 'convos conversations list' to see available conversations.`);
|
|
76
|
+
}
|
|
77
|
+
const client = await createClientForIdentity(identity, config);
|
|
78
|
+
const conversation = await client.conversations.getConversationById(args.id);
|
|
79
|
+
if (!conversation) {
|
|
80
|
+
this.error(`Conversation not found: ${args.id}`);
|
|
81
|
+
}
|
|
82
|
+
if (flags.sync) {
|
|
83
|
+
await conversation.sync();
|
|
84
|
+
}
|
|
85
|
+
const message = client.conversations.getMessageById(args["message-id"]);
|
|
86
|
+
if (!message) {
|
|
87
|
+
this.error(`Message not found: ${args["message-id"]}. Try --sync to fetch latest messages.`);
|
|
88
|
+
}
|
|
89
|
+
const typeId = message.contentType.typeId;
|
|
90
|
+
if (typeId === "attachment") {
|
|
91
|
+
await this.downloadInline(message.content, args["message-id"], flags);
|
|
92
|
+
}
|
|
93
|
+
else if (typeId === "remoteStaticAttachment") {
|
|
94
|
+
await this.downloadRemote(message.content, args["message-id"], flags);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.error(`Message ${args["message-id"]} is not an attachment (type: ${typeId})`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async downloadInline(attachment, messageId, flags) {
|
|
101
|
+
const bytes = attachment.content;
|
|
102
|
+
const mimeType = attachment.mimeType;
|
|
103
|
+
const filename = flags.output ??
|
|
104
|
+
attachment.filename ??
|
|
105
|
+
`${messageId.slice(0, 16)}${getExtension(mimeType)}`;
|
|
106
|
+
const outputPath = isAbsolute(filename) ? filename : join(cwd(), filename);
|
|
107
|
+
await writeFileWithDirs(outputPath, bytes);
|
|
108
|
+
this.output({
|
|
109
|
+
success: true,
|
|
110
|
+
type: "inline",
|
|
111
|
+
outputPath,
|
|
112
|
+
filename: attachment.filename,
|
|
113
|
+
mimeType,
|
|
114
|
+
size: bytes.length,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async downloadRemote(remote, messageId, flags) {
|
|
118
|
+
const response = await fetch(remote.url);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
this.error(`Failed to fetch remote attachment: ${response.status} ${response.statusText} (${remote.url})`);
|
|
121
|
+
}
|
|
122
|
+
const encryptedBytes = new Uint8Array(await response.arrayBuffer());
|
|
123
|
+
if (flags.raw) {
|
|
124
|
+
const filename = flags.output ??
|
|
125
|
+
`${remote.filename ?? messageId.slice(0, 16)}.encrypted`;
|
|
126
|
+
const outputPath = isAbsolute(filename)
|
|
127
|
+
? filename
|
|
128
|
+
: join(cwd(), filename);
|
|
129
|
+
await writeFileWithDirs(outputPath, encryptedBytes);
|
|
130
|
+
this.output({
|
|
131
|
+
success: true,
|
|
132
|
+
type: "remote-raw",
|
|
133
|
+
outputPath,
|
|
134
|
+
url: remote.url,
|
|
135
|
+
size: encryptedBytes.length,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const decrypted = decryptAttachment(encryptedBytes, remote);
|
|
140
|
+
const mimeType = decrypted.mimeType;
|
|
141
|
+
const filename = flags.output ??
|
|
142
|
+
decrypted.filename ??
|
|
143
|
+
remote.filename ??
|
|
144
|
+
`${messageId.slice(0, 16)}${getExtension(mimeType)}`;
|
|
145
|
+
const outputPath = isAbsolute(filename) ? filename : join(cwd(), filename);
|
|
146
|
+
await writeFileWithDirs(outputPath, decrypted.content);
|
|
147
|
+
this.output({
|
|
148
|
+
success: true,
|
|
149
|
+
type: "remote",
|
|
150
|
+
outputPath,
|
|
151
|
+
filename: decrypted.filename ?? remote.filename,
|
|
152
|
+
mimeType,
|
|
153
|
+
size: decrypted.content.length,
|
|
154
|
+
url: remote.url,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function isAbsolute(p) {
|
|
159
|
+
return p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p);
|
|
160
|
+
}
|
|
161
|
+
async function writeFileWithDirs(path, data) {
|
|
162
|
+
await mkdir(dirname(path), { recursive: true });
|
|
163
|
+
await fsWriteFile(path, data);
|
|
164
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class ConversationExplode 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
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
scheduled: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
|
+
};
|
|
23
|
+
run(): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Args, 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 { parseAppData, serializeAppData } from "../../utils/metadata.js";
|
|
7
|
+
/**
|
|
8
|
+
* Encode an ExplodeSettings message matching the iOS content type.
|
|
9
|
+
*
|
|
10
|
+
* Content type: convos.org/explode_settings:1.0
|
|
11
|
+
* Payload: JSON-encoded { expiresAt: ISO8601 string }
|
|
12
|
+
* Fallback: "Conversation expires at {date}"
|
|
13
|
+
*/
|
|
14
|
+
function encodeExplodeSettings(expiresAt) {
|
|
15
|
+
const payload = JSON.stringify({
|
|
16
|
+
expiresAt: expiresAt.toISOString(),
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
type: {
|
|
20
|
+
authorityId: "convos.org",
|
|
21
|
+
typeId: "explode_settings",
|
|
22
|
+
versionMajor: 1,
|
|
23
|
+
versionMinor: 0,
|
|
24
|
+
},
|
|
25
|
+
parameters: {},
|
|
26
|
+
fallback: `Conversation expires at ${expiresAt.toISOString()}`,
|
|
27
|
+
content: new TextEncoder().encode(payload),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export default class ConversationExplode extends ConvosBaseCommand {
|
|
31
|
+
static description = `Explode (permanently destroy) a conversation.
|
|
32
|
+
|
|
33
|
+
Sends an ExplodeSettings message to notify all members, updates the
|
|
34
|
+
group metadata with the expiration timestamp, removes all members,
|
|
35
|
+
then deletes the local identity including wallet key, database
|
|
36
|
+
encryption key, and XMTP database. This is irreversible.
|
|
37
|
+
|
|
38
|
+
Per ADR 004: destroying the per-conversation identity destroys the
|
|
39
|
+
cryptographic material needed to decrypt messages. Recovery is
|
|
40
|
+
impossible.
|
|
41
|
+
|
|
42
|
+
Steps:
|
|
43
|
+
1. Send ExplodeSettings message (notifies iOS/other clients)
|
|
44
|
+
2. Update group metadata with expiresAtUnix
|
|
45
|
+
3. Remove all other members from the XMTP group
|
|
46
|
+
4. Delete the local identity (private keys + database)
|
|
47
|
+
|
|
48
|
+
Only the conversation creator (super admin) should explode.`;
|
|
49
|
+
static examples = [
|
|
50
|
+
{
|
|
51
|
+
command: "<%= config.bin %> <%= command.id %> <conversation-id> --force",
|
|
52
|
+
description: "Explode a conversation immediately",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
command: '<%= config.bin %> <%= command.id %> <conversation-id> --scheduled "2025-03-01T00:00:00Z"',
|
|
56
|
+
description: "Schedule a conversation to explode at a specific time",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
static args = {
|
|
60
|
+
id: Args.string({ description: "The conversation ID", required: true }),
|
|
61
|
+
};
|
|
62
|
+
static flags = {
|
|
63
|
+
...ConvosBaseCommand.baseFlags,
|
|
64
|
+
force: Flags.boolean({
|
|
65
|
+
char: "f",
|
|
66
|
+
description: "Skip confirmation prompt",
|
|
67
|
+
default: false,
|
|
68
|
+
}),
|
|
69
|
+
scheduled: Flags.string({
|
|
70
|
+
description: "Schedule explosion for a future date (ISO8601). If omitted, explodes immediately.",
|
|
71
|
+
required: false,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
async run() {
|
|
75
|
+
const { args, flags } = await this.parse(ConversationExplode);
|
|
76
|
+
const config = this.getConvosConfig();
|
|
77
|
+
const store = createIdentityStore();
|
|
78
|
+
const identity = store.getByConversationId(args.id);
|
|
79
|
+
if (!identity)
|
|
80
|
+
this.error(`No identity found for conversation: ${args.id}`);
|
|
81
|
+
// Determine expiration time
|
|
82
|
+
let expiresAt;
|
|
83
|
+
if (flags.scheduled) {
|
|
84
|
+
expiresAt = new Date(flags.scheduled);
|
|
85
|
+
if (isNaN(expiresAt.getTime())) {
|
|
86
|
+
this.error(`Invalid date: ${flags.scheduled}`);
|
|
87
|
+
}
|
|
88
|
+
if (expiresAt <= new Date()) {
|
|
89
|
+
this.error("Scheduled date must be in the future");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
expiresAt = new Date();
|
|
94
|
+
}
|
|
95
|
+
const isImmediate = !flags.scheduled;
|
|
96
|
+
const confirmMessage = isImmediate
|
|
97
|
+
? "This will permanently destroy the conversation and delete all cryptographic keys.\n" +
|
|
98
|
+
"All members will be removed. Messages cannot be recovered."
|
|
99
|
+
: `This will schedule the conversation to explode at ${expiresAt.toISOString()}.\n` +
|
|
100
|
+
"All members will be notified. When the time arrives, clients will destroy their local data.";
|
|
101
|
+
await this.confirmAction(confirmMessage, flags.force);
|
|
102
|
+
const client = await createClientForIdentity(identity, config);
|
|
103
|
+
const conversation = await client.conversations.getConversationById(args.id);
|
|
104
|
+
if (!conversation)
|
|
105
|
+
this.error(`Conversation not found: ${args.id}`);
|
|
106
|
+
const group = requireGroup(conversation);
|
|
107
|
+
// Step 1: Send ExplodeSettings message (must happen before removing members)
|
|
108
|
+
const encodedContent = encodeExplodeSettings(expiresAt);
|
|
109
|
+
await group.send(encodedContent, { shouldPush: true });
|
|
110
|
+
// Step 2: Update group metadata with expiresAtUnix
|
|
111
|
+
try {
|
|
112
|
+
let appData = "";
|
|
113
|
+
try {
|
|
114
|
+
appData = group.appData ?? "";
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// No appData yet
|
|
118
|
+
}
|
|
119
|
+
const metadata = parseAppData(appData);
|
|
120
|
+
metadata.expiresAtUnix = Math.floor(expiresAt.getTime() / 1000);
|
|
121
|
+
const newAppData = serializeAppData(metadata);
|
|
122
|
+
await group.updateAppData(newAppData);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Non-fatal: metadata update is secondary to the message
|
|
126
|
+
}
|
|
127
|
+
if (isImmediate) {
|
|
128
|
+
// Step 3: Remove all other members
|
|
129
|
+
const members = await group.members();
|
|
130
|
+
const others = members.filter((m) => m.inboxId !== client.inboxId);
|
|
131
|
+
if (others.length > 0) {
|
|
132
|
+
await group.removeMembers(others.map((m) => m.inboxId));
|
|
133
|
+
}
|
|
134
|
+
// Step 4: Delete local identity
|
|
135
|
+
store.remove(identity.id);
|
|
136
|
+
this.output({
|
|
137
|
+
success: true,
|
|
138
|
+
conversationId: args.id,
|
|
139
|
+
identityDestroyed: identity.id,
|
|
140
|
+
membersRemoved: others.length,
|
|
141
|
+
expiresAt: expiresAt.toISOString(),
|
|
142
|
+
message: "Conversation exploded. All cryptographic keys destroyed.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Scheduled: don't remove members or destroy identity yet
|
|
147
|
+
this.output({
|
|
148
|
+
success: true,
|
|
149
|
+
conversationId: args.id,
|
|
150
|
+
scheduled: true,
|
|
151
|
+
expiresAt: expiresAt.toISOString(),
|
|
152
|
+
message: `Conversation scheduled to explode at ${expiresAt.toISOString()}. All members have been notified.`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class ConversationInfo 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,79 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { getAccountAddress, isGroup, 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 ConversationInfo extends ConvosBaseCommand {
|
|
7
|
+
static description = `Get detailed information about a conversation.
|
|
8
|
+
|
|
9
|
+
Resolves the per-conversation identity and shows full details
|
|
10
|
+
including members, permissions, metadata, and identity info.`;
|
|
11
|
+
static examples = [
|
|
12
|
+
{
|
|
13
|
+
command: "<%= config.bin %> <%= command.id %> <conversation-id>",
|
|
14
|
+
description: "Get conversation details",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
static args = {
|
|
18
|
+
id: Args.string({ description: "The conversation ID", required: true }),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
...ConvosBaseCommand.baseFlags,
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { args } = await this.parse(ConversationInfo);
|
|
25
|
+
const config = this.getConvosConfig();
|
|
26
|
+
const store = createIdentityStore();
|
|
27
|
+
const identity = store.getByConversationId(args.id);
|
|
28
|
+
if (!identity) {
|
|
29
|
+
this.error(`No identity found for conversation: ${args.id}`);
|
|
30
|
+
}
|
|
31
|
+
const client = await createClientForIdentity(identity, config);
|
|
32
|
+
const conversation = await client.conversations.getConversationById(args.id);
|
|
33
|
+
if (!conversation) {
|
|
34
|
+
this.error(`Conversation not found on network: ${args.id}`);
|
|
35
|
+
}
|
|
36
|
+
const metadata = await conversation.metadata();
|
|
37
|
+
const members = await conversation.members();
|
|
38
|
+
const convData = {
|
|
39
|
+
id: conversation.id,
|
|
40
|
+
createdAt: conversation.createdAt.toISOString(),
|
|
41
|
+
consentState: conversation.consentState(),
|
|
42
|
+
isActive: conversation.isActive,
|
|
43
|
+
creatorInboxId: metadata.creatorInboxId,
|
|
44
|
+
memberCount: members.length,
|
|
45
|
+
};
|
|
46
|
+
if (isGroup(conversation)) {
|
|
47
|
+
convData.name = conversation.name;
|
|
48
|
+
convData.description = conversation.description;
|
|
49
|
+
convData.imageUrl = conversation.imageUrl;
|
|
50
|
+
}
|
|
51
|
+
const identityData = {
|
|
52
|
+
identityId: identity.id,
|
|
53
|
+
address: getAccountAddress(identity.walletKey),
|
|
54
|
+
inboxId: client.inboxId,
|
|
55
|
+
label: identity.label ?? "",
|
|
56
|
+
profileName: identity.profileName ?? "",
|
|
57
|
+
};
|
|
58
|
+
const memberList = members.map((m) => ({
|
|
59
|
+
inboxId: m.inboxId,
|
|
60
|
+
accountIdentifiers: m.accountIdentifiers,
|
|
61
|
+
permissionLevel: m.permissionLevel,
|
|
62
|
+
}));
|
|
63
|
+
if (this.jsonOutput) {
|
|
64
|
+
this.output({
|
|
65
|
+
conversation: convData,
|
|
66
|
+
identity: identityData,
|
|
67
|
+
members: memberList,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.log(formatSections([
|
|
72
|
+
{ title: "Conversation", data: convData },
|
|
73
|
+
{ title: "Identity", data: identityData },
|
|
74
|
+
], 2));
|
|
75
|
+
this.log("\nMembers\n");
|
|
76
|
+
this.output(memberList);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class ConversationInvite 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
|
+
qr: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
"expires-in": import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"single-use": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
"include-metadata": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
24
|
+
};
|
|
25
|
+
run(): Promise<void>;
|
|
26
|
+
}
|