@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,59 @@
|
|
|
1
|
+
import { getAccountAddress } from "../../utils/xmtp.js";
|
|
2
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
3
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
4
|
+
export default class IdentityList extends ConvosBaseCommand {
|
|
5
|
+
static description = `List all Convos identities.
|
|
6
|
+
|
|
7
|
+
Each Convos conversation uses its own XMTP identity (inbox) for privacy.
|
|
8
|
+
This command shows all identities, including which conversation each is
|
|
9
|
+
linked to.
|
|
10
|
+
|
|
11
|
+
Identities without a conversation ID are "unlinked" — available for
|
|
12
|
+
use with new conversations.`;
|
|
13
|
+
static examples = [
|
|
14
|
+
{
|
|
15
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
16
|
+
description: "List all identities",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
command: "<%= config.bin %> <%= command.id %> --json",
|
|
20
|
+
description: "Output as JSON",
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
static flags = {
|
|
24
|
+
...ConvosBaseCommand.baseFlags,
|
|
25
|
+
};
|
|
26
|
+
async run() {
|
|
27
|
+
const store = createIdentityStore();
|
|
28
|
+
const identities = store.list();
|
|
29
|
+
if (identities.length === 0) {
|
|
30
|
+
this.output({
|
|
31
|
+
message: "No identities found. Create one with: convos identity create",
|
|
32
|
+
count: 0,
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Detect duplicate identities per conversation
|
|
37
|
+
const convCounts = new Map();
|
|
38
|
+
for (const identity of identities) {
|
|
39
|
+
if (identity.conversationId) {
|
|
40
|
+
convCounts.set(identity.conversationId, (convCounts.get(identity.conversationId) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const output = identities.map((identity) => ({
|
|
44
|
+
id: identity.id,
|
|
45
|
+
address: getAccountAddress(identity.walletKey),
|
|
46
|
+
inboxId: identity.inboxId ?? "(not yet registered)",
|
|
47
|
+
conversationId: identity.conversationId ?? "(unlinked)",
|
|
48
|
+
inviteTag: identity.inviteTag ?? "",
|
|
49
|
+
label: identity.label ?? "",
|
|
50
|
+
profileName: identity.profileName ?? "",
|
|
51
|
+
createdAt: identity.createdAt,
|
|
52
|
+
...(identity.conversationId &&
|
|
53
|
+
(convCounts.get(identity.conversationId) ?? 0) > 1
|
|
54
|
+
? { duplicate: true }
|
|
55
|
+
: {}),
|
|
56
|
+
}));
|
|
57
|
+
this.output(output);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
2
|
+
export default class IdentityRemove 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
|
+
"log-level": import("@oclif/core/interfaces").OptionFlag<"off" | "error" | "warn" | "info" | "debug" | "trace" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"structured-logging": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
"app-version": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
};
|
|
22
|
+
run(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Args, Flags } from "@oclif/core";
|
|
2
|
+
import { ConvosBaseCommand } from "../../baseCommand.js";
|
|
3
|
+
import { createIdentityStore } from "../../utils/identities.js";
|
|
4
|
+
export default class IdentityRemove extends ConvosBaseCommand {
|
|
5
|
+
static description = `Remove an identity and all associated data.
|
|
6
|
+
|
|
7
|
+
Permanently deletes the identity's wallet key, database encryption key,
|
|
8
|
+
XMTP database files, and all local data. This is irreversible.
|
|
9
|
+
|
|
10
|
+
Per ADR 002 and ADR 004: destroying an identity destroys the
|
|
11
|
+
cryptographic material needed to participate in its conversation.`;
|
|
12
|
+
static examples = [
|
|
13
|
+
{
|
|
14
|
+
command: "<%= config.bin %> <%= command.id %> <identity-id> --force",
|
|
15
|
+
description: "Remove an identity",
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
static args = {
|
|
19
|
+
id: Args.string({
|
|
20
|
+
description: "The identity ID to remove",
|
|
21
|
+
required: true,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
static flags = {
|
|
25
|
+
...ConvosBaseCommand.baseFlags,
|
|
26
|
+
force: Flags.boolean({
|
|
27
|
+
char: "f",
|
|
28
|
+
description: "Skip confirmation prompt",
|
|
29
|
+
default: false,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { args, flags } = await this.parse(IdentityRemove);
|
|
34
|
+
const store = createIdentityStore();
|
|
35
|
+
const identity = store.get(args.id);
|
|
36
|
+
if (!identity) {
|
|
37
|
+
this.error(`Identity not found: ${args.id}`);
|
|
38
|
+
}
|
|
39
|
+
await this.confirmAction(`This will permanently delete identity ${args.id}` +
|
|
40
|
+
(identity.conversationId
|
|
41
|
+
? ` and its conversation ${identity.conversationId}`
|
|
42
|
+
: "") +
|
|
43
|
+
". All cryptographic keys will be destroyed.", flags.force);
|
|
44
|
+
store.remove(args.id);
|
|
45
|
+
this.output({
|
|
46
|
+
success: true,
|
|
47
|
+
removedIdentityId: args.id,
|
|
48
|
+
removedConversationId: identity.conversationId ?? null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
export default class Init extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
stdout: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production", import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
catch(error: Error): never;
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { Command, Errors, Flags } from "@oclif/core";
|
|
5
|
+
import { VALID_ENVS } from "../utils/xmtp.js";
|
|
6
|
+
const CONVOS_HOME = join(homedir(), ".convos");
|
|
7
|
+
const DEFAULT_ENV_PATH = join(CONVOS_HOME, ".env");
|
|
8
|
+
export default class Init extends Command {
|
|
9
|
+
static description = `Initialize Convos CLI configuration.
|
|
10
|
+
|
|
11
|
+
Sets up the ~/.convos directory structure and default environment.
|
|
12
|
+
Unlike standard XMTP, Convos does not generate a single wallet key.
|
|
13
|
+
Each conversation creates its own identity (see 'convos identity create').
|
|
14
|
+
|
|
15
|
+
This command creates:
|
|
16
|
+
- The ~/.convos directory
|
|
17
|
+
- A .env file with default environment settings
|
|
18
|
+
- The identities storage directory`;
|
|
19
|
+
static examples = [
|
|
20
|
+
{
|
|
21
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
22
|
+
description: `Initialize with default settings`,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
command: "<%= config.bin %> <%= command.id %> --env production",
|
|
26
|
+
description: "Initialize with production environment",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
command: "<%= config.bin %> <%= command.id %> --force",
|
|
30
|
+
description: "Overwrite existing configuration",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
static flags = {
|
|
34
|
+
stdout: Flags.boolean({
|
|
35
|
+
description: "Output configuration to stdout instead of writing to file",
|
|
36
|
+
default: false,
|
|
37
|
+
}),
|
|
38
|
+
output: Flags.string({
|
|
39
|
+
char: "o",
|
|
40
|
+
description: "Path to write .env file",
|
|
41
|
+
helpValue: "<path>",
|
|
42
|
+
}),
|
|
43
|
+
env: Flags.option({
|
|
44
|
+
options: [...VALID_ENVS],
|
|
45
|
+
description: "XMTP environment",
|
|
46
|
+
default: "dev",
|
|
47
|
+
})(),
|
|
48
|
+
force: Flags.boolean({
|
|
49
|
+
char: "f",
|
|
50
|
+
description: "Overwrite existing configuration",
|
|
51
|
+
default: false,
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
catch(error) {
|
|
55
|
+
if (error instanceof Errors.CLIError) {
|
|
56
|
+
error.showHelp = false;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const cliError = new Errors.CLIError(error.message);
|
|
60
|
+
cliError.showHelp = false;
|
|
61
|
+
throw cliError;
|
|
62
|
+
}
|
|
63
|
+
async run() {
|
|
64
|
+
const { flags } = await this.parse(Init);
|
|
65
|
+
const envContent = `# Convos CLI Configuration
|
|
66
|
+
# Each conversation creates its own identity — no global wallet key needed.
|
|
67
|
+
CONVOS_ENV=${flags.env}
|
|
68
|
+
`;
|
|
69
|
+
if (flags.stdout) {
|
|
70
|
+
this.log(envContent);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const outputPath = resolve(flags.output ?? DEFAULT_ENV_PATH);
|
|
74
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
75
|
+
await mkdir(join(CONVOS_HOME, "identities"), { recursive: true });
|
|
76
|
+
let fileExists = false;
|
|
77
|
+
try {
|
|
78
|
+
await access(outputPath);
|
|
79
|
+
fileExists = true;
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
if (fileExists && !flags.force) {
|
|
83
|
+
this.error(`File already exists: ${outputPath}\nUse --force to overwrite.`);
|
|
84
|
+
}
|
|
85
|
+
await writeFile(outputPath, envContent, "utf-8");
|
|
86
|
+
this.log(`Configuration written to ${outputPath}`);
|
|
87
|
+
this.log(`\nNext steps:`);
|
|
88
|
+
this.log(` convos conversations create --name "My Group" # Create a conversation`);
|
|
89
|
+
this.log(` convos identity list # See all identities`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ConvosBaseCommand } from "../baseCommand.js";
|
|
2
|
+
export default class Reset extends ConvosBaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
env: import("@oclif/core/interfaces").OptionFlag<"local" | "dev" | "production" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"gateway-host": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Flags } from "@oclif/core";
|
|
5
|
+
import { ConvosBaseCommand } from "../baseCommand.js";
|
|
6
|
+
import { createIdentityStore } from "../utils/identities.js";
|
|
7
|
+
const CONVOS_HOME = join(homedir(), ".convos");
|
|
8
|
+
export default class Reset extends ConvosBaseCommand {
|
|
9
|
+
static description = `Reset Convos by deleting all identities and conversation data.
|
|
10
|
+
|
|
11
|
+
Permanently removes all per-conversation identities (wallet keys,
|
|
12
|
+
database encryption keys) and all XMTP databases. The .env
|
|
13
|
+
configuration file is preserved.
|
|
14
|
+
|
|
15
|
+
This is irreversible — all cryptographic keys will be destroyed
|
|
16
|
+
and conversations cannot be recovered.`;
|
|
17
|
+
static examples = [
|
|
18
|
+
{
|
|
19
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
20
|
+
description: "Reset with confirmation prompt",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
command: "<%= config.bin %> <%= command.id %> --force",
|
|
24
|
+
description: "Reset without confirmation",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
static flags = {
|
|
28
|
+
...ConvosBaseCommand.commonFlags,
|
|
29
|
+
force: Flags.boolean({
|
|
30
|
+
char: "f",
|
|
31
|
+
description: "Skip confirmation prompt",
|
|
32
|
+
default: false,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
async run() {
|
|
36
|
+
const { flags } = await this.parse(Reset);
|
|
37
|
+
const store = createIdentityStore();
|
|
38
|
+
const identities = store.list();
|
|
39
|
+
const identitiesDir = join(CONVOS_HOME, "identities");
|
|
40
|
+
const dbDir = join(CONVOS_HOME, "db");
|
|
41
|
+
// Count what will be deleted
|
|
42
|
+
const identityCount = identities.length;
|
|
43
|
+
const conversationCount = identities.filter((i) => i.conversationId).length;
|
|
44
|
+
let dbFileCount = 0;
|
|
45
|
+
if (existsSync(dbDir)) {
|
|
46
|
+
for (const envDir of readdirSync(dbDir)) {
|
|
47
|
+
const envPath = join(dbDir, envDir);
|
|
48
|
+
try {
|
|
49
|
+
dbFileCount += readdirSync(envPath).length;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// skip non-directories
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (identityCount === 0 && dbFileCount === 0) {
|
|
57
|
+
this.output({
|
|
58
|
+
success: true,
|
|
59
|
+
message: "Nothing to reset — no identities or databases found.",
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await this.confirmAction(`This will permanently delete:\n` +
|
|
64
|
+
` • ${identityCount} ${identityCount === 1 ? "identity" : "identities"}\n` +
|
|
65
|
+
` • ${conversationCount} linked ${conversationCount === 1 ? "conversation" : "conversations"}\n` +
|
|
66
|
+
` • ${dbFileCount} database ${dbFileCount === 1 ? "file" : "files"}\n\n` +
|
|
67
|
+
`All cryptographic keys will be destroyed. This cannot be undone.`, flags.force);
|
|
68
|
+
// Delete all identity files
|
|
69
|
+
let identitiesRemoved = 0;
|
|
70
|
+
if (existsSync(identitiesDir)) {
|
|
71
|
+
for (const file of readdirSync(identitiesDir)) {
|
|
72
|
+
if (file.endsWith(".json")) {
|
|
73
|
+
rmSync(join(identitiesDir, file));
|
|
74
|
+
identitiesRemoved++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Delete all database files
|
|
79
|
+
let dbFilesRemoved = 0;
|
|
80
|
+
if (existsSync(dbDir)) {
|
|
81
|
+
rmSync(dbDir, { recursive: true, force: true });
|
|
82
|
+
dbFilesRemoved = dbFileCount;
|
|
83
|
+
}
|
|
84
|
+
this.output({
|
|
85
|
+
success: true,
|
|
86
|
+
identitiesRemoved,
|
|
87
|
+
conversationsRemoved: conversationCount,
|
|
88
|
+
dbFilesRemoved,
|
|
89
|
+
message: "All identities and conversation data have been destroyed. " +
|
|
90
|
+
"Your .env configuration has been preserved.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/help.d.ts
ADDED
package/dist/help.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Help } from "@oclif/core";
|
|
2
|
+
export default class ConvosHelp extends Help {
|
|
3
|
+
async showRootHelp() {
|
|
4
|
+
await super.showRootHelp();
|
|
5
|
+
this.log(this.section("GETTING STARTED", this.wrap([
|
|
6
|
+
"Initialize your Convos configuration:",
|
|
7
|
+
"",
|
|
8
|
+
" $ convos init",
|
|
9
|
+
"",
|
|
10
|
+
"Create a conversation (auto-creates a per-conversation identity):",
|
|
11
|
+
"",
|
|
12
|
+
" $ convos conversations create --name 'My Group'",
|
|
13
|
+
"",
|
|
14
|
+
"Send a message:",
|
|
15
|
+
"",
|
|
16
|
+
" $ convos conversation send-text <conversation-id> 'Hello!'",
|
|
17
|
+
"",
|
|
18
|
+
"List all conversations across all identities:",
|
|
19
|
+
"",
|
|
20
|
+
" $ convos conversations list",
|
|
21
|
+
"",
|
|
22
|
+
"PRIVACY MODEL",
|
|
23
|
+
"",
|
|
24
|
+
" Convos creates a unique XMTP identity (inbox) for each conversation.",
|
|
25
|
+
" Conversations cannot be linked or correlated by external observers.",
|
|
26
|
+
"",
|
|
27
|
+
" Run 'convos <command> --help' for more information on a command.",
|
|
28
|
+
].join("\n"))));
|
|
29
|
+
this.log("");
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { run } from "@oclif/core";
|
|
2
|
+
export { ConvosBaseCommand } from "./baseCommand.js";
|
|
3
|
+
export { createIdentityStore, type Identity, type IdentityStore, } from "./utils/identities.js";
|
|
4
|
+
export { createClientForIdentity } from "./utils/client.js";
|
|
5
|
+
export type { ConvosConfig } from "./utils/config.js";
|
|
6
|
+
export { createInviteSlug, parseInvite, verifyInvite, verifyInviteSignature, recoverInvitePublicKey, inviteToSlug, encryptConversationToken, decryptConversationToken, type InviteOptions, type ParsedInvite, } from "./utils/invite.js";
|
|
7
|
+
export { parseAppData, serializeAppData, upsertProfile, removeProfile, getProfile, type ConversationProfile, type ConversationCustomMetadata, } from "./utils/metadata.js";
|
|
8
|
+
export { randomAlphanumeric } from "./utils/random.js";
|
|
9
|
+
export { getAccountAddress, toHexBytes, isGroup, isDm, requireGroup, requireDm, formatSections, formatHuman, jsonStringify, buildProfileMap, isDisplayableMessage, normalizeMessageContent, type ProfileMap, isTTY, VALID_ENVS, type Section, } from "./utils/xmtp.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { run } from "@oclif/core";
|
|
2
|
+
// Base command for plugin extension
|
|
3
|
+
export { ConvosBaseCommand } from "./baseCommand.js";
|
|
4
|
+
// Identity management
|
|
5
|
+
export { createIdentityStore, } from "./utils/identities.js";
|
|
6
|
+
// Client creation
|
|
7
|
+
export { createClientForIdentity } from "./utils/client.js";
|
|
8
|
+
// Invite system
|
|
9
|
+
export { createInviteSlug, parseInvite, verifyInvite, verifyInviteSignature, recoverInvitePublicKey, inviteToSlug, encryptConversationToken, decryptConversationToken, } from "./utils/invite.js";
|
|
10
|
+
// Metadata / profiles
|
|
11
|
+
export { parseAppData, serializeAppData, upsertProfile, removeProfile, getProfile, } from "./utils/metadata.js";
|
|
12
|
+
// Random utilities
|
|
13
|
+
export { randomAlphanumeric } from "./utils/random.js";
|
|
14
|
+
// Re-export XMTP utilities for downstream consumers
|
|
15
|
+
export { getAccountAddress, toHexBytes, isGroup, isDm, requireGroup, requireDm, formatSections, formatHuman, jsonStringify, buildProfileMap, isDisplayableMessage, normalizeMessageContent, isTTY, VALID_ENVS, } from "./utils/xmtp.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Client } from "@xmtp/node-sdk";
|
|
2
|
+
import type { ConvosConfig } from "./config.js";
|
|
3
|
+
import type { Identity } from "./identities.js";
|
|
4
|
+
/**
|
|
5
|
+
* Create an XMTP client for a specific Convos identity.
|
|
6
|
+
* Each conversation gets its own identity and client (ADR 002).
|
|
7
|
+
*/
|
|
8
|
+
export declare function createClientForIdentity(identity: Identity, config: ConvosConfig): Promise<Client>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { AttachmentCodec, RemoteAttachmentCodec, } from "@xmtp/content-type-remote-attachment";
|
|
6
|
+
import { Client } from "@xmtp/node-sdk";
|
|
7
|
+
import { toHexBytes, hexToBytes } from "./xmtp.js";
|
|
8
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
9
|
+
import { createIdentityStore } from "./identities.js";
|
|
10
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
12
|
+
const DEFAULT_APP_VERSION = `convos-cli/${pkg.version}`;
|
|
13
|
+
const LOG_LEVELS = {
|
|
14
|
+
off: "Off" /* LogLevel.Off */,
|
|
15
|
+
error: "Error" /* LogLevel.Error */,
|
|
16
|
+
warn: "Warn" /* LogLevel.Warn */,
|
|
17
|
+
info: "Info" /* LogLevel.Info */,
|
|
18
|
+
debug: "Debug" /* LogLevel.Debug */,
|
|
19
|
+
trace: "Trace" /* LogLevel.Trace */,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Create an XMTP client for a specific Convos identity.
|
|
23
|
+
* Each conversation gets its own identity and client (ADR 002).
|
|
24
|
+
*/
|
|
25
|
+
export async function createClientForIdentity(identity, config) {
|
|
26
|
+
const store = createIdentityStore();
|
|
27
|
+
const env = config.env ?? "dev";
|
|
28
|
+
const dbPath = store.getDbPath(identity.id, env);
|
|
29
|
+
const account = privateKeyToAccount(identity.walletKey);
|
|
30
|
+
const signer = {
|
|
31
|
+
type: "EOA",
|
|
32
|
+
getIdentifier: () => ({
|
|
33
|
+
identifierKind: 0 /* IdentifierKind.Ethereum */,
|
|
34
|
+
identifier: account.address.toLowerCase(),
|
|
35
|
+
}),
|
|
36
|
+
signMessage: async (message) => {
|
|
37
|
+
const signature = await account.signMessage({ message });
|
|
38
|
+
return hexToBytes(signature);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
await mkdir(dirname(dbPath), { recursive: true });
|
|
42
|
+
const client = await Client.create(signer, {
|
|
43
|
+
env,
|
|
44
|
+
codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()],
|
|
45
|
+
dbEncryptionKey: toHexBytes(identity.dbEncryptionKey),
|
|
46
|
+
dbPath,
|
|
47
|
+
gatewayHost: config.gatewayHost,
|
|
48
|
+
loggingLevel: config.logLevel ? LOG_LEVELS[config.logLevel] : undefined,
|
|
49
|
+
structuredLogging: config.structuredLogging,
|
|
50
|
+
disableDeviceSync: true, // Per ADR 002: each conversation is independent
|
|
51
|
+
appVersion: config.appVersion ?? DEFAULT_APP_VERSION,
|
|
52
|
+
});
|
|
53
|
+
// Cache the inbox ID on the identity
|
|
54
|
+
if (!identity.inboxId) {
|
|
55
|
+
store.update(identity.id, { inboxId: client.inboxId });
|
|
56
|
+
}
|
|
57
|
+
return client;
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { XmtpEnv } from "@xmtp/node-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Convos-specific config. The per-identity wallet/db keys are not here —
|
|
4
|
+
* they live in each Identity. This holds global settings only.
|
|
5
|
+
*/
|
|
6
|
+
export interface ConvosConfig {
|
|
7
|
+
env?: XmtpEnv;
|
|
8
|
+
logLevel?: string;
|
|
9
|
+
structuredLogging?: boolean;
|
|
10
|
+
gatewayHost?: string;
|
|
11
|
+
appVersion?: string;
|
|
12
|
+
uploadProvider?: string;
|
|
13
|
+
uploadProviderToken?: string;
|
|
14
|
+
uploadProviderGateway?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { XmtpEnv } from "@xmtp/node-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* A Convos identity represents a single XMTP inbox used for one conversation.
|
|
4
|
+
* Following ADR 002: Per-Conversation Identity Model.
|
|
5
|
+
*
|
|
6
|
+
* Each identity has its own:
|
|
7
|
+
* - Wallet private key (secp256k1)
|
|
8
|
+
* - Database encryption key (32 bytes)
|
|
9
|
+
* - XMTP database (SQLite)
|
|
10
|
+
* - Conversation association (once linked)
|
|
11
|
+
*/
|
|
12
|
+
export interface Identity {
|
|
13
|
+
/** Unique identifier for this identity (hex) */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Wallet private key (hex with 0x prefix) */
|
|
16
|
+
walletKey: string;
|
|
17
|
+
/** Database encryption key (hex) */
|
|
18
|
+
dbEncryptionKey: string;
|
|
19
|
+
/** XMTP inbox ID (set after first client creation) */
|
|
20
|
+
inboxId?: string;
|
|
21
|
+
/** Conversation ID this identity is linked to */
|
|
22
|
+
conversationId?: string;
|
|
23
|
+
/** Invite tag used to join/create this conversation */
|
|
24
|
+
inviteTag?: string;
|
|
25
|
+
/** Human-readable label */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Profile name shared with conversation members */
|
|
28
|
+
profileName?: string;
|
|
29
|
+
/** Profile image URL */
|
|
30
|
+
profileImageUrl?: string;
|
|
31
|
+
/** Creation timestamp */
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
export interface IdentityStore {
|
|
35
|
+
list(): Identity[];
|
|
36
|
+
get(id: string): Identity | undefined;
|
|
37
|
+
getByConversationId(conversationId: string): Identity | undefined;
|
|
38
|
+
getAllByConversationId(conversationId: string): Identity[];
|
|
39
|
+
getByInviteTag(tag: string): Identity | undefined;
|
|
40
|
+
getUnlinked(): Identity[];
|
|
41
|
+
create(opts?: {
|
|
42
|
+
label?: string;
|
|
43
|
+
profileName?: string;
|
|
44
|
+
}): Identity;
|
|
45
|
+
update(id: string, updates: Partial<Identity>): Identity;
|
|
46
|
+
remove(id: string): void;
|
|
47
|
+
getDbPath(id: string, env: XmtpEnv): string;
|
|
48
|
+
}
|
|
49
|
+
export declare function createIdentityStore(baseDir?: string): IdentityStore;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { generatePrivateKey } from "viem/accounts";
|
|
5
|
+
const DEFAULT_CONVOS_HOME = join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".convos");
|
|
6
|
+
export function createIdentityStore(baseDir) {
|
|
7
|
+
const home = baseDir ?? DEFAULT_CONVOS_HOME;
|
|
8
|
+
function identitiesDir() {
|
|
9
|
+
const dir = join(home, "identities");
|
|
10
|
+
mkdirSync(dir, { recursive: true });
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
function identityPath(id) {
|
|
14
|
+
return join(identitiesDir(), `${id}.json`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
list() {
|
|
18
|
+
const dir = identitiesDir();
|
|
19
|
+
if (!existsSync(dir))
|
|
20
|
+
return [];
|
|
21
|
+
return readdirSync(dir)
|
|
22
|
+
.filter((f) => f.endsWith(".json"))
|
|
23
|
+
.map((f) => JSON.parse(readFileSync(join(dir, f), "utf-8")))
|
|
24
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
25
|
+
},
|
|
26
|
+
get(id) {
|
|
27
|
+
const path = identityPath(id);
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return undefined;
|
|
30
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
31
|
+
},
|
|
32
|
+
getByConversationId(conversationId) {
|
|
33
|
+
// Return the oldest identity for this conversation (first-created = most likely original)
|
|
34
|
+
const matches = this.list().filter((i) => i.conversationId === conversationId);
|
|
35
|
+
return matches.length > 0
|
|
36
|
+
? matches[matches.length - 1] // list() sorts newest-first, so last = oldest
|
|
37
|
+
: undefined;
|
|
38
|
+
},
|
|
39
|
+
getAllByConversationId(conversationId) {
|
|
40
|
+
return this.list().filter((i) => i.conversationId === conversationId);
|
|
41
|
+
},
|
|
42
|
+
getByInviteTag(tag) {
|
|
43
|
+
return this.list().find((i) => i.inviteTag === tag);
|
|
44
|
+
},
|
|
45
|
+
getUnlinked() {
|
|
46
|
+
return this.list().filter((i) => !i.conversationId);
|
|
47
|
+
},
|
|
48
|
+
create(opts) {
|
|
49
|
+
const id = randomBytes(16).toString("hex");
|
|
50
|
+
const identity = {
|
|
51
|
+
id,
|
|
52
|
+
walletKey: generatePrivateKey(),
|
|
53
|
+
dbEncryptionKey: randomBytes(32).toString("hex"),
|
|
54
|
+
label: opts?.label,
|
|
55
|
+
profileName: opts?.profileName,
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
writeFileSync(identityPath(id), JSON.stringify(identity, null, 2));
|
|
59
|
+
return identity;
|
|
60
|
+
},
|
|
61
|
+
update(id, updates) {
|
|
62
|
+
const existing = this.get(id);
|
|
63
|
+
if (!existing)
|
|
64
|
+
throw new Error(`Identity not found: ${id}`);
|
|
65
|
+
const updated = { ...existing, ...updates, id: existing.id };
|
|
66
|
+
writeFileSync(identityPath(id), JSON.stringify(updated, null, 2));
|
|
67
|
+
return updated;
|
|
68
|
+
},
|
|
69
|
+
remove(id) {
|
|
70
|
+
const path = identityPath(id);
|
|
71
|
+
if (existsSync(path))
|
|
72
|
+
rmSync(path);
|
|
73
|
+
// Clean up XMTP database files
|
|
74
|
+
const dbBaseDir = join(home, "db");
|
|
75
|
+
if (existsSync(dbBaseDir)) {
|
|
76
|
+
for (const envDir of readdirSync(dbBaseDir)) {
|
|
77
|
+
const envPath = join(dbBaseDir, envDir);
|
|
78
|
+
for (const file of readdirSync(envPath)) {
|
|
79
|
+
if (file.startsWith(id)) {
|
|
80
|
+
rmSync(join(envPath, file));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
getDbPath(id, env) {
|
|
87
|
+
const dir = join(home, "db", env);
|
|
88
|
+
mkdirSync(dir, { recursive: true });
|
|
89
|
+
return join(dir, `${id}.db3`);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|