@tightknitai/cli 0.1.0-alpha.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/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # @tightknitai/cli
2
+
3
+ Command-line interface and MCP server for the [Tightknit API](https://docs.tightknit.ai/api-reference/introduction).
4
+
5
+ ## Setup
6
+
7
+ ### Prerequisites
8
+
9
+ - Node.js 18+ (for native `fetch`)
10
+ - A Tightknit API key
11
+
12
+ ### Create an API Key
13
+
14
+ 1. Go to **Studio > Settings > Integrations > API Keys**
15
+ 2. Click **Create API key**
16
+ 3. Give it a descriptive name (e.g. "CLI")
17
+ 4. Select the permissions for the APIs the key needs access to. Enable all permissions the CLI commands you plan to use require:
18
+ - **Calendar Events** — `events list`, `events get`, `events create`, `events delete`, `events update-attendee`
19
+ - **Feeds** — `feeds list`, `feeds get`, `feeds posts`
20
+ - **Posts** — `posts get`
21
+ - **Members** — `members add` (Enterprise), `members check`
22
+ - **Messages** — `messages send`
23
+ - **Groups** — `groups add-member`
24
+ - **Awards** — `awards assign`
25
+ - **Search** — `search query` (Beta)
26
+ 5. Copy the key (starts with `sk_`) — it is only shown once
27
+
28
+ ### Install and Configure
29
+
30
+ ```bash
31
+ # From the monorepo root
32
+ pnpm install
33
+
34
+ # Set your API key
35
+ npx tsx packages/tightknit-cli/bin/tightknit.ts config set api-key sk_your_key_here
36
+ ```
37
+
38
+ The API key is stored locally at `~/.config/tightknit/config.json`.
39
+
40
+ ## Usage
41
+
42
+ ### CLI Commands
43
+
44
+ ```bash
45
+ # Alias for convenience
46
+ alias tightknit="npx tsx packages/tightknit-cli/bin/tightknit.ts"
47
+
48
+ # Calendar Events
49
+ tightknit events list
50
+ tightknit events list --time-filter upcoming --status published --json
51
+ tightknit events get <event-id>
52
+ tightknit events create --title "Meetup" --description "Join us" --start-date "2025-06-01T18:00:00Z" --end-date "2025-06-01T20:00:00Z"
53
+ tightknit events delete <event-id>
54
+ tightknit events update-attendee <event-id> --email user@example.com --personal-join-link "https://zoom.us/..."
55
+
56
+ # Awards
57
+ tightknit awards assign <award-uuid> --recipient-email user@example.com
58
+
59
+ # Feeds & Posts
60
+ tightknit feeds list
61
+ tightknit feeds get <feed-id>
62
+ tightknit feeds posts <feed-id> --sort newest
63
+ tightknit posts get <post-id>
64
+
65
+ # Members
66
+ tightknit members add --email user@example.com --full-name "Jane Doe"
67
+ tightknit members check user@example.com
68
+
69
+ # Messages
70
+ tightknit messages send --channel C0123456 --text "Hello from CLI!"
71
+
72
+ # Groups
73
+ tightknit groups add-member <group-id> --email user@example.com
74
+
75
+ # Search (Beta)
76
+ tightknit search query "community meetup" --type post
77
+
78
+ # Config
79
+ tightknit config set api-key <key>
80
+ tightknit config get api-key
81
+ tightknit config set default-output json
82
+ ```
83
+
84
+ Every command supports `--json` for structured JSON output and `--help` for usage info.
85
+
86
+ ### MCP Server (for Claude Code)
87
+
88
+ The MCP server exposes all CLI commands as tools over STDIO.
89
+
90
+ Add to your Claude Code MCP config (`.claude/settings.json` or `~/.claude.json`):
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "tightknit": {
96
+ "command": "npx",
97
+ "args": ["--yes", "@tightknitai/cli@alpha", "--", "tightknit-mcp"]
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ Or if installed locally in the monorepo:
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "tightknit": {
109
+ "command": "npx",
110
+ "args": ["tsx", "packages/tightknit-cli/src/mcp/server.ts"],
111
+ "cwd": "/path/to/colombo"
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ This gives Claude access to 17 tools: `tightknit_events_list`, `tightknit_events_create`, `tightknit_feeds_list`, `tightknit_messages_send`, etc.
118
+
119
+ The MCP server reads the same API key from `~/.config/tightknit/config.json`, so run `tightknit config set api-key` first.
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ # Run tests
125
+ pnpm --filter @tightknitai/cli test
126
+
127
+ # Type check
128
+ pnpm --filter @tightknitai/cli typecheck
129
+
130
+ # Run CLI in dev mode
131
+ npx tsx packages/tightknit-cli/bin/tightknit.ts --help
132
+ ```
133
+
134
+ ## API Coverage
135
+
136
+ All endpoints from the [Tightknit Admin API](https://api.tightknit.ai/doc) (`/admin/v0/`) are supported:
137
+
138
+ | Method | Path | CLI Command |
139
+ |--------|------|-------------|
140
+ | GET | `/calendar_events` | `events list` |
141
+ | POST | `/calendar_events` | `events create` |
142
+ | GET | `/calendar_events/:id` | `events get` |
143
+ | DELETE | `/calendar_events/:id` | `events delete` |
144
+ | PATCH | `/calendar_events/:id/attendees` | `events update-attendee` |
145
+ | POST | `/awards/:id/assign` | `awards assign` |
146
+ | GET | `/feeds` | `feeds list` |
147
+ | GET | `/feeds/:id` | `feeds get` |
148
+ | GET | `/feeds/:id/posts` | `feeds posts` |
149
+ | GET | `/posts/:id` | `posts get` |
150
+ | POST | `/members` | `members add` |
151
+ | POST | `/members/check` | `members check` |
152
+ | POST | `/messages` | `messages send` |
153
+ | POST | `/groups/:id/members` | `groups add-member` |
154
+ | GET | `/search` | `search query` |
155
+
156
+ Not yet implemented: `POST /files` (file upload — requires multipart form handling).
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const tsx = join(__dirname, "..", "node_modules", ".bin", "tsx");
8
+ const script = join(__dirname, "..", "src", "mcp", "server.ts");
9
+
10
+ try {
11
+ execFileSync(tsx, [script], { stdio: "inherit" });
12
+ } catch (err) {
13
+ process.exit(err.status ?? 1);
14
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const tsx = join(__dirname, "..", "node_modules", ".bin", "tsx");
8
+ const script = join(__dirname, "tightknit.ts");
9
+
10
+ try {
11
+ execFileSync(tsx, [script, ...process.argv.slice(2)], { stdio: "inherit" });
12
+ } catch (err) {
13
+ process.exit(err.status ?? 1);
14
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env tsx
2
+ import { createProgram } from "../src/cli/index";
3
+
4
+ const program = createProgram();
5
+
6
+ // Set version from package.json dynamically
7
+ // Using a static version here to avoid JSON import complexity
8
+ program.version("0.1.0-alpha.0");
9
+
10
+ program.parseAsync(process.argv).catch(() => {
11
+ process.exit(1);
12
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@tightknitai/cli",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "CLI and MCP server for the Tightknit API",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=18.0.0"
9
+ },
10
+ "bin": {
11
+ "tightknit": "bin/tightknit.js",
12
+ "tightknit-mcp": "bin/tightknit-mcp.js"
13
+ },
14
+ "exports": {
15
+ ".": "./src/cli/index.ts",
16
+ "./mcp": "./src/mcp/server.ts"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "dev": "tsx --watch bin/tightknit.ts",
26
+ "start": "tsx bin/tightknit.ts",
27
+ "start:mcp": "tsx src/mcp/server.ts",
28
+ "test": "vitest run",
29
+ "test:unit": "vitest run",
30
+ "test:unit-silent": "vitest run --reporter=dot",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.12.1",
35
+ "chalk": "^5.4.1",
36
+ "cli-table3": "^0.6.5",
37
+ "commander": "^12.1.0",
38
+ "conf": "^13.0.1",
39
+ "tsx": "^4.19.4",
40
+ "zod": "^3.24.4"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.15.2",
44
+ "typescript": "^5.8.3",
45
+ "vitest": "^3.1.2"
46
+ }
47
+ }
@@ -0,0 +1,28 @@
1
+ import { Command } from "commander";
2
+ import { assignAward } from "../../core/client";
3
+ import { printOutput, printSuccess } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const assignAwardCommand = new Command("assign")
7
+ .description("Assign an award to a user")
8
+ .argument("<award-id>", "Award UUID")
9
+ .requiredOption("--recipient-email <email>", "Recipient email address")
10
+ .option("--sender-email <email>", "Sender email address")
11
+ .option("--anonymous", "Send the award anonymously")
12
+ .option("--json", "Output as JSON")
13
+ .action(async (awardId: string, opts) => {
14
+ try {
15
+ const result = await assignAward(awardId, {
16
+ recipient: { email: opts.recipientEmail },
17
+ sender: opts.senderEmail ? { email: opts.senderEmail } : undefined,
18
+ send_anonymously: opts.anonymous,
19
+ });
20
+ if (opts.json) {
21
+ printOutput(result, { json: true });
22
+ } else {
23
+ printSuccess("Award assigned successfully");
24
+ }
25
+ } catch (error) {
26
+ handleCommandError(error, opts.json);
27
+ }
28
+ });
@@ -0,0 +1,27 @@
1
+ import { Command } from "commander";
2
+ import { getConfigPath, getConfigValue } from "../../core/config";
3
+ import { printOutput } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const getConfigCommand = new Command("get")
7
+ .description("Get a configuration value")
8
+ .argument("<key>", "Config key (api-key, community-id, default-output)")
9
+ .option("--json", "Output as JSON")
10
+ .option("--show-path", "Show the config file path")
11
+ .action(async (key: string, opts) => {
12
+ try {
13
+ if (opts.showPath) {
14
+ console.log(getConfigPath());
15
+ return;
16
+ }
17
+ const value = getConfigValue(key);
18
+ const displayValue = key === "api-key" && value ? `${value.slice(0, 8)}...` : value;
19
+ if (opts.json) {
20
+ printOutput({ key, value: displayValue }, { json: true });
21
+ } else {
22
+ console.log(displayValue || "(not set)");
23
+ }
24
+ } catch (error) {
25
+ handleCommandError(error, opts.json);
26
+ }
27
+ });
@@ -0,0 +1,23 @@
1
+ import { Command } from "commander";
2
+ import { setConfigValue } from "../../core/config";
3
+ import { printSuccess } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const setConfigCommand = new Command("set")
7
+ .description("Set a configuration value")
8
+ .argument("<key>", "Config key (api-key, community-id, default-output)")
9
+ .argument("<value>", "Config value")
10
+ .option("--json", "Output as JSON")
11
+ .action(async (key: string, value: string, opts) => {
12
+ try {
13
+ setConfigValue(key, value);
14
+ if (opts.json) {
15
+ console.log(JSON.stringify({ success: true, key, value: key === "api-key" ? "***" : value }, null, 2));
16
+ } else {
17
+ const displayValue = key === "api-key" ? "***" : value;
18
+ printSuccess(`Set ${key} = ${displayValue}`);
19
+ }
20
+ } catch (error) {
21
+ handleCommandError(error, opts.json);
22
+ }
23
+ });
@@ -0,0 +1,26 @@
1
+ import { TightknitApiError } from "../core/client";
2
+ import { printError } from "../core/output";
3
+
4
+ /** Handle errors in CLI commands with consistent formatting */
5
+ export function handleCommandError(error: unknown, json?: boolean): never {
6
+ if (error instanceof TightknitApiError) {
7
+ if (json) {
8
+ console.error(JSON.stringify(error.toJSON(), null, 2));
9
+ } else {
10
+ printError(error.message);
11
+ }
12
+ process.exit(1);
13
+ }
14
+
15
+ if (error instanceof Error) {
16
+ if (json) {
17
+ console.error(JSON.stringify({ error: true, message: error.message }, null, 2));
18
+ } else {
19
+ printError(error.message);
20
+ }
21
+ process.exit(1);
22
+ }
23
+
24
+ printError("An unknown error occurred");
25
+ process.exit(1);
26
+ }
@@ -0,0 +1,45 @@
1
+ import { Command } from "commander";
2
+ import { createEvent } from "../../core/client";
3
+ import { printOutput, printSuccess } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const createEventCommand = new Command("create")
7
+ .description("Create a new calendar event")
8
+ .requiredOption("--title <title>", "Event title (3-70 chars)")
9
+ .requiredOption("--description <text>", "Event description")
10
+ .requiredOption("--start-date <datetime>", "Start date/time (ISO 8601)")
11
+ .requiredOption("--end-date <datetime>", "End date/time (ISO 8601)")
12
+ .option("--location <location>", "Event location")
13
+ .option("--link <url>", "Event link/URL")
14
+ .option("--slug <slug>", "URL slug (3-70 chars)")
15
+ .option("--status <status>", "Status: needs_approval or published", "published")
16
+ .option("--publish-to-site", "Publish to companion site")
17
+ .option("--no-publish-to-site", "Do not publish to companion site")
18
+ .option("--enable-registration", "Enable registration button")
19
+ .option("--triggers-webhooks", "Trigger webhooks on creation")
20
+ .option("--json", "Output as JSON")
21
+ .action(async (opts) => {
22
+ try {
23
+ const result = await createEvent({
24
+ title: opts.title,
25
+ description: opts.description,
26
+ start_date: opts.startDate,
27
+ end_date: opts.endDate,
28
+ location: opts.location,
29
+ link: opts.link,
30
+ slug: opts.slug,
31
+ status: opts.status,
32
+ publish_to_site: opts.publishToSite,
33
+ enable_registration_button: opts.enableRegistration,
34
+ triggers_webhooks: opts.triggersWebhooks,
35
+ });
36
+ if (opts.json) {
37
+ printOutput(result, { json: true });
38
+ } else {
39
+ printSuccess(`Event created: ${result.data.calendar_event_id}`);
40
+ printOutput(result, {});
41
+ }
42
+ } catch (error) {
43
+ handleCommandError(error, opts.json);
44
+ }
45
+ });
@@ -0,0 +1,21 @@
1
+ import { Command } from "commander";
2
+ import { deleteEvent } from "../../core/client";
3
+ import { printSuccess } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const deleteEventCommand = new Command("delete")
7
+ .description("Delete a calendar event")
8
+ .argument("<id>", "Event ID")
9
+ .option("--json", "Output as JSON")
10
+ .action(async (id: string, opts) => {
11
+ try {
12
+ await deleteEvent(id);
13
+ if (opts.json) {
14
+ console.log(JSON.stringify({ success: true, message: "Event deleted" }, null, 2));
15
+ } else {
16
+ printSuccess("Event deleted successfully");
17
+ }
18
+ } catch (error) {
19
+ handleCommandError(error, opts.json);
20
+ }
21
+ });
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ import { getEvent } from "../../core/client";
3
+ import { printOutput } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const getEventCommand = new Command("get")
7
+ .description("Get a calendar event by ID")
8
+ .argument("<id>", "Event ID")
9
+ .option("--json", "Output as JSON")
10
+ .action(async (id: string, opts) => {
11
+ try {
12
+ const result = await getEvent(id);
13
+ printOutput(result, { json: opts.json });
14
+ } catch (error) {
15
+ handleCommandError(error, opts.json);
16
+ }
17
+ });
@@ -0,0 +1,29 @@
1
+ import { Command } from "commander";
2
+ import { listEvents } from "../../core/client";
3
+ import { printOutput } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const listEventsCommand = new Command("list")
7
+ .description("List calendar events")
8
+ .option("--page <number>", "Page number (0-indexed)", "0")
9
+ .option("--per-page <number>", "Records per page", "25")
10
+ .option("--time-filter <filter>", "Filter by time: upcoming or past")
11
+ .option("--status <status>", "Filter by status: draft, needs_approval, published")
12
+ .option("--feed-id <id>", "Filter by feed ID")
13
+ .option("--tag-ids <ids>", "Comma-separated tag UUIDs")
14
+ .option("--json", "Output as JSON")
15
+ .action(async (opts) => {
16
+ try {
17
+ const result = await listEvents({
18
+ page: Number(opts.page),
19
+ per_page: Number(opts.perPage),
20
+ time_filter: opts.timeFilter,
21
+ status: opts.status,
22
+ feed_id: opts.feedId,
23
+ tag_ids: opts.tagIds,
24
+ });
25
+ printOutput(result.data, { json: opts.json });
26
+ } catch (error) {
27
+ handleCommandError(error, opts.json);
28
+ }
29
+ });
@@ -0,0 +1,27 @@
1
+ import { Command } from "commander";
2
+ import { updateAttendee } from "../../core/client";
3
+ import { printOutput, printSuccess } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const updateAttendeeCommand = new Command("update-attendee")
7
+ .description("Update a calendar event attendee")
8
+ .argument("<event-id>", "Calendar event ID")
9
+ .requiredOption("--email <email>", "Attendee email (user identifier)")
10
+ .option("--personal-join-link <url>", "Personal join link for the attendee")
11
+ .option("--json", "Output as JSON")
12
+ .action(async (eventId: string, opts) => {
13
+ try {
14
+ const result = await updateAttendee(eventId, {
15
+ user: { email: opts.email },
16
+ personal_join_link: opts.personalJoinLink,
17
+ });
18
+ if (opts.json) {
19
+ printOutput(result, { json: true });
20
+ } else {
21
+ printSuccess("Attendee updated successfully");
22
+ printOutput(result, {});
23
+ }
24
+ } catch (error) {
25
+ handleCommandError(error, opts.json);
26
+ }
27
+ });
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ import { getFeed } from "../../core/client";
3
+ import { printOutput } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const getFeedCommand = new Command("get")
7
+ .description("Retrieve a feed by ID")
8
+ .argument("<feed-id>", "Feed ID")
9
+ .option("--json", "Output as JSON")
10
+ .action(async (feedId: string, opts) => {
11
+ try {
12
+ const result = await getFeed(feedId);
13
+ printOutput(result, { json: opts.json });
14
+ } catch (error) {
15
+ handleCommandError(error, opts.json);
16
+ }
17
+ });
@@ -0,0 +1,25 @@
1
+ import { Command } from "commander";
2
+ import { listFeeds } from "../../core/client";
3
+ import { printOutput } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const listFeedsCommand = new Command("list")
7
+ .description("List feeds")
8
+ .option("--page <number>", "Page number (0-indexed)", "0")
9
+ .option("--per-page <number>", "Records per page", "25")
10
+ .option("--is-unlisted", "Filter to unlisted feeds only")
11
+ .option("--is-archived", "Filter to archived feeds only")
12
+ .option("--json", "Output as JSON")
13
+ .action(async (opts) => {
14
+ try {
15
+ const result = await listFeeds({
16
+ page: Number(opts.page),
17
+ per_page: Number(opts.perPage),
18
+ is_unlisted: opts.isUnlisted,
19
+ is_archived: opts.isArchived,
20
+ });
21
+ printOutput(result.data, { json: opts.json });
22
+ } catch (error) {
23
+ handleCommandError(error, opts.json);
24
+ }
25
+ });
@@ -0,0 +1,24 @@
1
+ import { Command } from "commander";
2
+ import { listPostsInFeed } from "../../core/client";
3
+ import { printOutput } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const listPostsInFeedCommand = new Command("posts")
7
+ .description("List posts in a feed")
8
+ .argument("<feed-id>", 'Feed ID or "home" for the Home feed')
9
+ .option("--page <number>", "Page number (0-indexed)", "0")
10
+ .option("--per-page <number>", "Records per page", "25")
11
+ .option("--sort <method>", "Sort: oldest, newest, most-recent-activity")
12
+ .option("--json", "Output as JSON")
13
+ .action(async (feedId: string, opts) => {
14
+ try {
15
+ const result = await listPostsInFeed(feedId, {
16
+ page: Number(opts.page),
17
+ per_page: Number(opts.perPage),
18
+ sort: opts.sort,
19
+ });
20
+ printOutput(result.data, { json: opts.json });
21
+ } catch (error) {
22
+ handleCommandError(error, opts.json);
23
+ }
24
+ });
@@ -0,0 +1,24 @@
1
+ import { Command } from "commander";
2
+ import { addUserToGroup } from "../../core/client";
3
+ import { printOutput, printSuccess } from "../../core/output";
4
+ import { handleCommandError } from "../error-handler";
5
+
6
+ export const addGroupMemberCommand = new Command("add-member")
7
+ .description("Add a user to a group")
8
+ .argument("<group-id>", "Group ID")
9
+ .requiredOption("--email <email>", "User email address")
10
+ .option("--json", "Output as JSON")
11
+ .action(async (groupId: string, opts) => {
12
+ try {
13
+ const result = await addUserToGroup(groupId, {
14
+ user: { email: opts.email },
15
+ });
16
+ if (opts.json) {
17
+ printOutput(result, { json: true });
18
+ } else {
19
+ printSuccess("User added to group");
20
+ }
21
+ } catch (error) {
22
+ handleCommandError(error, opts.json);
23
+ }
24
+ });