@tightknitai/tightknit 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 +167 -0
- package/bin/tightknit-mcp.js +14 -0
- package/bin/tightknit.js +14 -0
- package/bin/tightknit.ts +12 -0
- package/package.json +47 -0
- package/src/cli/awards/assign.ts +28 -0
- package/src/cli/config/get.ts +27 -0
- package/src/cli/config/set.ts +23 -0
- package/src/cli/error-handler.ts +26 -0
- package/src/cli/events/create.ts +45 -0
- package/src/cli/events/delete.ts +21 -0
- package/src/cli/events/get.ts +17 -0
- package/src/cli/events/list.ts +29 -0
- package/src/cli/events/update.ts +27 -0
- package/src/cli/feeds/get.ts +17 -0
- package/src/cli/feeds/list.ts +25 -0
- package/src/cli/feeds/posts.ts +24 -0
- package/src/cli/groups/add-member.ts +24 -0
- package/src/cli/index.ts +99 -0
- package/src/cli/members/add.ts +28 -0
- package/src/cli/members/check.ts +17 -0
- package/src/cli/messages/send.ts +27 -0
- package/src/cli/posts/get.ts +17 -0
- package/src/cli/search/query.ts +25 -0
- package/src/core/client.ts +297 -0
- package/src/core/config.ts +85 -0
- package/src/core/output.ts +79 -0
- package/src/core/types.ts +79 -0
- package/src/mcp/server.ts +14 -0
- package/src/mcp/tools.ts +275 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
// Events
|
|
4
|
+
import { listEventsCommand } from "./events/list";
|
|
5
|
+
import { getEventCommand } from "./events/get";
|
|
6
|
+
import { createEventCommand } from "./events/create";
|
|
7
|
+
import { updateAttendeeCommand } from "./events/update";
|
|
8
|
+
import { deleteEventCommand } from "./events/delete";
|
|
9
|
+
|
|
10
|
+
// Awards
|
|
11
|
+
import { assignAwardCommand } from "./awards/assign";
|
|
12
|
+
|
|
13
|
+
// Feeds
|
|
14
|
+
import { listFeedsCommand } from "./feeds/list";
|
|
15
|
+
import { getFeedCommand } from "./feeds/get";
|
|
16
|
+
import { listPostsInFeedCommand } from "./feeds/posts";
|
|
17
|
+
|
|
18
|
+
// Posts
|
|
19
|
+
import { getPostCommand } from "./posts/get";
|
|
20
|
+
|
|
21
|
+
// Members
|
|
22
|
+
import { addMemberCommand } from "./members/add";
|
|
23
|
+
import { checkMemberCommand } from "./members/check";
|
|
24
|
+
|
|
25
|
+
// Messages
|
|
26
|
+
import { sendMessageCommand } from "./messages/send";
|
|
27
|
+
|
|
28
|
+
// Groups
|
|
29
|
+
import { addGroupMemberCommand } from "./groups/add-member";
|
|
30
|
+
|
|
31
|
+
// Search
|
|
32
|
+
import { searchCommand } from "./search/query";
|
|
33
|
+
|
|
34
|
+
// Config
|
|
35
|
+
import { setConfigCommand } from "./config/set";
|
|
36
|
+
import { getConfigCommand } from "./config/get";
|
|
37
|
+
|
|
38
|
+
/** Create and configure the root CLI program */
|
|
39
|
+
export function createProgram(): Command {
|
|
40
|
+
const program = new Command("tightknit")
|
|
41
|
+
.description("Tightknit CLI — manage your community from the command line")
|
|
42
|
+
.option("--no-color", "Disable colored output")
|
|
43
|
+
.option("--verbose", "Enable verbose output");
|
|
44
|
+
|
|
45
|
+
// Events command group
|
|
46
|
+
const events = new Command("events").description("Manage calendar events");
|
|
47
|
+
events.addCommand(listEventsCommand);
|
|
48
|
+
events.addCommand(getEventCommand);
|
|
49
|
+
events.addCommand(createEventCommand);
|
|
50
|
+
events.addCommand(updateAttendeeCommand);
|
|
51
|
+
events.addCommand(deleteEventCommand);
|
|
52
|
+
program.addCommand(events);
|
|
53
|
+
|
|
54
|
+
// Awards command group
|
|
55
|
+
const awards = new Command("awards").description("Manage awards");
|
|
56
|
+
awards.addCommand(assignAwardCommand);
|
|
57
|
+
program.addCommand(awards);
|
|
58
|
+
|
|
59
|
+
// Feeds command group
|
|
60
|
+
const feeds = new Command("feeds").description("Manage feeds");
|
|
61
|
+
feeds.addCommand(listFeedsCommand);
|
|
62
|
+
feeds.addCommand(getFeedCommand);
|
|
63
|
+
feeds.addCommand(listPostsInFeedCommand);
|
|
64
|
+
program.addCommand(feeds);
|
|
65
|
+
|
|
66
|
+
// Posts command group
|
|
67
|
+
const posts = new Command("posts").description("Manage posts");
|
|
68
|
+
posts.addCommand(getPostCommand);
|
|
69
|
+
program.addCommand(posts);
|
|
70
|
+
|
|
71
|
+
// Members command group
|
|
72
|
+
const members = new Command("members").description("Manage community members");
|
|
73
|
+
members.addCommand(addMemberCommand);
|
|
74
|
+
members.addCommand(checkMemberCommand);
|
|
75
|
+
program.addCommand(members);
|
|
76
|
+
|
|
77
|
+
// Messages command group
|
|
78
|
+
const messages = new Command("messages").description("Send Slack messages");
|
|
79
|
+
messages.addCommand(sendMessageCommand);
|
|
80
|
+
program.addCommand(messages);
|
|
81
|
+
|
|
82
|
+
// Groups command group
|
|
83
|
+
const groups = new Command("groups").description("Manage groups");
|
|
84
|
+
groups.addCommand(addGroupMemberCommand);
|
|
85
|
+
program.addCommand(groups);
|
|
86
|
+
|
|
87
|
+
// Search command group
|
|
88
|
+
const searchGroup = new Command("search").description("[Beta] Search documents");
|
|
89
|
+
searchGroup.addCommand(searchCommand);
|
|
90
|
+
program.addCommand(searchGroup);
|
|
91
|
+
|
|
92
|
+
// Config command group
|
|
93
|
+
const config = new Command("config").description("Manage CLI configuration");
|
|
94
|
+
config.addCommand(setConfigCommand);
|
|
95
|
+
config.addCommand(getConfigCommand);
|
|
96
|
+
program.addCommand(config);
|
|
97
|
+
|
|
98
|
+
return program;
|
|
99
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { addMember } from "../../core/client";
|
|
3
|
+
import { printOutput, printSuccess } from "../../core/output";
|
|
4
|
+
import { handleCommandError } from "../error-handler";
|
|
5
|
+
|
|
6
|
+
export const addMemberCommand = new Command("add")
|
|
7
|
+
.description("Add a member to the community (Enterprise)")
|
|
8
|
+
.requiredOption("--email <email>", "Member email address")
|
|
9
|
+
.requiredOption("--full-name <name>", "Member full name (first and last)")
|
|
10
|
+
.option("--avatar-url <url>", "Avatar image URL")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await addMember({
|
|
15
|
+
email: opts.email,
|
|
16
|
+
full_name: opts.fullName,
|
|
17
|
+
avatar_url_original: opts.avatarUrl,
|
|
18
|
+
});
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
printOutput(result, { json: true });
|
|
21
|
+
} else {
|
|
22
|
+
printSuccess("Member added successfully");
|
|
23
|
+
printOutput(result, {});
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
handleCommandError(error, opts.json);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { checkMembership } from "../../core/client";
|
|
3
|
+
import { printOutput } from "../../core/output";
|
|
4
|
+
import { handleCommandError } from "../error-handler";
|
|
5
|
+
|
|
6
|
+
export const checkMemberCommand = new Command("check")
|
|
7
|
+
.description("Check if an email is a community member")
|
|
8
|
+
.argument("<email>", "Email address to check")
|
|
9
|
+
.option("--json", "Output as JSON")
|
|
10
|
+
.action(async (email: string, opts) => {
|
|
11
|
+
try {
|
|
12
|
+
const result = await checkMembership(email);
|
|
13
|
+
printOutput(result, { json: opts.json });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
handleCommandError(error, opts.json);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { sendMessage } from "../../core/client";
|
|
3
|
+
import { printOutput, printSuccess } from "../../core/output";
|
|
4
|
+
import { handleCommandError } from "../error-handler";
|
|
5
|
+
|
|
6
|
+
export const sendMessageCommand = new Command("send")
|
|
7
|
+
.description("Send a Slack message to a channel or user")
|
|
8
|
+
.requiredOption("--channel <id>", "Slack channel ID (e.g. C0123456) or user ID")
|
|
9
|
+
.requiredOption("--text <text>", "Message text (plain text or Slack mrkdwn)")
|
|
10
|
+
.option("--thread-ts <ts>", "Reply to a thread (message timestamp)")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await sendMessage({
|
|
15
|
+
channel: opts.channel,
|
|
16
|
+
text: opts.text,
|
|
17
|
+
thread_ts: opts.threadTs,
|
|
18
|
+
});
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
printOutput(result, { json: true });
|
|
21
|
+
} else {
|
|
22
|
+
printSuccess("Message sent");
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
handleCommandError(error, opts.json);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getPost } from "../../core/client";
|
|
3
|
+
import { printOutput } from "../../core/output";
|
|
4
|
+
import { handleCommandError } from "../error-handler";
|
|
5
|
+
|
|
6
|
+
export const getPostCommand = new Command("get")
|
|
7
|
+
.description("Retrieve a post by ID")
|
|
8
|
+
.argument("<post-id>", "Post ID")
|
|
9
|
+
.option("--json", "Output as JSON")
|
|
10
|
+
.action(async (postId: string, opts) => {
|
|
11
|
+
try {
|
|
12
|
+
const result = await getPost(postId);
|
|
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 { search } from "../../core/client";
|
|
3
|
+
import { printOutput } from "../../core/output";
|
|
4
|
+
import { handleCommandError } from "../error-handler";
|
|
5
|
+
|
|
6
|
+
export const searchCommand = new Command("query")
|
|
7
|
+
.description("[Beta] Search documents")
|
|
8
|
+
.argument("<query>", "Search query string")
|
|
9
|
+
.requiredOption("--type <type>", "Entity type: post, comment, content_resource")
|
|
10
|
+
.option("--page <number>", "Page number (0-indexed)", "0")
|
|
11
|
+
.option("--per-page <number>", "Records per page", "25")
|
|
12
|
+
.option("--json", "Output as JSON")
|
|
13
|
+
.action(async (query: string, opts) => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await search({
|
|
16
|
+
q: query,
|
|
17
|
+
type: opts.type,
|
|
18
|
+
page: Number(opts.page),
|
|
19
|
+
per_page: Number(opts.perPage),
|
|
20
|
+
});
|
|
21
|
+
printOutput(result.data, { json: opts.json });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
handleCommandError(error, opts.json);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { getApiKey } from "./config";
|
|
2
|
+
import type { ApiError } from "./types";
|
|
3
|
+
|
|
4
|
+
const BASE_URL = "https://api.tightknit.ai";
|
|
5
|
+
const API_PREFIX = "/admin/v0";
|
|
6
|
+
|
|
7
|
+
interface RequestOptions {
|
|
8
|
+
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
|
9
|
+
body?: Record<string, unknown>;
|
|
10
|
+
params?: Record<string, string | number | boolean | undefined>;
|
|
11
|
+
idempotencyKey?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Build a URL with query parameters */
|
|
15
|
+
function buildUrl(path: string, params?: Record<string, string | number | boolean | undefined>): string {
|
|
16
|
+
const url = new URL(`${API_PREFIX}${path}`, BASE_URL);
|
|
17
|
+
if (params) {
|
|
18
|
+
for (const [key, value] of Object.entries(params)) {
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
url.searchParams.set(key, String(value));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return url.toString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parse an API error response into a structured error */
|
|
28
|
+
async function parseErrorResponse(response: Response): Promise<ApiError> {
|
|
29
|
+
let message: string;
|
|
30
|
+
try {
|
|
31
|
+
const body = await response.json() as Record<string, unknown>;
|
|
32
|
+
message = (body.message as string) || (body.error as string) || response.statusText;
|
|
33
|
+
} catch {
|
|
34
|
+
message = response.statusText;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
error: true,
|
|
39
|
+
code: response.status,
|
|
40
|
+
message: `${message} (${response.status})`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Make an authenticated request to the Tightknit API */
|
|
45
|
+
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
46
|
+
const { method = "GET", body, params, idempotencyKey } = options;
|
|
47
|
+
|
|
48
|
+
const apiKey = getApiKey();
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
throw new TightknitApiError(
|
|
51
|
+
'No API key configured. Run: tightknit config set api-key <YOUR_KEY>',
|
|
52
|
+
401
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const headers: Record<string, string> = {
|
|
57
|
+
Authorization: `Bearer ${apiKey}`,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (idempotencyKey && (method === "POST" || method === "PATCH")) {
|
|
62
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const url = buildUrl(path, params);
|
|
66
|
+
|
|
67
|
+
const response = await fetch(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers,
|
|
70
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const apiError = await parseErrorResponse(response);
|
|
75
|
+
throw new TightknitApiError(apiError.message, apiError.code);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (response.status === 204) {
|
|
79
|
+
return undefined as T;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (await response.json()) as T;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Custom error class for Tightknit API errors */
|
|
86
|
+
export class TightknitApiError extends Error {
|
|
87
|
+
code: number;
|
|
88
|
+
|
|
89
|
+
constructor(message: string, code: number) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "TightknitApiError";
|
|
92
|
+
this.code = code;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
toJSON(): ApiError {
|
|
96
|
+
return {
|
|
97
|
+
error: true,
|
|
98
|
+
code: this.code,
|
|
99
|
+
message: this.message,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Calendar Events ---
|
|
105
|
+
// Paths use underscores: /calendar_events
|
|
106
|
+
|
|
107
|
+
export interface ListEventsParams {
|
|
108
|
+
page?: number;
|
|
109
|
+
per_page?: number;
|
|
110
|
+
time_filter?: "upcoming" | "past";
|
|
111
|
+
date_filter?: string;
|
|
112
|
+
status?: "draft" | "needs_approval" | "published";
|
|
113
|
+
published_to_site?: boolean;
|
|
114
|
+
is_unlisted?: boolean;
|
|
115
|
+
feed_id?: string;
|
|
116
|
+
tag_ids?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function listEvents(params?: ListEventsParams) {
|
|
120
|
+
return apiRequest<{ data: unknown[]; page: number; per_page: number }>(
|
|
121
|
+
"/calendar_events",
|
|
122
|
+
{ params: params as Record<string, string | number | boolean | undefined> }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CreateEventInput {
|
|
127
|
+
title: string;
|
|
128
|
+
description: string;
|
|
129
|
+
start_date: string;
|
|
130
|
+
end_date: string;
|
|
131
|
+
location?: string;
|
|
132
|
+
link?: string;
|
|
133
|
+
status?: "needs_approval" | "published";
|
|
134
|
+
slug?: string;
|
|
135
|
+
publish_to_site?: boolean;
|
|
136
|
+
is_unlisted?: boolean;
|
|
137
|
+
allow_public_guest_list?: boolean;
|
|
138
|
+
enable_registration_button?: boolean;
|
|
139
|
+
triggers_webhooks?: boolean;
|
|
140
|
+
external_speakers?: string;
|
|
141
|
+
cover_image_file_id?: string;
|
|
142
|
+
reminders_config?: number[];
|
|
143
|
+
publish_to_slack_channels?: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function createEvent(input: CreateEventInput) {
|
|
147
|
+
return apiRequest<{ success: boolean; data: { calendar_event_id: string } }>("/calendar_events", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
body: input as unknown as Record<string, unknown>,
|
|
150
|
+
idempotencyKey: crypto.randomUUID(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function getEvent(id: string) {
|
|
155
|
+
return apiRequest<unknown>(`/calendar_events/${id}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function deleteEvent(id: string) {
|
|
159
|
+
return apiRequest<void>(`/calendar_events/${id}`, { method: "DELETE" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface UpdateAttendeeInput {
|
|
163
|
+
user: { slack_user_id: string } | { email: string } | { profile_id: string };
|
|
164
|
+
personal_join_link?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function updateAttendee(eventId: string, input: UpdateAttendeeInput) {
|
|
168
|
+
return apiRequest<unknown>(`/calendar_events/${eventId}/attendees`, {
|
|
169
|
+
method: "PATCH",
|
|
170
|
+
body: input as unknown as Record<string, unknown>,
|
|
171
|
+
idempotencyKey: crypto.randomUUID(),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Awards ---
|
|
176
|
+
|
|
177
|
+
export interface AssignAwardInput {
|
|
178
|
+
recipient: { slack_user_id: string } | { email: string } | { profile_id: string };
|
|
179
|
+
sender?: { slack_user_id: string } | { email: string } | { profile_id: string } | null;
|
|
180
|
+
send_anonymously?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function assignAward(awardId: string, input: AssignAwardInput) {
|
|
184
|
+
return apiRequest<{ success: boolean }>(`/awards/${awardId}/assign`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
body: input as unknown as Record<string, unknown>,
|
|
187
|
+
idempotencyKey: crypto.randomUUID(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Feeds ---
|
|
192
|
+
|
|
193
|
+
export interface ListFeedsParams {
|
|
194
|
+
page?: number;
|
|
195
|
+
per_page?: number;
|
|
196
|
+
is_unlisted?: boolean;
|
|
197
|
+
is_archived?: boolean;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function listFeeds(params?: ListFeedsParams) {
|
|
201
|
+
return apiRequest<{ data: unknown[]; page: number; per_page: number }>(
|
|
202
|
+
"/feeds",
|
|
203
|
+
{ params: params as Record<string, string | number | boolean | undefined> }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function getFeed(feedId: string) {
|
|
208
|
+
return apiRequest<unknown>(`/feeds/${feedId}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface ListPostsInFeedParams {
|
|
212
|
+
page?: number;
|
|
213
|
+
per_page?: number;
|
|
214
|
+
sort?: "oldest" | "newest" | "most-recent-activity";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function listPostsInFeed(feedId: string, params?: ListPostsInFeedParams) {
|
|
218
|
+
return apiRequest<{ data: unknown[]; page: number; per_page: number }>(
|
|
219
|
+
`/feeds/${feedId}/posts`,
|
|
220
|
+
{ params: params as Record<string, string | number | boolean | undefined> }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- Posts ---
|
|
225
|
+
|
|
226
|
+
export async function getPost(postId: string) {
|
|
227
|
+
return apiRequest<unknown>(`/posts/${postId}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Members ---
|
|
231
|
+
|
|
232
|
+
export interface AddMemberInput {
|
|
233
|
+
email: string;
|
|
234
|
+
full_name: string;
|
|
235
|
+
avatar_url_original?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function addMember(input: AddMemberInput) {
|
|
239
|
+
return apiRequest<unknown>("/members", {
|
|
240
|
+
method: "POST",
|
|
241
|
+
body: input as unknown as Record<string, unknown>,
|
|
242
|
+
idempotencyKey: crypto.randomUUID(),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function checkMembership(email: string) {
|
|
247
|
+
return apiRequest<unknown>("/members/check", {
|
|
248
|
+
method: "POST",
|
|
249
|
+
body: { email },
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Groups ---
|
|
254
|
+
|
|
255
|
+
export interface AddUserToGroupInput {
|
|
256
|
+
user: { slack_user_id: string } | { email: string } | { profile_id: string };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function addUserToGroup(groupId: string, input: AddUserToGroupInput) {
|
|
260
|
+
return apiRequest<unknown>(`/groups/${groupId}/members`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
body: input as unknown as Record<string, unknown>,
|
|
263
|
+
idempotencyKey: crypto.randomUUID(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- Messages ---
|
|
268
|
+
|
|
269
|
+
export interface SendMessageInput {
|
|
270
|
+
channel: string;
|
|
271
|
+
text: string;
|
|
272
|
+
thread_ts?: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function sendMessage(input: SendMessageInput) {
|
|
276
|
+
return apiRequest<unknown>("/messages", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
body: input as unknown as Record<string, unknown>,
|
|
279
|
+
idempotencyKey: crypto.randomUUID(),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- Search ---
|
|
284
|
+
|
|
285
|
+
export interface SearchParams {
|
|
286
|
+
q: string;
|
|
287
|
+
type: "post" | "comment" | "content_resource";
|
|
288
|
+
page?: number;
|
|
289
|
+
per_page?: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function search(params: SearchParams) {
|
|
293
|
+
return apiRequest<{ data: unknown[]; page: number; per_page: number }>(
|
|
294
|
+
"/search",
|
|
295
|
+
{ params: params as unknown as Record<string, string | number | boolean | undefined> }
|
|
296
|
+
);
|
|
297
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
|
|
3
|
+
interface TightknitConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
communityId: string;
|
|
6
|
+
defaultOutput: "json" | "table";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const config = new Conf<TightknitConfig>({
|
|
10
|
+
projectName: "tightknit",
|
|
11
|
+
defaults: {
|
|
12
|
+
apiKey: "",
|
|
13
|
+
communityId: "",
|
|
14
|
+
defaultOutput: "table",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Get the API key — env var takes precedence over stored config */
|
|
19
|
+
export function getApiKey(): string {
|
|
20
|
+
return process.env.TIGHTKNIT_API_KEY || config.get("apiKey");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Set the API key */
|
|
24
|
+
export function setApiKey(key: string): void {
|
|
25
|
+
config.set("apiKey", key);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Get the stored community ID */
|
|
29
|
+
export function getCommunityId(): string {
|
|
30
|
+
return config.get("communityId");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Set the community ID */
|
|
34
|
+
export function setCommunityId(id: string): void {
|
|
35
|
+
config.set("communityId", id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Get the default output format */
|
|
39
|
+
export function getDefaultOutput(): "json" | "table" {
|
|
40
|
+
return config.get("defaultOutput");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Set the default output format */
|
|
44
|
+
export function setDefaultOutput(format: "json" | "table"): void {
|
|
45
|
+
config.set("defaultOutput", format);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Get a config value by key name */
|
|
49
|
+
export function getConfigValue(key: string): string {
|
|
50
|
+
switch (key) {
|
|
51
|
+
case "api-key":
|
|
52
|
+
return getApiKey();
|
|
53
|
+
case "community-id":
|
|
54
|
+
return getCommunityId();
|
|
55
|
+
case "default-output":
|
|
56
|
+
return getDefaultOutput();
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unknown config key: ${key}. Valid keys: api-key, community-id, default-output`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Set a config value by key name */
|
|
63
|
+
export function setConfigValue(key: string, value: string): void {
|
|
64
|
+
switch (key) {
|
|
65
|
+
case "api-key":
|
|
66
|
+
setApiKey(value);
|
|
67
|
+
break;
|
|
68
|
+
case "community-id":
|
|
69
|
+
setCommunityId(value);
|
|
70
|
+
break;
|
|
71
|
+
case "default-output":
|
|
72
|
+
if (value !== "json" && value !== "table") {
|
|
73
|
+
throw new Error(`Invalid output format: ${value}. Must be "json" or "table"`);
|
|
74
|
+
}
|
|
75
|
+
setDefaultOutput(value);
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unknown config key: ${key}. Valid keys: api-key, community-id, default-output`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Get the config file path (useful for debugging) */
|
|
83
|
+
export function getConfigPath(): string {
|
|
84
|
+
return config.path;
|
|
85
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
|
|
4
|
+
interface OutputOptions {
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Format and print data in JSON or human-readable table format */
|
|
9
|
+
export function formatOutput(data: unknown, options: OutputOptions = {}): string {
|
|
10
|
+
if (options.json) {
|
|
11
|
+
return JSON.stringify(data, null, 2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(data)) {
|
|
15
|
+
return formatTable(data);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof data === "object" && data !== null) {
|
|
19
|
+
return formatKeyValue(data as Record<string, unknown>);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return String(data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Print formatted output to stdout */
|
|
26
|
+
export function printOutput(data: unknown, options: OutputOptions = {}): void {
|
|
27
|
+
console.log(formatOutput(data, options));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Print a success message */
|
|
31
|
+
export function printSuccess(message: string): void {
|
|
32
|
+
console.log(chalk.green("✓"), message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Print an error message */
|
|
36
|
+
export function printError(message: string): void {
|
|
37
|
+
console.error(chalk.red("✗"), message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Format an array of objects as a table */
|
|
41
|
+
function formatTable(rows: unknown[]): string {
|
|
42
|
+
if (rows.length === 0) {
|
|
43
|
+
return chalk.dim("No results found.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const firstRow = rows[0] as Record<string, unknown>;
|
|
47
|
+
const keys = Object.keys(firstRow);
|
|
48
|
+
|
|
49
|
+
const table = new Table({
|
|
50
|
+
head: keys.map((k) => chalk.bold(k)),
|
|
51
|
+
style: { head: [], border: [] },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
const record = row as Record<string, unknown>;
|
|
56
|
+
table.push(keys.map((k) => truncate(String(record[k] ?? ""), 50)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return table.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Format a single object as key-value pairs */
|
|
63
|
+
function formatKeyValue(obj: Record<string, unknown>): string {
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
66
|
+
const displayValue =
|
|
67
|
+
typeof value === "object" && value !== null
|
|
68
|
+
? JSON.stringify(value)
|
|
69
|
+
: String(value ?? "");
|
|
70
|
+
lines.push(`${chalk.bold(key)}: ${displayValue}`);
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Truncate a string to a max length */
|
|
76
|
+
function truncate(str: string, maxLength: number): string {
|
|
77
|
+
if (str.length <= maxLength) return str;
|
|
78
|
+
return `${str.slice(0, maxLength - 1)}…`;
|
|
79
|
+
}
|