@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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +572 -0
  3. package/bin/dev.js +4 -0
  4. package/bin/run.js +4 -0
  5. package/dist/baseCommand.d.ts +46 -0
  6. package/dist/baseCommand.js +171 -0
  7. package/dist/commands/agent/serve.d.ts +67 -0
  8. package/dist/commands/agent/serve.js +662 -0
  9. package/dist/commands/conversation/add-members.d.ts +19 -0
  10. package/dist/commands/conversation/add-members.js +39 -0
  11. package/dist/commands/conversation/consent-state.d.ts +18 -0
  12. package/dist/commands/conversation/consent-state.js +24 -0
  13. package/dist/commands/conversation/download-attachment.d.ts +28 -0
  14. package/dist/commands/conversation/download-attachment.js +164 -0
  15. package/dist/commands/conversation/explode.d.ts +24 -0
  16. package/dist/commands/conversation/explode.js +156 -0
  17. package/dist/commands/conversation/info.d.ts +22 -0
  18. package/dist/commands/conversation/info.js +79 -0
  19. package/dist/commands/conversation/invite.d.ts +26 -0
  20. package/dist/commands/conversation/invite.js +137 -0
  21. package/dist/commands/conversation/lock.d.ts +24 -0
  22. package/dist/commands/conversation/lock.js +98 -0
  23. package/dist/commands/conversation/members.d.ts +22 -0
  24. package/dist/commands/conversation/members.js +39 -0
  25. package/dist/commands/conversation/messages.d.ts +31 -0
  26. package/dist/commands/conversation/messages.js +141 -0
  27. package/dist/commands/conversation/permissions.d.ts +18 -0
  28. package/dist/commands/conversation/permissions.js +33 -0
  29. package/dist/commands/conversation/profiles.d.ts +22 -0
  30. package/dist/commands/conversation/profiles.js +80 -0
  31. package/dist/commands/conversation/remove-members.d.ts +19 -0
  32. package/dist/commands/conversation/remove-members.js +36 -0
  33. package/dist/commands/conversation/send-attachment.d.ts +30 -0
  34. package/dist/commands/conversation/send-attachment.js +187 -0
  35. package/dist/commands/conversation/send-reaction.d.ts +21 -0
  36. package/dist/commands/conversation/send-reaction.js +38 -0
  37. package/dist/commands/conversation/send-remote-attachment.d.ts +30 -0
  38. package/dist/commands/conversation/send-remote-attachment.js +96 -0
  39. package/dist/commands/conversation/send-reply.d.ts +32 -0
  40. package/dist/commands/conversation/send-reply.js +170 -0
  41. package/dist/commands/conversation/send-text.d.ts +24 -0
  42. package/dist/commands/conversation/send-text.js +64 -0
  43. package/dist/commands/conversation/stream.d.ts +24 -0
  44. package/dist/commands/conversation/stream.js +81 -0
  45. package/dist/commands/conversation/sync.d.ts +18 -0
  46. package/dist/commands/conversation/sync.js +25 -0
  47. package/dist/commands/conversation/update-consent.d.ts +19 -0
  48. package/dist/commands/conversation/update-consent.js +35 -0
  49. package/dist/commands/conversation/update-description.d.ts +19 -0
  50. package/dist/commands/conversation/update-description.js +28 -0
  51. package/dist/commands/conversation/update-name.d.ts +19 -0
  52. package/dist/commands/conversation/update-name.js +29 -0
  53. package/dist/commands/conversation/update-profile.d.ts +24 -0
  54. package/dist/commands/conversation/update-profile.js +97 -0
  55. package/dist/commands/conversations/create.d.ts +26 -0
  56. package/dist/commands/conversations/create.js +165 -0
  57. package/dist/commands/conversations/join.d.ts +27 -0
  58. package/dist/commands/conversations/join.js +232 -0
  59. package/dist/commands/conversations/list.d.ts +20 -0
  60. package/dist/commands/conversations/list.js +109 -0
  61. package/dist/commands/conversations/process-join-requests.d.ts +26 -0
  62. package/dist/commands/conversations/process-join-requests.js +261 -0
  63. package/dist/commands/conversations/sync.d.ts +19 -0
  64. package/dist/commands/conversations/sync.js +50 -0
  65. package/dist/commands/identity/create.d.ts +21 -0
  66. package/dist/commands/identity/create.js +56 -0
  67. package/dist/commands/identity/info.d.ts +22 -0
  68. package/dist/commands/identity/info.js +63 -0
  69. package/dist/commands/identity/list.d.ts +19 -0
  70. package/dist/commands/identity/list.js +59 -0
  71. package/dist/commands/identity/remove.d.ts +23 -0
  72. package/dist/commands/identity/remove.js +51 -0
  73. package/dist/commands/init.d.ts +16 -0
  74. package/dist/commands/init.js +91 -0
  75. package/dist/commands/reset.d.ts +17 -0
  76. package/dist/commands/reset.js +93 -0
  77. package/dist/help.d.ts +4 -0
  78. package/dist/help.js +31 -0
  79. package/dist/index.d.ts +9 -0
  80. package/dist/index.js +15 -0
  81. package/dist/utils/client.d.ts +8 -0
  82. package/dist/utils/client.js +58 -0
  83. package/dist/utils/config.d.ts +15 -0
  84. package/dist/utils/config.js +1 -0
  85. package/dist/utils/identities.d.ts +49 -0
  86. package/dist/utils/identities.js +92 -0
  87. package/dist/utils/invite.d.ts +70 -0
  88. package/dist/utils/invite.js +339 -0
  89. package/dist/utils/metadata.d.ts +39 -0
  90. package/dist/utils/metadata.js +180 -0
  91. package/dist/utils/mime.d.ts +2 -0
  92. package/dist/utils/mime.js +42 -0
  93. package/dist/utils/random.d.ts +5 -0
  94. package/dist/utils/random.js +19 -0
  95. package/dist/utils/upload.d.ts +14 -0
  96. package/dist/utils/upload.js +51 -0
  97. package/dist/utils/xmtp.d.ts +45 -0
  98. package/dist/utils/xmtp.js +298 -0
  99. package/oclif.manifest.json +5562 -0
  100. package/package.json +124 -0
  101. 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
+ }