@crmy/cli 0.5.1
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/package.json +26 -0
- package/src/client.ts +287 -0
- package/src/commands/accounts.ts +78 -0
- package/src/commands/activity-types.ts +59 -0
- package/src/commands/actors.ts +80 -0
- package/src/commands/assignments.ts +164 -0
- package/src/commands/auth.ts +161 -0
- package/src/commands/briefing.ts +41 -0
- package/src/commands/config.ts +25 -0
- package/src/commands/contacts.ts +83 -0
- package/src/commands/context-types.ts +56 -0
- package/src/commands/context.ts +181 -0
- package/src/commands/custom-fields.ts +63 -0
- package/src/commands/emails.ts +71 -0
- package/src/commands/events.ts +54 -0
- package/src/commands/help.ts +81 -0
- package/src/commands/hitl.ts +50 -0
- package/src/commands/init.ts +129 -0
- package/src/commands/mcp.ts +70 -0
- package/src/commands/migrate.ts +49 -0
- package/src/commands/notes.ts +88 -0
- package/src/commands/opps.ts +106 -0
- package/src/commands/pipeline.ts +26 -0
- package/src/commands/search.ts +45 -0
- package/src/commands/server.ts +32 -0
- package/src/commands/use-cases.ts +97 -0
- package/src/commands/webhooks.ts +95 -0
- package/src/commands/workflows.ts +119 -0
- package/src/config.ts +74 -0
- package/src/index.ts +80 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { loadConfigFile } from '../config.js';
|
|
7
|
+
|
|
8
|
+
export function mcpCommand(): Command {
|
|
9
|
+
return new Command('mcp')
|
|
10
|
+
.description('Start stdio MCP server (for Claude Code)')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const config = loadConfigFile();
|
|
13
|
+
|
|
14
|
+
const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
|
|
15
|
+
const apiKey = process.env.CRMY_API_KEY ?? config.apiKey;
|
|
16
|
+
|
|
17
|
+
if (!databaseUrl) {
|
|
18
|
+
console.error('No database URL. Run `crmy init` first or set DATABASE_URL.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.env.CRMY_IMPORTED = '1';
|
|
23
|
+
|
|
24
|
+
const { initPool, createMcpServer } = await import('@crmy/server');
|
|
25
|
+
const { runMigrations } = await import('@crmy/server');
|
|
26
|
+
|
|
27
|
+
const db = await initPool(databaseUrl);
|
|
28
|
+
await runMigrations(db);
|
|
29
|
+
|
|
30
|
+
// Resolve actor from API key or create default
|
|
31
|
+
let actor = {
|
|
32
|
+
tenant_id: '',
|
|
33
|
+
actor_id: 'cli-agent',
|
|
34
|
+
actor_type: 'agent' as const,
|
|
35
|
+
role: 'owner' as const,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (apiKey) {
|
|
39
|
+
const crypto = await import('node:crypto');
|
|
40
|
+
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
|
|
41
|
+
const result = await db.query(
|
|
42
|
+
`SELECT ak.tenant_id, ak.user_id, ak.scopes, u.role
|
|
43
|
+
FROM api_keys ak LEFT JOIN users u ON ak.user_id = u.id
|
|
44
|
+
WHERE ak.key_hash = $1`,
|
|
45
|
+
[keyHash],
|
|
46
|
+
);
|
|
47
|
+
if (result.rows.length > 0) {
|
|
48
|
+
const row = result.rows[0];
|
|
49
|
+
actor = {
|
|
50
|
+
tenant_id: row.tenant_id,
|
|
51
|
+
actor_id: row.user_id ?? 'api-key-agent',
|
|
52
|
+
actor_type: 'agent',
|
|
53
|
+
role: row.role ?? 'member',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback: use default tenant
|
|
59
|
+
if (!actor.tenant_id) {
|
|
60
|
+
const tenantResult = await db.query("SELECT id FROM tenants WHERE slug = 'default' LIMIT 1");
|
|
61
|
+
if (tenantResult.rows.length > 0) {
|
|
62
|
+
actor.tenant_id = tenantResult.rows[0].id;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const server = createMcpServer(db, () => actor);
|
|
67
|
+
const transport = new StdioServerTransport();
|
|
68
|
+
await server.connect(transport);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { loadConfigFile } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export function migrateCommand(): Command {
|
|
8
|
+
const cmd = new Command('migrate').description('Database migrations');
|
|
9
|
+
|
|
10
|
+
cmd.command('run')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const config = loadConfigFile();
|
|
13
|
+
const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
|
|
14
|
+
if (!databaseUrl) {
|
|
15
|
+
console.error('No database URL configured.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
process.env.CRMY_IMPORTED = '1';
|
|
19
|
+
const { initPool, closePool, runMigrations } = await import('@crmy/server');
|
|
20
|
+
const db = await initPool(databaseUrl);
|
|
21
|
+
const ran = await runMigrations(db);
|
|
22
|
+
if (ran.length === 0) {
|
|
23
|
+
console.log('No pending migrations.');
|
|
24
|
+
} else {
|
|
25
|
+
console.log(`Ran ${ran.length} migration(s): ${ran.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
await closePool();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
cmd.command('status')
|
|
31
|
+
.action(async () => {
|
|
32
|
+
const config = loadConfigFile();
|
|
33
|
+
const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
|
|
34
|
+
if (!databaseUrl) {
|
|
35
|
+
console.error('No database URL configured.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
process.env.CRMY_IMPORTED = '1';
|
|
39
|
+
const { initPool, closePool } = await import('@crmy/server');
|
|
40
|
+
const { getMigrationStatus } = await import('@crmy/server/dist/db/migrate.js');
|
|
41
|
+
const db = await initPool(databaseUrl);
|
|
42
|
+
const status = await getMigrationStatus(db);
|
|
43
|
+
console.log('Applied:', status.applied.length ? status.applied.join(', ') : '(none)');
|
|
44
|
+
console.log('Pending:', status.pending.length ? status.pending.join(', ') : '(none)');
|
|
45
|
+
await closePool();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return cmd;
|
|
49
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { getClient } from '../client.js';
|
|
6
|
+
|
|
7
|
+
export function notesCommand(): Command {
|
|
8
|
+
const cmd = new Command('notes').description('Manage notes and comments on CRM objects');
|
|
9
|
+
|
|
10
|
+
cmd.command('list <object_type> <object_id>')
|
|
11
|
+
.description('List notes for an object (contact, account, opportunity, use_case)')
|
|
12
|
+
.option('--visibility <vis>', 'Filter: internal or external')
|
|
13
|
+
.option('--pinned', 'Show only pinned notes')
|
|
14
|
+
.action(async (objectType, objectId, opts) => {
|
|
15
|
+
const client = await getClient();
|
|
16
|
+
const result = await client.call('note_list', {
|
|
17
|
+
object_type: objectType,
|
|
18
|
+
object_id: objectId,
|
|
19
|
+
visibility: opts.visibility,
|
|
20
|
+
pinned: opts.pinned ?? undefined,
|
|
21
|
+
limit: 20,
|
|
22
|
+
});
|
|
23
|
+
const data = JSON.parse(result);
|
|
24
|
+
if (data.notes?.length === 0) {
|
|
25
|
+
console.log('No notes found.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const n of data.notes ?? []) {
|
|
29
|
+
const pin = n.pinned ? ' 📌' : '';
|
|
30
|
+
const vis = n.visibility === 'external' ? ' [external]' : '';
|
|
31
|
+
console.log(` [${(n.id as string).slice(0, 8)}]${pin}${vis} ${n.author_type}/${n.author_id ?? 'anon'}`);
|
|
32
|
+
console.log(` ${n.body.slice(0, 120)}`);
|
|
33
|
+
console.log(` ${n.created_at}\n`);
|
|
34
|
+
}
|
|
35
|
+
if (data.total > 20) console.log(` Showing 20 of ${data.total} notes`);
|
|
36
|
+
await client.close();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
cmd.command('add <object_type> <object_id>')
|
|
40
|
+
.description('Add a note to an object')
|
|
41
|
+
.option('--parent <id>', 'Reply to a note (thread)')
|
|
42
|
+
.option('--external', 'Make note visible externally')
|
|
43
|
+
.option('--pin', 'Pin this note')
|
|
44
|
+
.action(async (objectType, objectId, opts) => {
|
|
45
|
+
const { default: inquirer } = await import('inquirer');
|
|
46
|
+
const answers = await inquirer.prompt([
|
|
47
|
+
{ type: 'input', name: 'body', message: 'Note:' },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const client = await getClient();
|
|
51
|
+
const result = await client.call('note_create', {
|
|
52
|
+
object_type: objectType,
|
|
53
|
+
object_id: objectId,
|
|
54
|
+
body: answers.body,
|
|
55
|
+
parent_id: opts.parent,
|
|
56
|
+
visibility: opts.external ? 'external' : 'internal',
|
|
57
|
+
pinned: opts.pin ?? false,
|
|
58
|
+
});
|
|
59
|
+
const data = JSON.parse(result);
|
|
60
|
+
console.log(`\n Note created: ${data.note.id}\n`);
|
|
61
|
+
await client.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
cmd.command('get <id>')
|
|
65
|
+
.action(async (id) => {
|
|
66
|
+
const client = await getClient();
|
|
67
|
+
const result = await client.call('note_get', { id });
|
|
68
|
+
const data = JSON.parse(result);
|
|
69
|
+
console.log(`\n ${data.note.body}\n`);
|
|
70
|
+
if (data.replies?.length > 0) {
|
|
71
|
+
console.log(` ${data.replies.length} replies:`);
|
|
72
|
+
for (const r of data.replies) {
|
|
73
|
+
console.log(` [${(r.id as string).slice(0, 8)}] ${r.body.slice(0, 80)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await client.close();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
cmd.command('delete <id>')
|
|
80
|
+
.action(async (id) => {
|
|
81
|
+
const client = await getClient();
|
|
82
|
+
const result = await client.call('note_delete', { id });
|
|
83
|
+
console.log(JSON.parse(result));
|
|
84
|
+
await client.close();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return cmd;
|
|
88
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { getClient } from '../client.js';
|
|
6
|
+
|
|
7
|
+
export function oppsCommand(): Command {
|
|
8
|
+
const cmd = new Command('opps').description('Manage opportunities');
|
|
9
|
+
|
|
10
|
+
cmd.command('list')
|
|
11
|
+
.option('--stage <stage>', 'Filter by stage')
|
|
12
|
+
.option('-q, --query <query>', 'Search query')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const client = await getClient();
|
|
15
|
+
const result = await client.call('opportunity_search', { query: opts.query, stage: opts.stage, limit: 20 });
|
|
16
|
+
const data = JSON.parse(result);
|
|
17
|
+
if (data.opportunities?.length === 0) {
|
|
18
|
+
console.log('No opportunities found.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.table(data.opportunities?.map((o: Record<string, unknown>) => ({
|
|
22
|
+
id: (o.id as string).slice(0, 8),
|
|
23
|
+
name: o.name,
|
|
24
|
+
stage: o.stage,
|
|
25
|
+
amount: o.amount ?? 0,
|
|
26
|
+
close_date: o.close_date ?? '',
|
|
27
|
+
})));
|
|
28
|
+
if (data.total > 20) console.log(`\n Showing 20 of ${data.total} opportunities`);
|
|
29
|
+
await client.close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
cmd.command('get <id>')
|
|
33
|
+
.description('Get opportunity details')
|
|
34
|
+
.action(async (id) => {
|
|
35
|
+
const client = await getClient();
|
|
36
|
+
const result = await client.call('opportunity_get', { id });
|
|
37
|
+
console.log(JSON.parse(result));
|
|
38
|
+
await client.close();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
cmd.command('create')
|
|
42
|
+
.description('Create a new opportunity')
|
|
43
|
+
.action(async () => {
|
|
44
|
+
const { default: inquirer } = await import('inquirer');
|
|
45
|
+
const answers = await inquirer.prompt([
|
|
46
|
+
{ type: 'input', name: 'name', message: 'Opportunity name:' },
|
|
47
|
+
{ type: 'input', name: 'account_id', message: 'Account ID (optional):' },
|
|
48
|
+
{ type: 'input', name: 'amount', message: 'Amount (optional):' },
|
|
49
|
+
{
|
|
50
|
+
type: 'list',
|
|
51
|
+
name: 'stage',
|
|
52
|
+
message: 'Stage:',
|
|
53
|
+
choices: ['prospecting', 'qualification', 'proposal', 'negotiation', 'closed_won', 'closed_lost'],
|
|
54
|
+
default: 'prospecting',
|
|
55
|
+
},
|
|
56
|
+
{ type: 'input', name: 'close_date', message: 'Close date YYYY-MM-DD (optional):' },
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const client = await getClient();
|
|
60
|
+
const result = await client.call('opportunity_create', {
|
|
61
|
+
name: answers.name,
|
|
62
|
+
account_id: answers.account_id || undefined,
|
|
63
|
+
amount: answers.amount ? parseFloat(answers.amount) : undefined,
|
|
64
|
+
stage: answers.stage,
|
|
65
|
+
close_date: answers.close_date || undefined,
|
|
66
|
+
});
|
|
67
|
+
const data = JSON.parse(result);
|
|
68
|
+
console.log(`\n Created opportunity: ${data.opportunity.id}\n`);
|
|
69
|
+
await client.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
cmd.command('advance <id> <stage>')
|
|
73
|
+
.description('Advance opportunity to a new stage')
|
|
74
|
+
.option('--note <note>', 'Optional note')
|
|
75
|
+
.option('--lost-reason <reason>', 'Required when stage is closed_lost')
|
|
76
|
+
.action(async (id, stage, opts) => {
|
|
77
|
+
const client = await getClient();
|
|
78
|
+
const result = await client.call('opportunity_advance_stage', {
|
|
79
|
+
id,
|
|
80
|
+
stage,
|
|
81
|
+
note: opts.note,
|
|
82
|
+
lost_reason: opts.lostReason,
|
|
83
|
+
});
|
|
84
|
+
const data = JSON.parse(result);
|
|
85
|
+
console.log(` Stage updated to: ${data.opportunity.stage}`);
|
|
86
|
+
await client.close();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
cmd.command('delete <id>')
|
|
90
|
+
.description('Permanently delete an opportunity (admin/owner only)')
|
|
91
|
+
.action(async (id) => {
|
|
92
|
+
const { default: inquirer } = await import('inquirer');
|
|
93
|
+
const { confirm } = await inquirer.prompt([
|
|
94
|
+
{ type: 'confirm', name: 'confirm', message: `Delete opportunity ${id}? This cannot be undone.`, default: false },
|
|
95
|
+
]);
|
|
96
|
+
if (!confirm) { console.log(' Cancelled.'); return; }
|
|
97
|
+
|
|
98
|
+
const client = await getClient();
|
|
99
|
+
const result = await client.call('opportunity_delete', { id });
|
|
100
|
+
const data = JSON.parse(result);
|
|
101
|
+
if (data.deleted) console.log(` Deleted.`);
|
|
102
|
+
await client.close();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return cmd;
|
|
106
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { getClient } from '../client.js';
|
|
6
|
+
|
|
7
|
+
export function pipelineCommand(): Command {
|
|
8
|
+
return new Command('pipeline')
|
|
9
|
+
.description('Show pipeline summary by stage')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const client = await getClient();
|
|
12
|
+
const result = await client.call('pipeline_summary', { group_by: 'stage' });
|
|
13
|
+
const data = JSON.parse(result);
|
|
14
|
+
|
|
15
|
+
console.log(`\n Pipeline: $${(data.total_value / 100).toLocaleString()} across ${data.count} opportunities\n`);
|
|
16
|
+
|
|
17
|
+
if (data.by_stage?.length > 0) {
|
|
18
|
+
console.table(data.by_stage.map((s: Record<string, unknown>) => ({
|
|
19
|
+
stage: s.stage,
|
|
20
|
+
count: s.count,
|
|
21
|
+
value: `$${((s.value as number) / 100).toLocaleString()}`,
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
24
|
+
await client.close();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { getClient } from '../client.js';
|
|
6
|
+
|
|
7
|
+
export function searchCommand(): Command {
|
|
8
|
+
return new Command('search')
|
|
9
|
+
.description('Cross-entity search')
|
|
10
|
+
.argument('<query>', 'Search query')
|
|
11
|
+
.action(async (query) => {
|
|
12
|
+
const client = await getClient();
|
|
13
|
+
const result = await client.call('crm_search', { query, limit: 10 });
|
|
14
|
+
const data = JSON.parse(result);
|
|
15
|
+
|
|
16
|
+
if (data.contacts?.length > 0) {
|
|
17
|
+
console.log('\n Contacts:');
|
|
18
|
+
console.table(data.contacts.map((c: Record<string, unknown>) => ({
|
|
19
|
+
id: (c.id as string).slice(0, 8),
|
|
20
|
+
name: `${c.first_name} ${c.last_name}`,
|
|
21
|
+
email: c.email ?? '',
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
24
|
+
if (data.accounts?.length > 0) {
|
|
25
|
+
console.log('\n Accounts:');
|
|
26
|
+
console.table(data.accounts.map((a: Record<string, unknown>) => ({
|
|
27
|
+
id: (a.id as string).slice(0, 8),
|
|
28
|
+
name: a.name,
|
|
29
|
+
})));
|
|
30
|
+
}
|
|
31
|
+
if (data.opportunities?.length > 0) {
|
|
32
|
+
console.log('\n Opportunities:');
|
|
33
|
+
console.table(data.opportunities.map((o: Record<string, unknown>) => ({
|
|
34
|
+
id: (o.id as string).slice(0, 8),
|
|
35
|
+
name: o.name,
|
|
36
|
+
stage: o.stage,
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const total = (data.contacts?.length ?? 0) + (data.accounts?.length ?? 0) + (data.opportunities?.length ?? 0);
|
|
41
|
+
if (total === 0) console.log(' No results found.');
|
|
42
|
+
|
|
43
|
+
await client.close();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { loadConfigFile } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export function serverCommand(): Command {
|
|
8
|
+
return new Command('server')
|
|
9
|
+
.description('Start the crmy HTTP server')
|
|
10
|
+
.option('--port <port>', 'HTTP port', '3000')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const config = loadConfigFile();
|
|
13
|
+
|
|
14
|
+
process.env.DATABASE_URL = config.database?.url ?? process.env.DATABASE_URL;
|
|
15
|
+
process.env.JWT_SECRET = config.jwtSecret ?? process.env.JWT_SECRET ?? 'dev-secret';
|
|
16
|
+
process.env.PORT = opts.port;
|
|
17
|
+
process.env.CRMY_IMPORTED = '1';
|
|
18
|
+
|
|
19
|
+
if (!process.env.DATABASE_URL) {
|
|
20
|
+
console.error('No database URL. Run `crmy init` first or set DATABASE_URL.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { createApp, loadConfig } = await import('@crmy/server');
|
|
25
|
+
const serverConfig = loadConfig();
|
|
26
|
+
const { app } = await createApp(serverConfig);
|
|
27
|
+
|
|
28
|
+
app.listen(serverConfig.port, () => {
|
|
29
|
+
console.log(`crmy server ready on :${serverConfig.port}`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { getClient } from '../client.js';
|
|
6
|
+
|
|
7
|
+
export function useCasesCommand(): Command {
|
|
8
|
+
const cmd = new Command('use-cases').description('Manage use cases');
|
|
9
|
+
|
|
10
|
+
cmd.command('list')
|
|
11
|
+
.option('--account <id>', 'Filter by account ID')
|
|
12
|
+
.option('--stage <stage>', 'Filter by stage')
|
|
13
|
+
.option('-q, --query <query>', 'Search query')
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
const client = await getClient();
|
|
16
|
+
const result = await client.call('use_case_search', {
|
|
17
|
+
account_id: opts.account,
|
|
18
|
+
stage: opts.stage,
|
|
19
|
+
query: opts.query,
|
|
20
|
+
limit: 20,
|
|
21
|
+
});
|
|
22
|
+
const data = JSON.parse(result);
|
|
23
|
+
if (data.use_cases?.length === 0) {
|
|
24
|
+
console.log('No use cases found.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.table(data.use_cases?.map((uc: Record<string, unknown>) => ({
|
|
28
|
+
id: (uc.id as string).slice(0, 8),
|
|
29
|
+
name: uc.name,
|
|
30
|
+
stage: uc.stage,
|
|
31
|
+
arr: uc.attributed_arr ?? '',
|
|
32
|
+
health: uc.health_score ?? '',
|
|
33
|
+
})));
|
|
34
|
+
if (data.total > 20) console.log(`\n Showing 20 of ${data.total} use cases`);
|
|
35
|
+
await client.close();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
cmd.command('get <id>')
|
|
39
|
+
.action(async (id) => {
|
|
40
|
+
const client = await getClient();
|
|
41
|
+
const result = await client.call('use_case_get', { id });
|
|
42
|
+
console.log(JSON.parse(result));
|
|
43
|
+
await client.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
cmd.command('create')
|
|
47
|
+
.action(async () => {
|
|
48
|
+
const { default: inquirer } = await import('inquirer');
|
|
49
|
+
const answers = await inquirer.prompt([
|
|
50
|
+
{ type: 'input', name: 'account_id', message: 'Account ID:' },
|
|
51
|
+
{ type: 'input', name: 'name', message: 'Use case name:' },
|
|
52
|
+
{ type: 'input', name: 'description', message: 'Description:' },
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const client = await getClient();
|
|
56
|
+
const result = await client.call('use_case_create', {
|
|
57
|
+
account_id: answers.account_id,
|
|
58
|
+
name: answers.name,
|
|
59
|
+
description: answers.description || undefined,
|
|
60
|
+
});
|
|
61
|
+
const data = JSON.parse(result);
|
|
62
|
+
console.log(`\n Created use case: ${data.use_case.id}\n`);
|
|
63
|
+
await client.close();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
cmd.command('summary')
|
|
67
|
+
.option('--account <id>', 'Filter by account ID')
|
|
68
|
+
.option('--group-by <field>', 'Group by: stage, product_line, owner', 'stage')
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
const client = await getClient();
|
|
71
|
+
const result = await client.call('use_case_summary', {
|
|
72
|
+
account_id: opts.account,
|
|
73
|
+
group_by: opts.groupBy,
|
|
74
|
+
});
|
|
75
|
+
const data = JSON.parse(result);
|
|
76
|
+
console.table(data.summary);
|
|
77
|
+
await client.close();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
cmd.command('delete <id>')
|
|
81
|
+
.description('Delete a use case (admin/owner only)')
|
|
82
|
+
.action(async (id) => {
|
|
83
|
+
const { default: inquirer } = await import('inquirer');
|
|
84
|
+
const { confirm } = await inquirer.prompt([
|
|
85
|
+
{ type: 'confirm', name: 'confirm', message: `Delete use case ${id}? This cannot be undone.`, default: false },
|
|
86
|
+
]);
|
|
87
|
+
if (!confirm) { console.log(' Cancelled.'); return; }
|
|
88
|
+
|
|
89
|
+
const client = await getClient();
|
|
90
|
+
const result = await client.call('use_case_delete', { id });
|
|
91
|
+
const data = JSON.parse(result);
|
|
92
|
+
if (data.deleted) console.log(` Deleted.`);
|
|
93
|
+
await client.close();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return cmd;
|
|
97
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { getClient } from '../client.js';
|
|
6
|
+
|
|
7
|
+
export function webhooksCommand(): Command {
|
|
8
|
+
const cmd = new Command('webhooks').description('Manage webhook endpoints');
|
|
9
|
+
|
|
10
|
+
cmd.command('list')
|
|
11
|
+
.option('--active', 'Show only active webhooks')
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const client = await getClient();
|
|
14
|
+
const result = await client.call('webhook_list', {
|
|
15
|
+
active: opts.active ?? undefined,
|
|
16
|
+
limit: 20,
|
|
17
|
+
});
|
|
18
|
+
const data = JSON.parse(result);
|
|
19
|
+
if (data.webhooks?.length === 0) {
|
|
20
|
+
console.log('No webhooks found.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.table(data.webhooks?.map((w: Record<string, unknown>) => ({
|
|
24
|
+
id: (w.id as string).slice(0, 8),
|
|
25
|
+
url: w.url,
|
|
26
|
+
events: (w.event_types as string[])?.join(', '),
|
|
27
|
+
active: w.is_active,
|
|
28
|
+
})));
|
|
29
|
+
await client.close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
cmd.command('get <id>')
|
|
33
|
+
.action(async (id) => {
|
|
34
|
+
const client = await getClient();
|
|
35
|
+
const result = await client.call('webhook_get', { id });
|
|
36
|
+
console.log(JSON.parse(result));
|
|
37
|
+
await client.close();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
cmd.command('create')
|
|
41
|
+
.action(async () => {
|
|
42
|
+
const { default: inquirer } = await import('inquirer');
|
|
43
|
+
const answers = await inquirer.prompt([
|
|
44
|
+
{ type: 'input', name: 'url', message: 'Webhook URL:' },
|
|
45
|
+
{ type: 'input', name: 'events', message: 'Events (comma-separated):' },
|
|
46
|
+
{ type: 'input', name: 'description', message: 'Description:' },
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const client = await getClient();
|
|
50
|
+
const result = await client.call('webhook_create', {
|
|
51
|
+
url: answers.url,
|
|
52
|
+
events: answers.events.split(',').map((e: string) => e.trim()),
|
|
53
|
+
description: answers.description || undefined,
|
|
54
|
+
});
|
|
55
|
+
const data = JSON.parse(result);
|
|
56
|
+
console.log(`\n Created webhook: ${data.webhook.id}\n`);
|
|
57
|
+
console.log(` Secret: ${data.webhook.secret}\n`);
|
|
58
|
+
await client.close();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
cmd.command('delete <id>')
|
|
62
|
+
.action(async (id) => {
|
|
63
|
+
const client = await getClient();
|
|
64
|
+
const result = await client.call('webhook_delete', { id });
|
|
65
|
+
console.log(JSON.parse(result));
|
|
66
|
+
await client.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
cmd.command('deliveries')
|
|
70
|
+
.option('--endpoint <id>', 'Filter by endpoint ID')
|
|
71
|
+
.option('--status <status>', 'Filter by status (pending, success, failed)')
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
const client = await getClient();
|
|
74
|
+
const result = await client.call('webhook_list_deliveries', {
|
|
75
|
+
endpoint_id: opts.endpoint,
|
|
76
|
+
status: opts.status,
|
|
77
|
+
limit: 20,
|
|
78
|
+
});
|
|
79
|
+
const data = JSON.parse(result);
|
|
80
|
+
if (data.deliveries?.length === 0) {
|
|
81
|
+
console.log('No deliveries found.');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.table(data.deliveries?.map((d: Record<string, unknown>) => ({
|
|
85
|
+
id: (d.id as string).slice(0, 8),
|
|
86
|
+
event_type: d.event_type,
|
|
87
|
+
status: d.status,
|
|
88
|
+
attempts: d.attempt_count,
|
|
89
|
+
created: d.created_at,
|
|
90
|
+
})));
|
|
91
|
+
await client.close();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return cmd;
|
|
95
|
+
}
|