@bragduck/mcp 1.7.0 → 1.8.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 +29 -0
- package/dist/index.js +2006 -122
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- package/dist/api-client.d.ts +0 -29
- package/dist/api-client.js +0 -146
- package/dist/api-client.js.map +0 -1
- package/dist/auth.d.ts +0 -8
- package/dist/auth.js +0 -202
- package/dist/auth.js.map +0 -1
- package/dist/credentials.d.ts +0 -31
- package/dist/credentials.js +0 -150
- package/dist/credentials.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/org-preference.d.ts +0 -7
- package/dist/org-preference.js +0 -22
- package/dist/org-preference.js.map +0 -1
- package/dist/tools/link-brag-to-key-result.d.ts +0 -30
- package/dist/tools/link-brag-to-key-result.js +0 -81
- package/dist/tools/link-brag-to-key-result.js.map +0 -1
- package/dist/tools/list-brags.d.ts +0 -53
- package/dist/tools/list-brags.js +0 -110
- package/dist/tools/list-brags.js.map +0 -1
- package/dist/tools/list-objectives.d.ts +0 -15
- package/dist/tools/list-objectives.js +0 -66
- package/dist/tools/list-objectives.js.map +0 -1
- package/dist/tools/list-organisations.d.ts +0 -15
- package/dist/tools/list-organisations.js +0 -73
- package/dist/tools/list-organisations.js.map +0 -1
- package/dist/tools/list-stories.d.ts +0 -41
- package/dist/tools/list-stories.js +0 -103
- package/dist/tools/list-stories.js.map +0 -1
- package/dist/tools/log-brag.d.ts +0 -88
- package/dist/tools/log-brag.js +0 -175
- package/dist/tools/log-brag.js.map +0 -1
- package/dist/tools/log-story.d.ts +0 -81
- package/dist/tools/log-story.js +0 -122
- package/dist/tools/log-story.js.map +0 -1
- package/dist/tools/login.d.ts +0 -24
- package/dist/tools/login.js +0 -46
- package/dist/tools/login.js.map +0 -1
- package/dist/tools/preview-brag.d.ts +0 -71
- package/dist/tools/preview-brag.js +0 -103
- package/dist/tools/preview-brag.js.map +0 -1
- package/dist/tools/quick-brag.d.ts +0 -38
- package/dist/tools/quick-brag.js +0 -143
- package/dist/tools/quick-brag.js.map +0 -1
- package/dist/tools/save-org-preference.d.ts +0 -30
- package/dist/tools/save-org-preference.js +0 -47
- package/dist/tools/save-org-preference.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,131 +1,2015 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
McpError
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
|
|
13
|
+
// src/auth.ts
|
|
14
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
15
|
+
import { createServer } from "http";
|
|
16
|
+
import { parse } from "url";
|
|
17
|
+
import { exec } from "child_process";
|
|
18
|
+
import { promisify } from "util";
|
|
19
|
+
|
|
20
|
+
// src/credentials.ts
|
|
21
|
+
import { createDecipheriv, scryptSync, randomBytes, createCipheriv } from "crypto";
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import Conf from "conf";
|
|
26
|
+
var APP_NAME = "bragduck";
|
|
27
|
+
var CREDENTIALS_DIR = ".bragduck";
|
|
28
|
+
var CREDENTIALS_FILE = "credentials.enc";
|
|
29
|
+
var ENCRYPTION = {
|
|
30
|
+
ALGORITHM: "aes-256-gcm",
|
|
31
|
+
KEY_LENGTH: 32,
|
|
32
|
+
IV_LENGTH: 16,
|
|
33
|
+
AUTH_TAG_LENGTH: 16,
|
|
34
|
+
SALT_LENGTH: 32
|
|
35
|
+
};
|
|
36
|
+
function deriveEncryptionKey(salt) {
|
|
37
|
+
const password = `${APP_NAME}-${homedir()}-${process.platform}`;
|
|
38
|
+
return scryptSync(password, salt, ENCRYPTION.KEY_LENGTH);
|
|
39
|
+
}
|
|
40
|
+
function decrypt(encryptedData, key) {
|
|
41
|
+
const decipher = createDecipheriv(
|
|
42
|
+
ENCRYPTION.ALGORITHM,
|
|
43
|
+
key,
|
|
44
|
+
Buffer.from(encryptedData.iv, "hex"),
|
|
45
|
+
{ authTagLength: ENCRYPTION.AUTH_TAG_LENGTH }
|
|
46
|
+
);
|
|
47
|
+
decipher.setAuthTag(Buffer.from(encryptedData.authTag, "hex"));
|
|
48
|
+
let decrypted = decipher.update(encryptedData.encrypted, "hex", "utf8");
|
|
49
|
+
decrypted += decipher.final("utf8");
|
|
50
|
+
return decrypted;
|
|
51
|
+
}
|
|
52
|
+
function encrypt(data, key) {
|
|
53
|
+
const iv = randomBytes(ENCRYPTION.IV_LENGTH);
|
|
54
|
+
const cipher = createCipheriv(ENCRYPTION.ALGORITHM, key, iv, {
|
|
55
|
+
authTagLength: ENCRYPTION.AUTH_TAG_LENGTH
|
|
56
|
+
});
|
|
57
|
+
let encrypted = cipher.update(data, "utf8", "hex");
|
|
58
|
+
encrypted += cipher.final("hex");
|
|
59
|
+
const authTag = cipher.getAuthTag();
|
|
60
|
+
return {
|
|
61
|
+
encrypted,
|
|
62
|
+
iv: iv.toString("hex"),
|
|
63
|
+
authTag: authTag.toString("hex"),
|
|
64
|
+
salt: ""
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function readStoredCredentials() {
|
|
68
|
+
const credentialsPath = join(homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE);
|
|
69
|
+
if (!existsSync(credentialsPath)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const encryptedData = JSON.parse(
|
|
74
|
+
readFileSync(credentialsPath, "utf8")
|
|
75
|
+
);
|
|
76
|
+
const salt = Buffer.from(encryptedData.salt, "hex");
|
|
77
|
+
const key = deriveEncryptionKey(salt);
|
|
78
|
+
const decrypted = decrypt(encryptedData, key);
|
|
79
|
+
return JSON.parse(decrypted);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function writeStoredCredentials(credentials) {
|
|
85
|
+
const credentialsDir = join(homedir(), CREDENTIALS_DIR);
|
|
86
|
+
const credentialsPath = join(credentialsDir, CREDENTIALS_FILE);
|
|
87
|
+
if (!existsSync(credentialsDir)) {
|
|
88
|
+
mkdirSync(credentialsDir, { recursive: true, mode: 448 });
|
|
89
|
+
}
|
|
90
|
+
const data = JSON.stringify(credentials);
|
|
91
|
+
const salt = randomBytes(ENCRYPTION.SALT_LENGTH);
|
|
92
|
+
const key = deriveEncryptionKey(salt);
|
|
93
|
+
const encrypted = encrypt(data, key);
|
|
94
|
+
encrypted.salt = salt.toString("hex");
|
|
95
|
+
writeFileSync(credentialsPath, JSON.stringify(encrypted), {
|
|
96
|
+
mode: 384,
|
|
97
|
+
encoding: "utf8"
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function getAccessToken() {
|
|
101
|
+
const credentials = readStoredCredentials();
|
|
102
|
+
if (!credentials) return null;
|
|
103
|
+
if (credentials.accessToken && !credentials.services) {
|
|
104
|
+
return credentials.accessToken;
|
|
105
|
+
}
|
|
106
|
+
return credentials.services?.bragduck?.accessToken || null;
|
|
107
|
+
}
|
|
108
|
+
function getRefreshToken() {
|
|
109
|
+
const credentials = readStoredCredentials();
|
|
110
|
+
if (!credentials) return null;
|
|
111
|
+
if (credentials.accessToken && !credentials.services) {
|
|
112
|
+
return credentials.refreshToken || null;
|
|
113
|
+
}
|
|
114
|
+
return credentials.services?.bragduck?.refreshToken || null;
|
|
115
|
+
}
|
|
116
|
+
function storeCredentials(creds) {
|
|
117
|
+
let existing = readStoredCredentials();
|
|
118
|
+
if (existing && existing.accessToken && !existing.services) {
|
|
119
|
+
existing = {
|
|
120
|
+
services: {
|
|
121
|
+
bragduck: {
|
|
122
|
+
accessToken: existing.accessToken,
|
|
123
|
+
refreshToken: existing.refreshToken,
|
|
124
|
+
expiresAt: existing.expiresAt
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const updated = {
|
|
130
|
+
...existing,
|
|
131
|
+
services: {
|
|
132
|
+
...existing?.services,
|
|
133
|
+
bragduck: creds
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
writeStoredCredentials(updated);
|
|
137
|
+
}
|
|
138
|
+
function getUserInfo() {
|
|
139
|
+
try {
|
|
140
|
+
const config = new Conf({ projectName: APP_NAME });
|
|
141
|
+
return config.get("userInfo") || null;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function storeUserInfo(userInfo) {
|
|
147
|
+
const config = new Conf({ projectName: APP_NAME });
|
|
148
|
+
config.set("userInfo", userInfo);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/auth.ts
|
|
152
|
+
var execAsync = promisify(exec);
|
|
153
|
+
var API_BASE_URL = process.env.BRAGDUCK_API_URL || "https://api.bragduck.com";
|
|
154
|
+
var OAUTH_CLIENT_ID = "bragduck-cli";
|
|
155
|
+
var CALLBACK_PATH = "/callback";
|
|
156
|
+
var TIMEOUT_MS = 12e4;
|
|
157
|
+
var MIN_PORT = 8010;
|
|
158
|
+
var MAX_PORT = 8014;
|
|
159
|
+
async function findAvailablePort() {
|
|
160
|
+
for (let port = MIN_PORT; port <= MAX_PORT; port++) {
|
|
161
|
+
try {
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
const testServer = createServer();
|
|
164
|
+
testServer.once("error", reject);
|
|
165
|
+
testServer.once("listening", () => {
|
|
166
|
+
testServer.close(() => resolve());
|
|
167
|
+
});
|
|
168
|
+
testServer.listen(port, "127.0.0.1");
|
|
169
|
+
});
|
|
170
|
+
return port;
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`No available ports found in range ${MIN_PORT}-${MAX_PORT}`);
|
|
176
|
+
}
|
|
177
|
+
async function openBrowser(url) {
|
|
178
|
+
const platform = process.platform;
|
|
179
|
+
let command;
|
|
180
|
+
switch (platform) {
|
|
181
|
+
case "darwin":
|
|
182
|
+
command = `open "${url}"`;
|
|
183
|
+
break;
|
|
184
|
+
case "win32":
|
|
185
|
+
command = `start "" "${url}"`;
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
command = `xdg-open "${url}"`;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
await execAsync(command);
|
|
192
|
+
}
|
|
193
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
194
|
+
<html><head><meta charset="UTF-8"><title>BragDuck - Authenticated</title>
|
|
195
|
+
<style>
|
|
196
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
197
|
+
display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0;
|
|
198
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
|
|
199
|
+
.container { text-align: center; padding: 2rem; background: rgba(255,255,255,0.1);
|
|
200
|
+
border-radius: 1rem; backdrop-filter: blur(10px); max-width: 500px; }
|
|
201
|
+
h1 { font-size: 2rem; margin: 0 0 1rem; }
|
|
202
|
+
.check { font-size: 4rem; animation: pop 0.3s ease-out; }
|
|
203
|
+
p { font-size: 1.1rem; opacity: 0.9; }
|
|
204
|
+
@keyframes pop { from { transform: scale(0); } to { transform: scale(1); } }
|
|
205
|
+
</style></head><body>
|
|
206
|
+
<div class="container">
|
|
207
|
+
<div class="check">✓</div>
|
|
208
|
+
<h1>Authentication Successful</h1>
|
|
209
|
+
<p>You can close this window and return to your editor.</p>
|
|
210
|
+
</div></body></html>`;
|
|
211
|
+
var ERROR_HTML = (error) => `<!DOCTYPE html>
|
|
212
|
+
<html><head><meta charset="UTF-8"><title>BragDuck - Auth Failed</title>
|
|
213
|
+
<style>
|
|
214
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
215
|
+
display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0;
|
|
216
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; }
|
|
217
|
+
.container { text-align: center; padding: 2rem; background: rgba(255,255,255,0.1);
|
|
218
|
+
border-radius: 1rem; backdrop-filter: blur(10px); max-width: 500px; }
|
|
219
|
+
h1 { font-size: 2rem; margin: 0 0 1rem; }
|
|
220
|
+
.err { font-size: 4rem; }
|
|
221
|
+
p { font-size: 1.1rem; opacity: 0.9; }
|
|
222
|
+
.details { background: rgba(0,0,0,0.2); padding: 1rem; border-radius: 0.5rem;
|
|
223
|
+
font-family: monospace; font-size: 0.9rem; margin-top: 1rem; }
|
|
224
|
+
</style></head><body>
|
|
225
|
+
<div class="container">
|
|
226
|
+
<div class="err">✗</div>
|
|
227
|
+
<h1>Authentication Failed</h1>
|
|
228
|
+
<p>Please try again.</p>
|
|
229
|
+
<div class="details">${error}</div>
|
|
230
|
+
</div></body></html>`;
|
|
231
|
+
async function exchangeCodeForToken(code, callbackUrl) {
|
|
232
|
+
const response = await fetch(`${API_BASE_URL}/v1/auth/cli/token`, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
code,
|
|
237
|
+
redirect_uri: callbackUrl,
|
|
238
|
+
client_id: OAUTH_CLIENT_ID,
|
|
239
|
+
grant_type: "authorization_code"
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
const errorData = await response.json().catch(() => ({}));
|
|
244
|
+
throw new Error(errorData.message || `Token exchange failed: ${response.statusText}`);
|
|
245
|
+
}
|
|
246
|
+
return response.json();
|
|
247
|
+
}
|
|
248
|
+
async function login() {
|
|
249
|
+
const state = randomBytes2(32).toString("hex");
|
|
250
|
+
const port = await findAvailablePort();
|
|
251
|
+
const callbackUrl = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
|
|
252
|
+
const authParams = new URLSearchParams({
|
|
253
|
+
client_id: OAUTH_CLIENT_ID,
|
|
254
|
+
redirect_uri: callbackUrl,
|
|
255
|
+
state
|
|
256
|
+
});
|
|
257
|
+
const authUrl = `${API_BASE_URL}/v1/auth/cli/initiate?${authParams.toString()}`;
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
let server2 = null;
|
|
260
|
+
let timeoutId = null;
|
|
261
|
+
const cleanup = () => {
|
|
262
|
+
if (timeoutId) globalThis.clearTimeout(timeoutId);
|
|
263
|
+
if (server2) {
|
|
264
|
+
if (typeof server2.closeAllConnections === "function") {
|
|
265
|
+
server2.closeAllConnections();
|
|
266
|
+
}
|
|
267
|
+
server2.close();
|
|
268
|
+
server2.unref();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const handleRequest = async (req, res) => {
|
|
272
|
+
const parsedUrl = parse(req.url || "", true);
|
|
273
|
+
if (parsedUrl.pathname !== CALLBACK_PATH) {
|
|
274
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
275
|
+
res.end("Not Found");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const { code, state: returnedState, error, error_description } = parsedUrl.query;
|
|
279
|
+
if (error) {
|
|
280
|
+
const errorMsg = String(error_description || error);
|
|
281
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
282
|
+
res.end(ERROR_HTML(errorMsg));
|
|
283
|
+
cleanup();
|
|
284
|
+
reject(new Error(`OAuth error: ${errorMsg}`));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!code || !returnedState) {
|
|
288
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
289
|
+
res.end(ERROR_HTML("Missing code or state parameter"));
|
|
290
|
+
cleanup();
|
|
291
|
+
reject(new Error("Missing code or state parameter"));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (returnedState !== state) {
|
|
295
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
296
|
+
res.end(ERROR_HTML("Invalid state parameter"));
|
|
297
|
+
cleanup();
|
|
298
|
+
reject(new Error("Invalid state parameter (possible CSRF)"));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
302
|
+
res.end(SUCCESS_HTML);
|
|
303
|
+
try {
|
|
304
|
+
const tokenResponse = await exchangeCodeForToken(String(code), callbackUrl);
|
|
305
|
+
const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
|
|
306
|
+
storeCredentials({
|
|
307
|
+
accessToken: tokenResponse.access_token,
|
|
308
|
+
refreshToken: tokenResponse.refresh_token,
|
|
309
|
+
expiresAt
|
|
310
|
+
});
|
|
311
|
+
const user = tokenResponse.user || { id: "unknown", email: "unknown", name: "Unknown" };
|
|
312
|
+
storeUserInfo(user);
|
|
313
|
+
globalThis.setTimeout(() => {
|
|
314
|
+
cleanup();
|
|
315
|
+
resolve({ email: user.email, name: user.name });
|
|
316
|
+
}, 100);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
cleanup();
|
|
319
|
+
reject(err);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
server2 = createServer(handleRequest);
|
|
323
|
+
server2.on("error", (err) => {
|
|
324
|
+
cleanup();
|
|
325
|
+
reject(new Error(`OAuth server error: ${err.message}`));
|
|
326
|
+
});
|
|
327
|
+
server2.listen(port, "127.0.0.1", () => {
|
|
328
|
+
openBrowser(authUrl).catch(() => {
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
timeoutId = globalThis.setTimeout(() => {
|
|
332
|
+
cleanup();
|
|
333
|
+
reject(new Error("Authentication timed out. No callback received within 2 minutes."));
|
|
334
|
+
}, TIMEOUT_MS);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/tools/login.ts
|
|
339
|
+
var loginTool = {
|
|
340
|
+
name: "login",
|
|
341
|
+
description: "Authenticate with BragDuck. Opens the browser for sign-in/sign-up. Call this when other tools return an authentication error.",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: {
|
|
345
|
+
force: {
|
|
346
|
+
type: "boolean",
|
|
347
|
+
description: "Force re-authentication even if already logged in"
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
async function handleLogin(input) {
|
|
353
|
+
if (!input?.force) {
|
|
354
|
+
const existingToken = getAccessToken();
|
|
355
|
+
if (existingToken) {
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: "Already authenticated with BragDuck." }]
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const user = await login();
|
|
363
|
+
return {
|
|
364
|
+
content: [{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: `Authenticated as ${user.name} (${user.email}). You can now log brags and manage objectives.`
|
|
367
|
+
}]
|
|
368
|
+
};
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return {
|
|
371
|
+
content: [{
|
|
372
|
+
type: "text",
|
|
373
|
+
text: `Authentication failed: ${error instanceof Error ? error.message : String(error)}`
|
|
374
|
+
}],
|
|
375
|
+
isError: true
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/api-client.ts
|
|
381
|
+
var API_BASE_URL2 = process.env.BRAGDUCK_API_URL || "https://api.bragduck.com";
|
|
382
|
+
var DEBUG = !!process.env.BRAGDUCK_DEBUG;
|
|
383
|
+
var OAUTH_CLIENT_ID2 = "bragduck-cli";
|
|
384
|
+
var TOKEN_ENDPOINT = "/v1/auth/cli/token";
|
|
385
|
+
function debug(...args) {
|
|
386
|
+
if (DEBUG) console.error("[bragduck-mcp]", ...args);
|
|
387
|
+
}
|
|
388
|
+
async function refreshAccessToken() {
|
|
389
|
+
const refreshToken = getRefreshToken();
|
|
390
|
+
if (!refreshToken) return null;
|
|
391
|
+
try {
|
|
392
|
+
const response = await fetch(`${API_BASE_URL2}${TOKEN_ENDPOINT}`, {
|
|
393
|
+
method: "POST",
|
|
394
|
+
headers: { "Content-Type": "application/json" },
|
|
395
|
+
body: JSON.stringify({
|
|
396
|
+
refresh_token: refreshToken,
|
|
397
|
+
client_id: OAUTH_CLIENT_ID2,
|
|
398
|
+
grant_type: "refresh_token"
|
|
399
|
+
})
|
|
400
|
+
});
|
|
401
|
+
if (!response.ok) return null;
|
|
402
|
+
const data = await response.json();
|
|
403
|
+
const expiresAt = data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0;
|
|
404
|
+
storeCredentials({
|
|
405
|
+
accessToken: data.access_token,
|
|
406
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
407
|
+
expiresAt
|
|
408
|
+
});
|
|
409
|
+
return data.access_token;
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function apiRequest(endpoint, options = {}) {
|
|
415
|
+
const { method = "GET", body, params } = options;
|
|
416
|
+
let token = getAccessToken();
|
|
417
|
+
if (!token) {
|
|
418
|
+
throw new Error("NOT_AUTHENTICATED");
|
|
419
|
+
}
|
|
420
|
+
let url = `${API_BASE_URL2}${endpoint}`;
|
|
421
|
+
if (params) {
|
|
422
|
+
const searchParams = new URLSearchParams();
|
|
423
|
+
for (const [key, value] of Object.entries(params)) {
|
|
424
|
+
if (Array.isArray(value)) {
|
|
425
|
+
value.forEach((v) => searchParams.append(`${key}[]`, v));
|
|
426
|
+
} else {
|
|
427
|
+
searchParams.append(key, value);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
url += `?${searchParams.toString()}`;
|
|
431
|
+
}
|
|
432
|
+
const headers = {
|
|
433
|
+
Authorization: `Bearer ${token}`,
|
|
434
|
+
"User-Agent": "BragDuck-MCP/1.0.0"
|
|
435
|
+
};
|
|
436
|
+
if (body) {
|
|
437
|
+
headers["Content-Type"] = "application/json";
|
|
438
|
+
}
|
|
439
|
+
const bodyString = body ? JSON.stringify(body) : void 0;
|
|
440
|
+
debug(`${method} ${url}`);
|
|
441
|
+
if (bodyString) debug("Request body:", bodyString);
|
|
442
|
+
let response = await fetch(url, {
|
|
443
|
+
method,
|
|
444
|
+
headers,
|
|
445
|
+
body: bodyString
|
|
446
|
+
});
|
|
447
|
+
debug(`Response: ${response.status} ${response.statusText}`);
|
|
448
|
+
if (response.status === 401) {
|
|
449
|
+
const newToken = await refreshAccessToken();
|
|
450
|
+
if (!newToken) {
|
|
451
|
+
throw new Error("TOKEN_EXPIRED");
|
|
452
|
+
}
|
|
453
|
+
headers.Authorization = `Bearer ${newToken}`;
|
|
454
|
+
response = await fetch(url, {
|
|
455
|
+
method,
|
|
456
|
+
headers,
|
|
457
|
+
body: bodyString
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
const text = await response.text().catch(() => "");
|
|
462
|
+
debug("Error response body:", text);
|
|
463
|
+
let msg = `API error: ${response.status} ${response.statusText}`;
|
|
464
|
+
try {
|
|
465
|
+
const errorData = JSON.parse(text);
|
|
466
|
+
msg = errorData.message || errorData.error || msg;
|
|
467
|
+
} catch {
|
|
468
|
+
if (text) msg = text;
|
|
469
|
+
}
|
|
470
|
+
throw new Error(msg);
|
|
471
|
+
}
|
|
472
|
+
if (response.status === 204) return void 0;
|
|
473
|
+
return response.json();
|
|
474
|
+
}
|
|
475
|
+
async function getSubscriptionStatus() {
|
|
476
|
+
return apiRequest("/v1/subscriptions/status");
|
|
477
|
+
}
|
|
478
|
+
async function requirePaidPlan() {
|
|
479
|
+
try {
|
|
480
|
+
const { tier } = await getSubscriptionStatus();
|
|
481
|
+
if (tier === "FREE") {
|
|
482
|
+
return {
|
|
483
|
+
content: [
|
|
484
|
+
{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: "BragDuck MCP requires a PLUS or PRO subscription.\n\nThe free plan does not include CLI or MCP access.\nUpgrade at: https://bragduck.com/app/settings/plans"
|
|
487
|
+
}
|
|
488
|
+
],
|
|
489
|
+
isError: true
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/tools/log-brag.ts
|
|
499
|
+
var logBragTool = {
|
|
500
|
+
name: "log_brag",
|
|
501
|
+
description: "Persist a career achievement (brag) to BragDuck. Call this in the same response as the preview table \u2014 the tool permission prompt (Allow/Deny) that appears to the user IS the confirmation. Do not ask the user to type anything.",
|
|
502
|
+
inputSchema: {
|
|
503
|
+
type: "object",
|
|
504
|
+
properties: {
|
|
505
|
+
title: {
|
|
506
|
+
type: "string",
|
|
507
|
+
minLength: 10,
|
|
508
|
+
maxLength: 200,
|
|
509
|
+
description: "Short title of the achievement (must be 10-200 characters)"
|
|
510
|
+
},
|
|
511
|
+
description: {
|
|
512
|
+
type: "string",
|
|
513
|
+
minLength: 50,
|
|
514
|
+
maxLength: 2e3,
|
|
515
|
+
description: "Detailed description of what was accomplished and its impact (must be 50-2000 characters)"
|
|
516
|
+
},
|
|
517
|
+
tags: {
|
|
518
|
+
type: "array",
|
|
519
|
+
items: { type: "string", minLength: 2, maxLength: 30 },
|
|
520
|
+
maxItems: 10,
|
|
521
|
+
description: 'Tags to categorize the brag (e.g., "performance", "leadership", "backend"). Each tag must be 2-30 characters. Maximum 10 tags.'
|
|
522
|
+
},
|
|
523
|
+
date: {
|
|
524
|
+
type: "string",
|
|
525
|
+
description: "ISO datetime string for when this happened (defaults to now)"
|
|
526
|
+
},
|
|
527
|
+
impact_level: {
|
|
528
|
+
type: "number",
|
|
529
|
+
enum: [0, 1, 2, 3],
|
|
530
|
+
description: "Impact level of the achievement. 0 = none (personal learning, routine tasks). 1 = low (individual contributor impact, fixed small bug). 2 = medium (team-level impact, improved team process, led project). 3 = high (organization-level impact, reduced costs, improved key metrics)."
|
|
531
|
+
},
|
|
532
|
+
impact_description: {
|
|
533
|
+
type: "string",
|
|
534
|
+
description: 'Narrative description of the impact. Start with scope (Individual/Team/Organization/Company-wide), then include 1-3 measurable outcomes. Example: "Team impact: Reduced deploy time by 60%, unblocking 4 engineers"'
|
|
535
|
+
},
|
|
536
|
+
type: {
|
|
537
|
+
type: "string",
|
|
538
|
+
enum: [
|
|
539
|
+
"progress",
|
|
540
|
+
"achievement",
|
|
541
|
+
"recognition",
|
|
542
|
+
"support",
|
|
543
|
+
"code",
|
|
544
|
+
"leadership",
|
|
545
|
+
"learning",
|
|
546
|
+
"collaboration",
|
|
547
|
+
"challenge",
|
|
548
|
+
"innovation",
|
|
549
|
+
"meeting",
|
|
550
|
+
"bug",
|
|
551
|
+
"others"
|
|
552
|
+
],
|
|
553
|
+
description: "Type of achievement. Choose the most appropriate category for this brag."
|
|
554
|
+
},
|
|
555
|
+
references: {
|
|
556
|
+
type: "array",
|
|
557
|
+
items: { type: "string", format: "uri" },
|
|
558
|
+
maxItems: 10,
|
|
559
|
+
description: 'URLs to reference materials for this achievement. Include any relevant links that provide context or proof: pull request URLs, Jira/Linear tickets, commit URLs, design docs, Notion pages, etc. Example: ["https://github.com/org/repo/pull/123", "https://jira.company.com/browse/PROJ-456"]'
|
|
560
|
+
},
|
|
561
|
+
orgId: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "Organisation ID to associate the brag with"
|
|
564
|
+
},
|
|
565
|
+
agent: {
|
|
566
|
+
type: "string",
|
|
567
|
+
maxLength: 50,
|
|
568
|
+
description: 'Optional AI agent/client name for MCP-origin brags, e.g. "claude", "codex", "cursor", or "copilot".'
|
|
569
|
+
}
|
|
22
570
|
},
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
571
|
+
required: ["title", "description", "type", "impact_level", "impact_description", "references"]
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
function toAgentExternalType(agent) {
|
|
575
|
+
if (!agent) return void 0;
|
|
576
|
+
const normalized = agent.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "");
|
|
577
|
+
if (normalized.length === 0) return void 0;
|
|
578
|
+
return `agent:${normalized.slice(0, 50)}`;
|
|
579
|
+
}
|
|
580
|
+
async function handleLogBrag(input) {
|
|
581
|
+
const planError = await requirePaidPlan();
|
|
582
|
+
if (planError) return planError;
|
|
583
|
+
try {
|
|
584
|
+
const brag = {
|
|
585
|
+
title: input.title,
|
|
586
|
+
message: input.description,
|
|
587
|
+
description: input.description,
|
|
588
|
+
tags: input.tags || [],
|
|
589
|
+
date: input.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
590
|
+
visibility: "private"
|
|
591
|
+
};
|
|
592
|
+
if (input.impact_level !== void 0) brag.impactLevel = input.impact_level;
|
|
593
|
+
if (input.impact_description) brag.impact_description = input.impact_description;
|
|
594
|
+
if (input.type) brag.typeKey = input.type;
|
|
595
|
+
if (input.references && input.references.length > 0) brag.attachments = input.references;
|
|
596
|
+
if (input.orgId) brag.orgId = input.orgId;
|
|
597
|
+
const agentExternalType = toAgentExternalType(input.agent);
|
|
598
|
+
if (agentExternalType) brag.externalType = agentExternalType;
|
|
599
|
+
const response = await apiRequest("/v1/brags", {
|
|
600
|
+
method: "POST",
|
|
601
|
+
body: { brags: [brag], source: "mcp" }
|
|
602
|
+
});
|
|
603
|
+
if (response.brags && response.brags.length > 0) {
|
|
604
|
+
const created = response.brags[0];
|
|
605
|
+
const lines = [`Brag logged: "${created.title}"
|
|
606
|
+
View it at: ${created.url}`];
|
|
607
|
+
if (created.keyResultIds && created.keyResultIds.length > 0) {
|
|
608
|
+
lines.push(`Automatically linked to key result: ${created.keyResultIds[0]}`);
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
content: [{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: lines.join("\n")
|
|
614
|
+
}]
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const firstError = response.errors && response.errors.length > 0 ? response.errors[0].message : "Server returned no brag and no error detail";
|
|
618
|
+
return {
|
|
619
|
+
content: [{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: `Failed to create brag: ${firstError}`
|
|
622
|
+
}],
|
|
623
|
+
isError: true
|
|
624
|
+
};
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
627
|
+
return {
|
|
628
|
+
content: [{
|
|
629
|
+
type: "text",
|
|
630
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
631
|
+
}],
|
|
632
|
+
isError: true
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
content: [{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: `Failed to create brag: ${error instanceof Error ? error.message : String(error)}`
|
|
639
|
+
}],
|
|
640
|
+
isError: true
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/tools/quick-brag.ts
|
|
646
|
+
var quickBragTool = {
|
|
647
|
+
name: "quick_brag",
|
|
648
|
+
description: 'Log a brag from raw text. Accepts free-form text (a sentence, paragraph, or paste from a message/PR/doc) and uses AI to extract and structure it into a proper brag. The AI will parse the text into a title, description, type, impact, and tags, then log it. Use this when the user says "/brag" followed by text, or pastes something and asks to log it as a brag.',
|
|
649
|
+
inputSchema: {
|
|
650
|
+
type: "object",
|
|
651
|
+
properties: {
|
|
652
|
+
text: {
|
|
653
|
+
type: "string",
|
|
654
|
+
minLength: 10,
|
|
655
|
+
maxLength: 5e3,
|
|
656
|
+
description: "Raw text describing the achievement. Can be a sentence, paragraph, paste from a PR description, Slack message, commit message, or any free-form text. The AI will extract brag fields from it."
|
|
657
|
+
},
|
|
658
|
+
orgId: {
|
|
659
|
+
type: "string",
|
|
660
|
+
description: "Organisation ID to associate the brag with (optional)"
|
|
661
|
+
},
|
|
662
|
+
agent: {
|
|
663
|
+
type: "string",
|
|
664
|
+
maxLength: 50,
|
|
665
|
+
description: 'Optional AI agent/client name, e.g. "claude", "cursor", "copilot".'
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
required: ["text"]
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
function toAgentExternalType2(agent) {
|
|
672
|
+
if (!agent) return void 0;
|
|
673
|
+
const normalized = agent.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "");
|
|
674
|
+
if (normalized.length === 0) return void 0;
|
|
675
|
+
return `agent:${normalized.slice(0, 50)}`;
|
|
676
|
+
}
|
|
677
|
+
async function handleQuickBrag(input) {
|
|
678
|
+
const planError = await requirePaidPlan();
|
|
679
|
+
if (planError) return planError;
|
|
680
|
+
try {
|
|
681
|
+
const refined = await apiRequest("/v1/brags/refine", {
|
|
682
|
+
method: "POST",
|
|
683
|
+
body: { brags: [{ text: input.text }] }
|
|
684
|
+
});
|
|
685
|
+
if (!refined.refined_brags || refined.refined_brags.length === 0) {
|
|
686
|
+
return {
|
|
687
|
+
content: [{
|
|
688
|
+
type: "text",
|
|
689
|
+
text: "Could not extract a brag from the provided text. Try adding more detail about what was accomplished and its impact."
|
|
690
|
+
}],
|
|
691
|
+
isError: true
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const extracted = refined.refined_brags[0];
|
|
695
|
+
const brag = {
|
|
696
|
+
title: extracted.refined_title,
|
|
697
|
+
description: extracted.refined_description,
|
|
698
|
+
tags: extracted.suggested_tags || [],
|
|
699
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
700
|
+
visibility: "private",
|
|
701
|
+
impactLevel: extracted.suggested_impactLevel,
|
|
702
|
+
impact_description: extracted.impact_description || "",
|
|
703
|
+
typeKey: extracted.suggested_type_key
|
|
704
|
+
};
|
|
705
|
+
const allLinks = [
|
|
706
|
+
...extracted.extracted_links || [],
|
|
707
|
+
...extracted.attachments || []
|
|
708
|
+
].filter(Boolean);
|
|
709
|
+
if (allLinks.length > 0) brag.attachments = allLinks;
|
|
710
|
+
if (input.orgId) brag.orgId = input.orgId;
|
|
711
|
+
const agentExternalType = toAgentExternalType2(input.agent);
|
|
712
|
+
if (agentExternalType) brag.externalType = agentExternalType;
|
|
713
|
+
const response = await apiRequest("/v1/brags", {
|
|
714
|
+
method: "POST",
|
|
715
|
+
body: { brags: [brag], source: "mcp" }
|
|
716
|
+
});
|
|
717
|
+
if (response.errors && response.errors.length > 0) {
|
|
718
|
+
const errorMessages = response.errors.map((e) => e.message).join("; ");
|
|
719
|
+
return {
|
|
720
|
+
content: [{
|
|
721
|
+
type: "text",
|
|
722
|
+
text: `Failed to create brag: ${errorMessages}`
|
|
723
|
+
}],
|
|
724
|
+
isError: true
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
if (response.brags && response.brags.length > 0) {
|
|
728
|
+
const created = response.brags[0];
|
|
729
|
+
const lines = [
|
|
730
|
+
`Brag logged: "${created.title}"`,
|
|
731
|
+
`${created.url}`
|
|
732
|
+
];
|
|
733
|
+
if (created.keyResultIds && created.keyResultIds.length > 0) {
|
|
734
|
+
lines.push(`Automatically linked to key result: ${created.keyResultIds[0]}`);
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
content: [{
|
|
738
|
+
type: "text",
|
|
739
|
+
text: lines.join("\n")
|
|
740
|
+
}]
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
content: [{
|
|
745
|
+
type: "text",
|
|
746
|
+
text: "Server returned no brag and no error detail"
|
|
747
|
+
}],
|
|
748
|
+
isError: true
|
|
749
|
+
};
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
752
|
+
return {
|
|
753
|
+
content: [{
|
|
754
|
+
type: "text",
|
|
755
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
756
|
+
}],
|
|
757
|
+
isError: true
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
content: [{
|
|
762
|
+
type: "text",
|
|
763
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
764
|
+
}],
|
|
765
|
+
isError: true
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/tools/preview-brag.ts
|
|
771
|
+
var IMPACT_LABELS = {
|
|
772
|
+
0: "None (personal learning / routine)",
|
|
773
|
+
1: "Low (individual contributor impact)",
|
|
774
|
+
2: "Medium (team-level impact)",
|
|
775
|
+
3: "High (organisation-level impact)"
|
|
776
|
+
};
|
|
777
|
+
var TYPE_EMOJIS = {
|
|
778
|
+
progress: "\u{1F4C8}",
|
|
779
|
+
achievement: "\u{1F3C6}",
|
|
780
|
+
recognition: "\u{1F31F}",
|
|
781
|
+
support: "\u{1F91D}",
|
|
782
|
+
code: "\u{1F4BB}",
|
|
783
|
+
leadership: "\u{1F9ED}",
|
|
784
|
+
learning: "\u{1F4DA}",
|
|
785
|
+
collaboration: "\u{1F465}",
|
|
786
|
+
challenge: "\u2694\uFE0F",
|
|
787
|
+
innovation: "\u{1F4A1}",
|
|
788
|
+
meeting: "\u{1F5E3}\uFE0F",
|
|
789
|
+
bug: "\u{1F41B}",
|
|
790
|
+
others: "\u{1F4CC}"
|
|
791
|
+
};
|
|
792
|
+
function handlePreviewBrag(input) {
|
|
793
|
+
const emoji = TYPE_EMOJIS[input.type] ?? "\u{1F4CC}";
|
|
794
|
+
const impactLabel = IMPACT_LABELS[input.impact_level] ?? String(input.impact_level);
|
|
795
|
+
return {
|
|
796
|
+
content: [
|
|
797
|
+
{
|
|
798
|
+
type: "text",
|
|
799
|
+
text: JSON.stringify({
|
|
800
|
+
title: input.title,
|
|
801
|
+
type: `${emoji} ${input.type}`,
|
|
802
|
+
impact_level: impactLabel,
|
|
803
|
+
impact_details: input.impact_description,
|
|
804
|
+
description: input.description,
|
|
805
|
+
tags: input.tags && input.tags.length > 0 ? input.tags.join(", ") : "\u2014",
|
|
806
|
+
references: input.references && input.references.length > 0 ? input.references.join(", ") : "\u2014"
|
|
807
|
+
})
|
|
808
|
+
}
|
|
809
|
+
]
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/tools/list-brags.ts
|
|
814
|
+
var listBragsTool = {
|
|
815
|
+
name: "list_brags",
|
|
816
|
+
description: "List recent brags (career achievements) from BragDuck. Use this to review past accomplishments, find brags to link to keyResults, or check recent entries.",
|
|
817
|
+
inputSchema: {
|
|
818
|
+
type: "object",
|
|
819
|
+
properties: {
|
|
820
|
+
limit: {
|
|
821
|
+
type: "number",
|
|
822
|
+
description: "Number of brags to return (default: 10, max: 100)"
|
|
823
|
+
},
|
|
824
|
+
search: {
|
|
825
|
+
type: "string",
|
|
826
|
+
description: "Search term to filter brags by title or description"
|
|
827
|
+
},
|
|
828
|
+
tags: {
|
|
829
|
+
type: "array",
|
|
830
|
+
items: { type: "string" },
|
|
831
|
+
description: "Filter brags by tags"
|
|
832
|
+
},
|
|
833
|
+
date_from: {
|
|
834
|
+
type: "string",
|
|
835
|
+
description: "Filter brags from this date (YYYY-MM-DD)"
|
|
836
|
+
},
|
|
837
|
+
date_to: {
|
|
838
|
+
type: "string",
|
|
839
|
+
description: "Filter brags up to this date (YYYY-MM-DD)"
|
|
840
|
+
},
|
|
841
|
+
sort: {
|
|
842
|
+
type: "string",
|
|
843
|
+
enum: ["date_asc", "date_desc", "title_asc", "title_desc", "created_asc", "created_desc"],
|
|
844
|
+
description: "Sort order (default: date_desc)"
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
var IMPACT_LABELS2 = {
|
|
850
|
+
0: "None",
|
|
851
|
+
1: "Low",
|
|
852
|
+
2: "Medium",
|
|
853
|
+
3: "High"
|
|
854
|
+
};
|
|
855
|
+
async function handleListBrags(input) {
|
|
856
|
+
const planError = await requirePaidPlan();
|
|
857
|
+
if (planError) return planError;
|
|
858
|
+
try {
|
|
859
|
+
const params = {
|
|
860
|
+
limit: String(input.limit || 10),
|
|
861
|
+
offset: "0"
|
|
862
|
+
};
|
|
863
|
+
if (input.search) params.search = input.search;
|
|
864
|
+
if (input.tags && input.tags.length > 0) params["tags[]"] = input.tags;
|
|
865
|
+
if (input.date_from) params.date_from = input.date_from;
|
|
866
|
+
if (input.date_to) params.date_to = input.date_to;
|
|
867
|
+
if (input.sort) params.sort = input.sort;
|
|
868
|
+
const response = await apiRequest("/v1/brags", { params });
|
|
869
|
+
if (response.brags.length === 0) {
|
|
870
|
+
return {
|
|
871
|
+
content: [{ type: "text", text: "No brags found." }]
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const lines = response.brags.map((brag) => {
|
|
875
|
+
const tags = brag.tags.length > 0 ? ` [${brag.tags.join(", ")}]` : "";
|
|
876
|
+
const date = new Date(brag.date).toLocaleDateString();
|
|
877
|
+
const impact = `Impact: ${IMPACT_LABELS2[brag.impactLevel ?? 0] ?? (brag.impactLevel ?? 0)}`;
|
|
878
|
+
const type = brag.type ? ` | Type: ${brag.type}` : "";
|
|
879
|
+
const keyResultNote = brag.keyResultIds?.length > 0 ? `
|
|
880
|
+
Linked to key result: ${brag.keyResultIds[0]}` : "";
|
|
881
|
+
const impactDesc = brag.impactDescription ? `
|
|
882
|
+
Impact: ${brag.impactDescription}` : "";
|
|
883
|
+
return `- **${brag.title}** (ID: ${brag.id}, ${date})${tags}
|
|
884
|
+
${impact}${type}` + impactDesc + keyResultNote + `
|
|
885
|
+
${brag.description.slice(0, 150)}${brag.description.length > 150 ? "..." : ""}
|
|
886
|
+
View: ${brag.url}`;
|
|
887
|
+
});
|
|
888
|
+
const { total, has_more } = response.pagination;
|
|
889
|
+
const summary = `Found ${total} brags (showing ${response.brags.length}${has_more ? ", more available" : ""}):`;
|
|
890
|
+
return {
|
|
891
|
+
content: [{ type: "text", text: `${summary}
|
|
892
|
+
|
|
893
|
+
${lines.join("\n\n")}` }]
|
|
894
|
+
};
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
897
|
+
return {
|
|
898
|
+
content: [{
|
|
899
|
+
type: "text",
|
|
900
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
901
|
+
}],
|
|
902
|
+
isError: true
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
content: [{
|
|
907
|
+
type: "text",
|
|
908
|
+
text: `Failed to list brags: ${error instanceof Error ? error.message : String(error)}`
|
|
909
|
+
}],
|
|
910
|
+
isError: true
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/tools/list-objectives.ts
|
|
916
|
+
var listObjectivesTool = {
|
|
917
|
+
name: "list_objectives",
|
|
918
|
+
description: "List OKRs (Objectives and Key Results) from BragDuck. Use this to see current keyResults and their IDs, which can be used to link brags to specific keyResults.",
|
|
919
|
+
inputSchema: {
|
|
920
|
+
type: "object",
|
|
921
|
+
properties: {}
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
async function handleListObjectives() {
|
|
925
|
+
const planError = await requirePaidPlan();
|
|
926
|
+
if (planError) return planError;
|
|
927
|
+
try {
|
|
928
|
+
const userInfo = getUserInfo();
|
|
929
|
+
if (!userInfo) {
|
|
930
|
+
return {
|
|
931
|
+
content: [{
|
|
932
|
+
type: "text",
|
|
933
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
934
|
+
}],
|
|
935
|
+
isError: true
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const response = await apiRequest(
|
|
939
|
+
`/v1/users/${userInfo.id}/objectives`
|
|
940
|
+
);
|
|
941
|
+
if (response.items.length === 0) {
|
|
942
|
+
return {
|
|
943
|
+
content: [{ type: "text", text: "No objectives found." }]
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const lines = response.items.map((obj) => {
|
|
947
|
+
const keyResults = obj.keyResults.map((g) => ` - ${g.label} (KeyResult ID: ${g.id}, status: ${g.status})`).join("\n");
|
|
948
|
+
const category = obj.category ? ` [${obj.category}]` : "";
|
|
949
|
+
return `- **${obj.title}** (ID: ${obj.id}, status: ${obj.status})${category}
|
|
950
|
+
${keyResults || " (no keyResults)"}`;
|
|
951
|
+
});
|
|
952
|
+
return {
|
|
953
|
+
content: [{
|
|
954
|
+
type: "text",
|
|
955
|
+
text: `Found ${response.total} objectives:
|
|
956
|
+
|
|
957
|
+
${lines.join("\n\n")}`
|
|
958
|
+
}]
|
|
959
|
+
};
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
962
|
+
return {
|
|
963
|
+
content: [{
|
|
964
|
+
type: "text",
|
|
965
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
966
|
+
}],
|
|
967
|
+
isError: true
|
|
968
|
+
};
|
|
969
|
+
}
|
|
96
970
|
return {
|
|
97
|
-
|
|
971
|
+
content: [{
|
|
972
|
+
type: "text",
|
|
973
|
+
text: `Failed to list objectives: ${error instanceof Error ? error.message : String(error)}`
|
|
974
|
+
}],
|
|
975
|
+
isError: true
|
|
98
976
|
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/tools/link-brag-to-key-result.ts
|
|
981
|
+
var linkBragToKeyResultTool = {
|
|
982
|
+
name: "link_brag_to_key_result",
|
|
983
|
+
description: "Add a keyResult/key-result to a brag (supports multiple keyResults per brag). Use list_objectives to find keyResult IDs and list_brags to find brag IDs.",
|
|
984
|
+
inputSchema: {
|
|
985
|
+
type: "object",
|
|
986
|
+
properties: {
|
|
987
|
+
bragId: {
|
|
988
|
+
type: "string",
|
|
989
|
+
description: "The ID of the brag to update"
|
|
990
|
+
},
|
|
991
|
+
keyResultId: {
|
|
992
|
+
type: "string",
|
|
993
|
+
description: "The ID of the keyResult to link the brag to"
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
required: ["bragId", "keyResultId"]
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
async function handleLinkBragToKeyResult(input) {
|
|
1000
|
+
const planError = await requirePaidPlan();
|
|
1001
|
+
if (planError) return planError;
|
|
1002
|
+
try {
|
|
1003
|
+
const userInfo = getUserInfo();
|
|
1004
|
+
if (!userInfo) {
|
|
1005
|
+
return {
|
|
1006
|
+
content: [{
|
|
1007
|
+
type: "text",
|
|
1008
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1009
|
+
}],
|
|
1010
|
+
isError: true
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const currentBrag = await apiRequest(
|
|
1014
|
+
`/v1/users/${userInfo.id}/brags/${input.bragId}`
|
|
1015
|
+
);
|
|
1016
|
+
const existingKeyResultIds = currentBrag.keyResultIds || [];
|
|
1017
|
+
if (existingKeyResultIds.includes(input.keyResultId)) {
|
|
1018
|
+
return {
|
|
1019
|
+
content: [{
|
|
1020
|
+
type: "text",
|
|
1021
|
+
text: `Brag ${input.bragId} is already linked to keyResult ${input.keyResultId}.`
|
|
1022
|
+
}]
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const updatedKeyResultIds = [...existingKeyResultIds, input.keyResultId];
|
|
1026
|
+
await apiRequest(`/v1/users/${userInfo.id}/brags/${input.bragId}`, {
|
|
1027
|
+
method: "PATCH",
|
|
1028
|
+
body: { keyResultIds: updatedKeyResultIds }
|
|
1029
|
+
});
|
|
1030
|
+
return {
|
|
1031
|
+
content: [{
|
|
1032
|
+
type: "text",
|
|
1033
|
+
text: `KeyResult ${input.keyResultId} added to brag ${input.bragId} (now linked to ${updatedKeyResultIds.length} keyResult${updatedKeyResultIds.length === 1 ? "" : "s"}).`
|
|
1034
|
+
}]
|
|
1035
|
+
};
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
1038
|
+
return {
|
|
1039
|
+
content: [{
|
|
1040
|
+
type: "text",
|
|
1041
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1042
|
+
}],
|
|
1043
|
+
isError: true
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
content: [{
|
|
1048
|
+
type: "text",
|
|
1049
|
+
text: `Failed to link brag to keyResult: ${error instanceof Error ? error.message : String(error)}`
|
|
1050
|
+
}],
|
|
1051
|
+
isError: true
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/org-preference.ts
|
|
1057
|
+
import Conf2 from "conf";
|
|
1058
|
+
var APP_NAME2 = "bragduck";
|
|
1059
|
+
function getConfig() {
|
|
1060
|
+
return new Conf2({ projectName: APP_NAME2 });
|
|
1061
|
+
}
|
|
1062
|
+
function getOrgPreference(cwd) {
|
|
1063
|
+
try {
|
|
1064
|
+
const config = getConfig();
|
|
1065
|
+
const prefs = config.get("orgPreferences") || {};
|
|
1066
|
+
return prefs[cwd] || null;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function saveOrgPreference(cwd, orgId, orgName) {
|
|
1072
|
+
const config = getConfig();
|
|
1073
|
+
const prefs = config.get("orgPreferences") || {};
|
|
1074
|
+
prefs[cwd] = { orgId, orgName };
|
|
1075
|
+
config.set("orgPreferences", prefs);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/tools/list-organisations.ts
|
|
1079
|
+
var listOrganisationsTool = {
|
|
1080
|
+
name: "list_organisations",
|
|
1081
|
+
description: "List the user's organisations (companies) from BragDuck. Also returns the saved organisation preference for the current working directory, if any. Use this before logging a brag to determine which company to associate it with.",
|
|
1082
|
+
inputSchema: {
|
|
1083
|
+
type: "object",
|
|
1084
|
+
properties: {}
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
async function handleListOrganisations() {
|
|
1088
|
+
const planError = await requirePaidPlan();
|
|
1089
|
+
if (planError) return planError;
|
|
1090
|
+
try {
|
|
1091
|
+
const userInfo = getUserInfo();
|
|
1092
|
+
if (!userInfo) {
|
|
1093
|
+
return {
|
|
1094
|
+
content: [{
|
|
1095
|
+
type: "text",
|
|
1096
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1097
|
+
}],
|
|
1098
|
+
isError: true
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const response = await apiRequest(
|
|
1102
|
+
`/v1/users/${userInfo.id}/organisations`
|
|
1103
|
+
);
|
|
1104
|
+
if (response.items.length === 0) {
|
|
1105
|
+
return {
|
|
1106
|
+
content: [{
|
|
1107
|
+
type: "text",
|
|
1108
|
+
text: "No organisations found. You can add companies in the BragDuck web app."
|
|
1109
|
+
}]
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
const cwd = process.cwd();
|
|
1113
|
+
const saved = getOrgPreference(cwd);
|
|
1114
|
+
const lines = response.items.map((org) => {
|
|
1115
|
+
const status = org.current ? "current" : "former";
|
|
1116
|
+
return `- **${org.name}** (ID: ${org.id}, role: ${org.role}, ${status})`;
|
|
1117
|
+
});
|
|
1118
|
+
const savedLine = saved ? `
|
|
1119
|
+
Saved preference for this directory: **${saved.orgName}** (ID: ${saved.orgId})` : "\nNo saved preference for this directory.";
|
|
1120
|
+
return {
|
|
1121
|
+
content: [{
|
|
1122
|
+
type: "text",
|
|
1123
|
+
text: `Found ${response.items.length} organisation(s):
|
|
1124
|
+
|
|
1125
|
+
${lines.join("\n")}${savedLine}`
|
|
1126
|
+
}]
|
|
1127
|
+
};
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
1130
|
+
return {
|
|
1131
|
+
content: [{
|
|
1132
|
+
type: "text",
|
|
1133
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1134
|
+
}],
|
|
1135
|
+
isError: true
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
content: [{
|
|
1140
|
+
type: "text",
|
|
1141
|
+
text: `Failed to list organisations: ${error instanceof Error ? error.message : String(error)}`
|
|
1142
|
+
}],
|
|
1143
|
+
isError: true
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/tools/save-org-preference.ts
|
|
1149
|
+
var saveOrgPreferenceTool = {
|
|
1150
|
+
name: "save_org_preference",
|
|
1151
|
+
description: "Save the organisation preference for the current working directory. Call this after the user selects which company a brag belongs to, so the same choice is remembered for future brags in this directory.",
|
|
1152
|
+
inputSchema: {
|
|
1153
|
+
type: "object",
|
|
1154
|
+
properties: {
|
|
1155
|
+
orgId: {
|
|
1156
|
+
type: "string",
|
|
1157
|
+
description: "The organisation ID to save as the preference for this directory"
|
|
1158
|
+
},
|
|
1159
|
+
orgName: {
|
|
1160
|
+
type: "string",
|
|
1161
|
+
description: "The organisation name (for display purposes)"
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
required: ["orgId", "orgName"]
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
async function handleSaveOrgPreference(input) {
|
|
1168
|
+
const planError = await requirePaidPlan();
|
|
1169
|
+
if (planError) return planError;
|
|
1170
|
+
try {
|
|
1171
|
+
const cwd = process.cwd();
|
|
1172
|
+
saveOrgPreference(cwd, input.orgId, input.orgName);
|
|
1173
|
+
return {
|
|
1174
|
+
content: [{
|
|
1175
|
+
type: "text",
|
|
1176
|
+
text: `Saved: brags in this directory will be associated with **${input.orgName}**.`
|
|
1177
|
+
}]
|
|
1178
|
+
};
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
return {
|
|
1181
|
+
content: [{
|
|
1182
|
+
type: "text",
|
|
1183
|
+
text: `Failed to save preference: ${error instanceof Error ? error.message : String(error)}`
|
|
1184
|
+
}],
|
|
1185
|
+
isError: true
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/tools/log-story.ts
|
|
1191
|
+
var logStoryTool = {
|
|
1192
|
+
name: "log_story",
|
|
1193
|
+
description: "Save a STAR-formatted interview story to BragDuck. Use this when the user discusses interview prep, behavioral questions, or asks to save a story. Each story has Situation, Task, Action, Result fields plus competency tags. Call this in the same response as a preview \u2014 the tool permission prompt (Allow/Deny) that appears to the user IS the confirmation.",
|
|
1194
|
+
inputSchema: {
|
|
1195
|
+
type: "object",
|
|
1196
|
+
properties: {
|
|
1197
|
+
title: {
|
|
1198
|
+
type: "string",
|
|
1199
|
+
minLength: 5,
|
|
1200
|
+
maxLength: 200,
|
|
1201
|
+
description: 'Short title for the story (e.g., "Led migration to microservices")'
|
|
1202
|
+
},
|
|
1203
|
+
situation: {
|
|
1204
|
+
type: "string",
|
|
1205
|
+
minLength: 10,
|
|
1206
|
+
description: "The context and background \u2014 what was happening, what was the environment"
|
|
1207
|
+
},
|
|
1208
|
+
task: {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
minLength: 10,
|
|
1211
|
+
description: "What was your specific responsibility or challenge"
|
|
1212
|
+
},
|
|
1213
|
+
action: {
|
|
1214
|
+
type: "string",
|
|
1215
|
+
minLength: 10,
|
|
1216
|
+
description: "What you specifically did \u2014 the steps you took"
|
|
1217
|
+
},
|
|
1218
|
+
result: {
|
|
1219
|
+
type: "string",
|
|
1220
|
+
minLength: 10,
|
|
1221
|
+
description: "The outcome \u2014 measurable impact, what changed, what you learned"
|
|
1222
|
+
},
|
|
1223
|
+
competencies: {
|
|
1224
|
+
type: "array",
|
|
1225
|
+
items: { type: "string", minLength: 2, maxLength: 50 },
|
|
1226
|
+
maxItems: 20,
|
|
1227
|
+
description: 'Skills or competencies this story demonstrates (e.g., "leadership", "system design", "conflict resolution")'
|
|
1228
|
+
},
|
|
1229
|
+
bragIds: {
|
|
1230
|
+
type: "array",
|
|
1231
|
+
items: { type: "string" },
|
|
1232
|
+
maxItems: 20,
|
|
1233
|
+
description: "IDs of BragDuck brags that back this story"
|
|
1234
|
+
},
|
|
1235
|
+
orgId: {
|
|
1236
|
+
type: "string",
|
|
1237
|
+
description: "Organisation ID to associate the story with"
|
|
1238
|
+
},
|
|
1239
|
+
category: {
|
|
1240
|
+
type: "string",
|
|
1241
|
+
description: "Interview category this story demonstrates (e.g. leadership, technical_excellence, problem_solving, innovation, teamwork, communication, project_delivery, customer_impact, mentorship, process_improvement)"
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
required: ["title", "situation", "task", "action", "result"]
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
async function handleLogStory(input) {
|
|
1248
|
+
const planError = await requirePaidPlan();
|
|
1249
|
+
if (planError) return planError;
|
|
1250
|
+
try {
|
|
1251
|
+
const userInfo = getUserInfo();
|
|
1252
|
+
if (!userInfo) {
|
|
1253
|
+
return {
|
|
1254
|
+
content: [{
|
|
1255
|
+
type: "text",
|
|
1256
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1257
|
+
}],
|
|
1258
|
+
isError: true
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const body = {
|
|
1262
|
+
title: input.title,
|
|
1263
|
+
situation: input.situation,
|
|
1264
|
+
task: input.task,
|
|
1265
|
+
action: input.action,
|
|
1266
|
+
result: input.result,
|
|
1267
|
+
competencies: input.competencies || [],
|
|
1268
|
+
bragIds: input.bragIds || [],
|
|
1269
|
+
source: "mcp"
|
|
1270
|
+
};
|
|
1271
|
+
if (input.orgId) body.orgId = input.orgId;
|
|
1272
|
+
if (input.category) body.category = input.category;
|
|
1273
|
+
const story = await apiRequest(
|
|
1274
|
+
`/v1/users/${userInfo.id}/stories`,
|
|
1275
|
+
{ method: "POST", body }
|
|
1276
|
+
);
|
|
1277
|
+
const competencyLine = story.competencies.length > 0 ? `
|
|
1278
|
+
Competencies: ${story.competencies.join(", ")}` : "";
|
|
1279
|
+
return {
|
|
1280
|
+
content: [{
|
|
1281
|
+
type: "text",
|
|
1282
|
+
text: `Story saved: "${story.title}"${competencyLine}`
|
|
1283
|
+
}]
|
|
1284
|
+
};
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
1287
|
+
return {
|
|
1288
|
+
content: [{
|
|
1289
|
+
type: "text",
|
|
1290
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1291
|
+
}],
|
|
1292
|
+
isError: true
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
content: [{
|
|
1297
|
+
type: "text",
|
|
1298
|
+
text: `Failed to save story: ${error instanceof Error ? error.message : String(error)}`
|
|
1299
|
+
}],
|
|
1300
|
+
isError: true
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// ../server/src/utils/subscription/feature-config.ts
|
|
1306
|
+
var ORG_TEAM_ROLE_FEATURES = [
|
|
1307
|
+
"org_access" /* ORG_ACCESS */,
|
|
1308
|
+
"org_mutate_base" /* ORG_MUTATE_BASE */,
|
|
1309
|
+
"org_admin_access" /* ORG_ADMIN_ACCESS */,
|
|
1310
|
+
"org_owner_access" /* ORG_OWNER_ACCESS */,
|
|
1311
|
+
"org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */,
|
|
1312
|
+
"manager_tier_access" /* MANAGER_TIER_ACCESS */,
|
|
1313
|
+
"team_manage" /* TEAM_MANAGE */,
|
|
1314
|
+
"team_owner_actions" /* TEAM_OWNER_ACTIONS */,
|
|
1315
|
+
"team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
|
|
1316
|
+
"org_members_manage" /* ORG_MEMBERS_MANAGE */,
|
|
1317
|
+
"org_invites_manage" /* ORG_INVITES_MANAGE */,
|
|
1318
|
+
"org_teams_manage" /* ORG_TEAMS_MANAGE */,
|
|
1319
|
+
"org_growth_view" /* ORG_GROWTH_VIEW */,
|
|
1320
|
+
"org_career_metrics_manage" /* ORG_CAREER_METRICS_MANAGE */,
|
|
1321
|
+
"org_cycles_manage" /* ORG_CYCLES_MANAGE */,
|
|
1322
|
+
"org_stats_view" /* ORG_STATS_VIEW */,
|
|
1323
|
+
"org_audit_log_view" /* ORG_AUDIT_LOG_VIEW */,
|
|
1324
|
+
"org_billing_manage" /* ORG_BILLING_MANAGE */,
|
|
1325
|
+
"org_notifications_manage" /* ORG_NOTIFICATIONS_MANAGE */
|
|
1326
|
+
];
|
|
1327
|
+
var DISABLED_ORG_TEAM_FEATURES = Object.fromEntries(
|
|
1328
|
+
ORG_TEAM_ROLE_FEATURES.map((f) => [f, { enabled: false }])
|
|
1329
|
+
);
|
|
1330
|
+
var UNIVERSAL_FEATURES = [
|
|
1331
|
+
"brags_create" /* BRAGS_CREATE */,
|
|
1332
|
+
"brags_read" /* BRAGS_READ */,
|
|
1333
|
+
"brags_update" /* BRAGS_UPDATE */,
|
|
1334
|
+
"brags_delete" /* BRAGS_DELETE */,
|
|
1335
|
+
"stories_create" /* STORIES_CREATE */,
|
|
1336
|
+
"stories_read" /* STORIES_READ */,
|
|
1337
|
+
"stories_update" /* STORIES_UPDATE */,
|
|
1338
|
+
"stories_delete" /* STORIES_DELETE */,
|
|
1339
|
+
"brag_comments_create" /* BRAG_COMMENTS_CREATE */,
|
|
1340
|
+
"brag_comments_read" /* BRAG_COMMENTS_READ */,
|
|
1341
|
+
"brag_comments_update" /* BRAG_COMMENTS_UPDATE */,
|
|
1342
|
+
"brag_comments_delete" /* BRAG_COMMENTS_DELETE */,
|
|
1343
|
+
"brag_types_create" /* BRAG_TYPES_CREATE */,
|
|
1344
|
+
"brag_types_read" /* BRAG_TYPES_READ */,
|
|
1345
|
+
"brag_types_update" /* BRAG_TYPES_UPDATE */,
|
|
1346
|
+
"objectives_read" /* OBJECTIVES_READ */,
|
|
1347
|
+
"objectives_update" /* OBJECTIVES_UPDATE */,
|
|
1348
|
+
"objectives_delete" /* OBJECTIVES_DELETE */,
|
|
1349
|
+
"objectives_key_results_create" /* OBJECTIVES_KEY_RESULTS_CREATE */,
|
|
1350
|
+
"objectives_key_results_update" /* OBJECTIVES_KEY_RESULTS_UPDATE */,
|
|
1351
|
+
"objectives_key_results_delete" /* OBJECTIVES_KEY_RESULTS_DELETE */,
|
|
1352
|
+
"reports_read" /* REPORTS_READ */,
|
|
1353
|
+
"reports_update" /* REPORTS_UPDATE */,
|
|
1354
|
+
"reports_delete" /* REPORTS_DELETE */,
|
|
1355
|
+
"shares_read" /* SHARES_READ */,
|
|
1356
|
+
"shares_delete" /* SHARES_DELETE */,
|
|
1357
|
+
"fields_read" /* FIELDS_READ */,
|
|
1358
|
+
"user_emails_read" /* USER_EMAILS_READ */,
|
|
1359
|
+
"notifications_preferences_read" /* NOTIFICATIONS_PREFERENCES_READ */,
|
|
1360
|
+
"notifications_preferences_update" /* NOTIFICATIONS_PREFERENCES_UPDATE */,
|
|
1361
|
+
"security_info_read" /* SECURITY_INFO_READ */,
|
|
1362
|
+
"data_access_log_read" /* DATA_ACCESS_LOG_READ */,
|
|
1363
|
+
"users_read" /* USERS_READ */,
|
|
1364
|
+
"users_update" /* USERS_UPDATE */,
|
|
1365
|
+
"users_delete" /* USERS_DELETE */,
|
|
1366
|
+
"badges_read" /* BADGES_READ */,
|
|
1367
|
+
"user_teams_accept_invite" /* USER_TEAMS_ACCEPT_INVITE */,
|
|
1368
|
+
"user_teams_reject_invite" /* USER_TEAMS_REJECT_INVITE */,
|
|
1369
|
+
"page_dashboard_view" /* PAGE_DASHBOARD_VIEW */,
|
|
1370
|
+
"page_brags_view" /* PAGE_BRAGS_VIEW */,
|
|
1371
|
+
"page_stories_view" /* PAGE_STORIES_VIEW */,
|
|
1372
|
+
"page_okrs_view" /* PAGE_OKRS_VIEW */,
|
|
1373
|
+
"page_reports_view" /* PAGE_REPORTS_VIEW */,
|
|
1374
|
+
"page_files_view" /* PAGE_FILES_VIEW */,
|
|
1375
|
+
"page_settings_view" /* PAGE_SETTINGS_VIEW */,
|
|
1376
|
+
"page_integrations_view" /* PAGE_INTEGRATIONS_VIEW */,
|
|
1377
|
+
"settings_tab_account_view" /* SETTINGS_TAB_ACCOUNT_VIEW */,
|
|
1378
|
+
"settings_tab_preferences_view" /* SETTINGS_TAB_PREFERENCES_VIEW */,
|
|
1379
|
+
"settings_tab_notifications_view" /* SETTINGS_TAB_NOTIFICATIONS_VIEW */,
|
|
1380
|
+
"settings_tab_billing_view" /* SETTINGS_TAB_BILLING_VIEW */,
|
|
1381
|
+
"settings_tab_org_view" /* SETTINGS_TAB_ORG_VIEW */,
|
|
1382
|
+
"settings_tab_plans_view" /* SETTINGS_TAB_PLANS_VIEW */,
|
|
1383
|
+
// Account features: plans belong to companies, so these are open to everyone
|
|
1384
|
+
"brags_export" /* BRAGS_EXPORT */,
|
|
1385
|
+
"brags_import" /* BRAGS_IMPORT */,
|
|
1386
|
+
"reports_csv_export" /* REPORTS_CSV_EXPORT */,
|
|
1387
|
+
"llm_keys_create" /* LLM_KEYS_CREATE */,
|
|
1388
|
+
"llm_keys_read" /* LLM_KEYS_READ */,
|
|
1389
|
+
"llm_keys_update" /* LLM_KEYS_UPDATE */,
|
|
1390
|
+
"llm_keys_delete" /* LLM_KEYS_DELETE */,
|
|
1391
|
+
"llm_keys_validate" /* LLM_KEYS_VALIDATE */,
|
|
1392
|
+
"llm_settings_read" /* LLM_SETTINGS_READ */,
|
|
1393
|
+
"llm_settings_update" /* LLM_SETTINGS_UPDATE */,
|
|
1394
|
+
"user_settings_openai_key_manage" /* USER_SETTINGS_OPENAI_KEY_MANAGE */,
|
|
1395
|
+
"notifications_settings_read" /* NOTIFICATIONS_SETTINGS_READ */,
|
|
1396
|
+
"notifications_settings_update" /* NOTIFICATIONS_SETTINGS_UPDATE */,
|
|
1397
|
+
"settings_tab_ai_keys_view" /* SETTINGS_TAB_AI_KEYS_VIEW */,
|
|
1398
|
+
"user_emails_update" /* USER_EMAILS_UPDATE */,
|
|
1399
|
+
"user_emails_delete" /* USER_EMAILS_DELETE */,
|
|
1400
|
+
"user_emails_verify" /* USER_EMAILS_VERIFY */,
|
|
1401
|
+
"badges_read_others" /* BADGES_READ_OTHERS */
|
|
1402
|
+
];
|
|
1403
|
+
var PLUS_GATED_FEATURES = [
|
|
1404
|
+
"cli_brags_create" /* CLI_BRAGS_CREATE */,
|
|
1405
|
+
"cli_brags_read" /* CLI_BRAGS_READ */,
|
|
1406
|
+
"cli_brags_refine" /* CLI_BRAGS_REFINE */,
|
|
1407
|
+
"commits_refine" /* COMMITS_REFINE */,
|
|
1408
|
+
"ask_ducky" /* ASK_DUCKY */
|
|
1409
|
+
];
|
|
1410
|
+
var PRO_GATED_FEATURES = [
|
|
1411
|
+
"page_practice_view" /* PAGE_PRACTICE_VIEW */,
|
|
1412
|
+
"page_resume_view" /* PAGE_RESUME_VIEW */
|
|
1413
|
+
];
|
|
1414
|
+
var TEAM_GATED_FEATURES = [
|
|
1415
|
+
"evaluation_comments_create" /* EVALUATION_COMMENTS_CREATE */,
|
|
1416
|
+
"evaluation_comments_read" /* EVALUATION_COMMENTS_READ */,
|
|
1417
|
+
"peer_reviews_view" /* PEER_REVIEWS_VIEW */,
|
|
1418
|
+
"peer_reviews_accept" /* PEER_REVIEWS_ACCEPT */,
|
|
1419
|
+
"peer_reviews_submit" /* PEER_REVIEWS_SUBMIT */,
|
|
1420
|
+
"user_teams_view" /* USER_TEAMS_VIEW */,
|
|
1421
|
+
"user_teams_nominate_peer" /* USER_TEAMS_NOMINATE_PEER */,
|
|
1422
|
+
"user_cycles_view" /* USER_CYCLES_VIEW */,
|
|
1423
|
+
"user_cycles_self_review_generate" /* USER_CYCLES_SELF_REVIEW_GENERATE */,
|
|
1424
|
+
"user_cycles_self_review_submit" /* USER_CYCLES_SELF_REVIEW_SUBMIT */
|
|
1425
|
+
];
|
|
1426
|
+
var ENABLED_UNIVERSAL = Object.fromEntries(
|
|
1427
|
+
UNIVERSAL_FEATURES.map((f) => [f, { enabled: true }])
|
|
1428
|
+
);
|
|
1429
|
+
var DISABLED_PLUS_GATED = Object.fromEntries(
|
|
1430
|
+
PLUS_GATED_FEATURES.map((f) => [f, { enabled: false }])
|
|
1431
|
+
);
|
|
1432
|
+
var ENABLED_PLUS_GATED = Object.fromEntries(
|
|
1433
|
+
PLUS_GATED_FEATURES.map((f) => [f, { enabled: true }])
|
|
1434
|
+
);
|
|
1435
|
+
var DISABLED_PRO_GATED = Object.fromEntries(
|
|
1436
|
+
PRO_GATED_FEATURES.map((f) => [f, { enabled: false }])
|
|
1437
|
+
);
|
|
1438
|
+
var ENABLED_PRO_GATED = Object.fromEntries(
|
|
1439
|
+
PRO_GATED_FEATURES.map((f) => [f, { enabled: true }])
|
|
1440
|
+
);
|
|
1441
|
+
var DISABLED_TEAM_GATED = Object.fromEntries(
|
|
1442
|
+
TEAM_GATED_FEATURES.map((f) => [f, { enabled: false }])
|
|
1443
|
+
);
|
|
1444
|
+
var PRO_TIER_TOKEN_BUDGET = 1e6;
|
|
1445
|
+
var TIER_FEATURES = {
|
|
1446
|
+
FREE: {
|
|
1447
|
+
tier: "FREE",
|
|
1448
|
+
features: {
|
|
1449
|
+
...DISABLED_ORG_TEAM_FEATURES,
|
|
1450
|
+
...ENABLED_UNIVERSAL,
|
|
1451
|
+
...DISABLED_PLUS_GATED,
|
|
1452
|
+
...DISABLED_PRO_GATED,
|
|
1453
|
+
...DISABLED_TEAM_GATED,
|
|
1454
|
+
["page_team_view" /* PAGE_TEAM_VIEW */]: { enabled: false },
|
|
1455
|
+
["basic_brags" /* BASIC_BRAGS */]: { enabled: true },
|
|
1456
|
+
["basic_organizations" /* BASIC_ORGANIZATIONS */]: { enabled: true },
|
|
1457
|
+
["ai_report_generation" /* AI_REPORT_GENERATION */]: {
|
|
1458
|
+
enabled: true,
|
|
1459
|
+
quota: 2,
|
|
1460
|
+
resetPeriod: "monthly"
|
|
1461
|
+
},
|
|
1462
|
+
["pdf_export" /* PDF_EXPORT */]: { enabled: true, quota: 3, resetPeriod: "monthly" },
|
|
1463
|
+
["share_links" /* SHARE_LINKS */]: { enabled: true, quota: 5 },
|
|
1464
|
+
// max 5 active
|
|
1465
|
+
["objectives" /* OBJECTIVES */]: { enabled: true, quota: 3 },
|
|
1466
|
+
// max 3 active
|
|
1467
|
+
["advanced_notifications" /* ADVANCED_NOTIFICATIONS */]: { enabled: true },
|
|
1468
|
+
["full_export_import" /* FULL_EXPORT_IMPORT */]: { enabled: true },
|
|
1469
|
+
["cli_turbo_mode" /* CLI_TURBO_MODE */]: { enabled: false },
|
|
1470
|
+
["priority_support" /* PRIORITY_SUPPORT */]: { enabled: false },
|
|
1471
|
+
["early_access" /* EARLY_ACCESS */]: { enabled: false },
|
|
1472
|
+
["smart_goal_suggestions" /* SMART_GOAL_SUGGESTIONS */]: { enabled: false },
|
|
1473
|
+
["ai_powered_suggestions" /* AI_POWERED_SUGGESTIONS */]: { enabled: false },
|
|
1474
|
+
["ai_token_budget" /* AI_TOKEN_BUDGET */]: { enabled: false },
|
|
1475
|
+
["generate_objectives" /* GENERATE_OBJECTIVES */]: { enabled: false },
|
|
1476
|
+
["multi_email" /* MULTI_EMAIL */]: { enabled: true }
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
PLUS: {
|
|
1480
|
+
tier: "PLUS",
|
|
1481
|
+
features: {
|
|
1482
|
+
...DISABLED_ORG_TEAM_FEATURES,
|
|
1483
|
+
...ENABLED_UNIVERSAL,
|
|
1484
|
+
...ENABLED_PLUS_GATED,
|
|
1485
|
+
...DISABLED_PRO_GATED,
|
|
1486
|
+
...DISABLED_TEAM_GATED,
|
|
1487
|
+
["page_team_view" /* PAGE_TEAM_VIEW */]: { enabled: false },
|
|
1488
|
+
["basic_brags" /* BASIC_BRAGS */]: { enabled: true },
|
|
1489
|
+
["basic_organizations" /* BASIC_ORGANIZATIONS */]: { enabled: true },
|
|
1490
|
+
["ai_report_generation" /* AI_REPORT_GENERATION */]: { enabled: true, quota: 30, resetPeriod: "monthly" },
|
|
1491
|
+
["pdf_export" /* PDF_EXPORT */]: {
|
|
1492
|
+
enabled: true,
|
|
1493
|
+
quota: 15,
|
|
1494
|
+
resetPeriod: "monthly"
|
|
1495
|
+
},
|
|
1496
|
+
["share_links" /* SHARE_LINKS */]: { enabled: true, quota: 20 },
|
|
1497
|
+
// max 20 active
|
|
1498
|
+
["objectives" /* OBJECTIVES */]: { enabled: true, quota: 4 },
|
|
1499
|
+
// max 4 active
|
|
1500
|
+
["advanced_notifications" /* ADVANCED_NOTIFICATIONS */]: { enabled: true },
|
|
1501
|
+
["full_export_import" /* FULL_EXPORT_IMPORT */]: { enabled: true },
|
|
1502
|
+
["cli_turbo_mode" /* CLI_TURBO_MODE */]: { enabled: false },
|
|
1503
|
+
["priority_support" /* PRIORITY_SUPPORT */]: { enabled: true },
|
|
1504
|
+
["early_access" /* EARLY_ACCESS */]: { enabled: false },
|
|
1505
|
+
["smart_goal_suggestions" /* SMART_GOAL_SUGGESTIONS */]: { enabled: true },
|
|
1506
|
+
["ai_powered_suggestions" /* AI_POWERED_SUGGESTIONS */]: { enabled: true, quota: 30, resetPeriod: "monthly" },
|
|
1507
|
+
["ai_token_budget" /* AI_TOKEN_BUDGET */]: { enabled: false },
|
|
1508
|
+
["generate_objectives" /* GENERATE_OBJECTIVES */]: { enabled: true },
|
|
1509
|
+
["multi_email" /* MULTI_EMAIL */]: { enabled: true }
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
PRO: {
|
|
1513
|
+
tier: "PRO",
|
|
1514
|
+
features: {
|
|
1515
|
+
...DISABLED_ORG_TEAM_FEATURES,
|
|
1516
|
+
...ENABLED_UNIVERSAL,
|
|
1517
|
+
...ENABLED_PLUS_GATED,
|
|
1518
|
+
...ENABLED_PRO_GATED,
|
|
1519
|
+
...DISABLED_TEAM_GATED,
|
|
1520
|
+
["page_team_view" /* PAGE_TEAM_VIEW */]: { enabled: false },
|
|
1521
|
+
["basic_brags" /* BASIC_BRAGS */]: { enabled: true },
|
|
1522
|
+
["basic_organizations" /* BASIC_ORGANIZATIONS */]: { enabled: true },
|
|
1523
|
+
["ai_report_generation" /* AI_REPORT_GENERATION */]: {
|
|
1524
|
+
enabled: true,
|
|
1525
|
+
tokenQuota: PRO_TIER_TOKEN_BUDGET,
|
|
1526
|
+
resetPeriod: "monthly"
|
|
1527
|
+
},
|
|
1528
|
+
["pdf_export" /* PDF_EXPORT */]: { enabled: true },
|
|
1529
|
+
// unlimited
|
|
1530
|
+
["share_links" /* SHARE_LINKS */]: { enabled: true },
|
|
1531
|
+
// unlimited
|
|
1532
|
+
["objectives" /* OBJECTIVES */]: { enabled: true },
|
|
1533
|
+
// unlimited
|
|
1534
|
+
["advanced_notifications" /* ADVANCED_NOTIFICATIONS */]: { enabled: true },
|
|
1535
|
+
["full_export_import" /* FULL_EXPORT_IMPORT */]: { enabled: true },
|
|
1536
|
+
["cli_turbo_mode" /* CLI_TURBO_MODE */]: { enabled: true },
|
|
1537
|
+
["priority_support" /* PRIORITY_SUPPORT */]: { enabled: true },
|
|
1538
|
+
["early_access" /* EARLY_ACCESS */]: { enabled: true },
|
|
1539
|
+
["smart_goal_suggestions" /* SMART_GOAL_SUGGESTIONS */]: { enabled: true },
|
|
1540
|
+
["ai_powered_suggestions" /* AI_POWERED_SUGGESTIONS */]: { enabled: true },
|
|
1541
|
+
["ai_token_budget" /* AI_TOKEN_BUDGET */]: {
|
|
1542
|
+
enabled: true,
|
|
1543
|
+
tokenQuota: PRO_TIER_TOKEN_BUDGET,
|
|
1544
|
+
resetPeriod: "monthly"
|
|
1545
|
+
},
|
|
1546
|
+
["generate_objectives" /* GENERATE_OBJECTIVES */]: { enabled: true },
|
|
1547
|
+
["multi_email" /* MULTI_EMAIL */]: { enabled: true }
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
// ../server/src/utils/access-control/roles.ts
|
|
1553
|
+
function deriveEnabledFeatures(tier) {
|
|
1554
|
+
const config = TIER_FEATURES[tier];
|
|
1555
|
+
if (!config) return [];
|
|
1556
|
+
return Object.entries(config.features).filter(([, limit]) => limit.enabled).map(([f]) => f);
|
|
1557
|
+
}
|
|
1558
|
+
var PLAN_ROLE_FEATURES = {
|
|
1559
|
+
"plan:FREE": deriveEnabledFeatures("FREE"),
|
|
1560
|
+
"plan:PLUS": deriveEnabledFeatures("PLUS"),
|
|
1561
|
+
"plan:PRO": deriveEnabledFeatures("PRO")
|
|
1562
|
+
};
|
|
1563
|
+
var ORG_ADMIN_SHARED_FEATURES = [
|
|
1564
|
+
"org_access" /* ORG_ACCESS */,
|
|
1565
|
+
"org_mutate_base" /* ORG_MUTATE_BASE */,
|
|
1566
|
+
"org_admin_access" /* ORG_ADMIN_ACCESS */,
|
|
1567
|
+
"org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */,
|
|
1568
|
+
"manager_tier_access" /* MANAGER_TIER_ACCESS */,
|
|
1569
|
+
"org_members_manage" /* ORG_MEMBERS_MANAGE */,
|
|
1570
|
+
"org_invites_manage" /* ORG_INVITES_MANAGE */,
|
|
1571
|
+
"org_teams_manage" /* ORG_TEAMS_MANAGE */,
|
|
1572
|
+
"org_growth_view" /* ORG_GROWTH_VIEW */,
|
|
1573
|
+
"org_career_metrics_manage" /* ORG_CAREER_METRICS_MANAGE */,
|
|
1574
|
+
"org_cycles_manage" /* ORG_CYCLES_MANAGE */,
|
|
1575
|
+
"org_stats_view" /* ORG_STATS_VIEW */,
|
|
1576
|
+
"org_audit_log_view" /* ORG_AUDIT_LOG_VIEW */,
|
|
1577
|
+
"org_billing_manage" /* ORG_BILLING_MANAGE */,
|
|
1578
|
+
"org_notifications_manage" /* ORG_NOTIFICATIONS_MANAGE */
|
|
1579
|
+
];
|
|
1580
|
+
var ORG_ROLE_FEATURES = {
|
|
1581
|
+
"org:org_owner": [...ORG_ADMIN_SHARED_FEATURES, "org_owner_access" /* ORG_OWNER_ACCESS */, "page_team_view" /* PAGE_TEAM_VIEW */, ...TEAM_GATED_FEATURES],
|
|
1582
|
+
"org:org_admin": [...ORG_ADMIN_SHARED_FEATURES, "page_team_view" /* PAGE_TEAM_VIEW */, ...TEAM_GATED_FEATURES],
|
|
1583
|
+
// growth_admin: matches canManageGrowth (owner/admin/growth_admin) but NOT
|
|
1584
|
+
// isOrgAdminRole (owner/admin only) — no ORG_ADMIN_ACCESS or its dependents
|
|
1585
|
+
// (members/invites/teams/stats/audit-log/billing/notifications manage).
|
|
1586
|
+
"org:growth_admin": [
|
|
1587
|
+
"org_access" /* ORG_ACCESS */,
|
|
1588
|
+
"org_mutate_base" /* ORG_MUTATE_BASE */,
|
|
1589
|
+
"org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */,
|
|
1590
|
+
"manager_tier_access" /* MANAGER_TIER_ACCESS */,
|
|
1591
|
+
"org_growth_view" /* ORG_GROWTH_VIEW */,
|
|
1592
|
+
"org_career_metrics_manage" /* ORG_CAREER_METRICS_MANAGE */,
|
|
1593
|
+
"org_cycles_manage" /* ORG_CYCLES_MANAGE */,
|
|
1594
|
+
"page_team_view" /* PAGE_TEAM_VIEW */,
|
|
1595
|
+
...TEAM_GATED_FEATURES
|
|
1596
|
+
],
|
|
1597
|
+
"org:org_member": ["org_access" /* ORG_ACCESS */, "org_mutate_base" /* ORG_MUTATE_BASE */, "page_team_view" /* PAGE_TEAM_VIEW */, ...TEAM_GATED_FEATURES],
|
|
1598
|
+
// org_viewer: read-only — gets view/read subset of team features only.
|
|
1599
|
+
"org:org_viewer": [
|
|
1600
|
+
"org_access" /* ORG_ACCESS */,
|
|
1601
|
+
"page_team_view" /* PAGE_TEAM_VIEW */,
|
|
1602
|
+
"evaluation_comments_read" /* EVALUATION_COMMENTS_READ */,
|
|
1603
|
+
"peer_reviews_view" /* PEER_REVIEWS_VIEW */,
|
|
1604
|
+
"user_teams_view" /* USER_TEAMS_VIEW */,
|
|
1605
|
+
"user_cycles_view" /* USER_CYCLES_VIEW */
|
|
1606
|
+
]
|
|
1607
|
+
};
|
|
1608
|
+
var TEAM_ROLE_FEATURES = {
|
|
1609
|
+
"team:owner": [
|
|
1610
|
+
"team_manage" /* TEAM_MANAGE */,
|
|
1611
|
+
"team_owner_actions" /* TEAM_OWNER_ACTIONS */,
|
|
1612
|
+
"team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
|
|
1613
|
+
"manager_tier_access" /* MANAGER_TIER_ACCESS */,
|
|
1614
|
+
"page_team_view" /* PAGE_TEAM_VIEW */,
|
|
1615
|
+
...TEAM_GATED_FEATURES
|
|
1616
|
+
],
|
|
1617
|
+
"team:co_manager": [
|
|
1618
|
+
"team_manage" /* TEAM_MANAGE */,
|
|
1619
|
+
"team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
|
|
1620
|
+
"manager_tier_access" /* MANAGER_TIER_ACCESS */,
|
|
1621
|
+
"page_team_view" /* PAGE_TEAM_VIEW */,
|
|
1622
|
+
...TEAM_GATED_FEATURES
|
|
1623
|
+
],
|
|
1624
|
+
"team:member": [
|
|
1625
|
+
"team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
|
|
1626
|
+
"page_team_view" /* PAGE_TEAM_VIEW */,
|
|
1627
|
+
...TEAM_GATED_FEATURES
|
|
1628
|
+
]
|
|
1629
|
+
};
|
|
1630
|
+
var ROLE_FEATURES = {
|
|
1631
|
+
...PLAN_ROLE_FEATURES,
|
|
1632
|
+
...ORG_ROLE_FEATURES,
|
|
1633
|
+
...TEAM_ROLE_FEATURES
|
|
1634
|
+
};
|
|
1635
|
+
var ORG_GROWTH_CAPABLE_ROLE_IDS = Object.keys(ORG_ROLE_FEATURES).filter((id) => ROLE_FEATURES[id].includes("org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */));
|
|
1636
|
+
var ORG_GROWTH_CAPABLE_ROLES = ORG_GROWTH_CAPABLE_ROLE_IDS.map(
|
|
1637
|
+
(id) => id.replace("org:", "")
|
|
1638
|
+
);
|
|
1639
|
+
|
|
1640
|
+
// ../server/src/utils/badges/badge.constants.ts
|
|
1641
|
+
var BadgeKeys = {
|
|
1642
|
+
// Milestone - Brags
|
|
1643
|
+
FIRST_BRAG: "first_brag",
|
|
1644
|
+
BRAGS_10: "10_brags",
|
|
1645
|
+
BRAGS_25: "25_brags",
|
|
1646
|
+
BRAGS_100: "100_brags",
|
|
1647
|
+
BRAGS_500: "500_brags",
|
|
1648
|
+
// Milestone - Features
|
|
1649
|
+
FIRST_OBJECTIVE: "first_objective",
|
|
1650
|
+
FIRST_COMPANY: "first_company",
|
|
1651
|
+
FIRST_REPORT: "first_report",
|
|
1652
|
+
// Streaks
|
|
1653
|
+
STREAK_7_DAY: "7_day_streak",
|
|
1654
|
+
STREAK_30_DAY: "30_day_streak",
|
|
1655
|
+
STREAK_90_DAY: "90_day_streak",
|
|
1656
|
+
// Goal Completion
|
|
1657
|
+
FIRST_OBJECTIVE_COMPLETED: "first_objective_completed",
|
|
1658
|
+
OBJECTIVES_COMPLETED_5: "5_objectives_completed",
|
|
1659
|
+
OBJECTIVES_COMPLETED_25: "25_objectives_completed"
|
|
1660
|
+
};
|
|
1661
|
+
var BadgeCategories = {
|
|
1662
|
+
MILESTONE: "milestone",
|
|
1663
|
+
STREAK: "streak",
|
|
1664
|
+
GOAL: "goal"
|
|
1665
|
+
};
|
|
1666
|
+
var BADGE_DEFINITIONS = [
|
|
1667
|
+
// Milestone - Brags
|
|
1668
|
+
{ key: BadgeKeys.FIRST_BRAG, category: BadgeCategories.MILESTONE, threshold: 1 },
|
|
1669
|
+
{ key: BadgeKeys.BRAGS_10, category: BadgeCategories.MILESTONE, threshold: 10 },
|
|
1670
|
+
{ key: BadgeKeys.BRAGS_25, category: BadgeCategories.MILESTONE, threshold: 25 },
|
|
1671
|
+
{ key: BadgeKeys.BRAGS_100, category: BadgeCategories.MILESTONE, threshold: 100 },
|
|
1672
|
+
{ key: BadgeKeys.BRAGS_500, category: BadgeCategories.MILESTONE, threshold: 500 },
|
|
1673
|
+
// Milestone - Features
|
|
1674
|
+
{ key: BadgeKeys.FIRST_OBJECTIVE, category: BadgeCategories.MILESTONE, threshold: 1 },
|
|
1675
|
+
{ key: BadgeKeys.FIRST_COMPANY, category: BadgeCategories.MILESTONE, threshold: 1 },
|
|
1676
|
+
{ key: BadgeKeys.FIRST_REPORT, category: BadgeCategories.MILESTONE, threshold: 1 },
|
|
1677
|
+
// Streaks
|
|
1678
|
+
{ key: BadgeKeys.STREAK_7_DAY, category: BadgeCategories.STREAK, threshold: 7 },
|
|
1679
|
+
{ key: BadgeKeys.STREAK_30_DAY, category: BadgeCategories.STREAK, threshold: 30 },
|
|
1680
|
+
{ key: BadgeKeys.STREAK_90_DAY, category: BadgeCategories.STREAK, threshold: 90 },
|
|
1681
|
+
// Goal Completion
|
|
1682
|
+
{
|
|
1683
|
+
key: BadgeKeys.FIRST_OBJECTIVE_COMPLETED,
|
|
1684
|
+
category: BadgeCategories.GOAL,
|
|
1685
|
+
threshold: 1
|
|
1686
|
+
},
|
|
1687
|
+
{ key: BadgeKeys.OBJECTIVES_COMPLETED_5, category: BadgeCategories.GOAL, threshold: 5 },
|
|
1688
|
+
{ key: BadgeKeys.OBJECTIVES_COMPLETED_25, category: BadgeCategories.GOAL, threshold: 25 }
|
|
1689
|
+
];
|
|
1690
|
+
var BRAG_MILESTONE_BADGES = [
|
|
1691
|
+
BadgeKeys.FIRST_BRAG,
|
|
1692
|
+
BadgeKeys.BRAGS_10,
|
|
1693
|
+
BadgeKeys.BRAGS_25,
|
|
1694
|
+
BadgeKeys.BRAGS_100,
|
|
1695
|
+
BadgeKeys.BRAGS_500
|
|
1696
|
+
];
|
|
1697
|
+
var STREAK_BADGES = [
|
|
1698
|
+
BadgeKeys.STREAK_7_DAY,
|
|
1699
|
+
BadgeKeys.STREAK_30_DAY,
|
|
1700
|
+
BadgeKeys.STREAK_90_DAY
|
|
1701
|
+
];
|
|
1702
|
+
var OBJECTIVE_COMPLETED_BADGES = [
|
|
1703
|
+
BadgeKeys.FIRST_OBJECTIVE_COMPLETED,
|
|
1704
|
+
BadgeKeys.OBJECTIVES_COMPLETED_5,
|
|
1705
|
+
BadgeKeys.OBJECTIVES_COMPLETED_25
|
|
1706
|
+
];
|
|
1707
|
+
|
|
1708
|
+
// ../../packages/types/domain/story-categories.ts
|
|
1709
|
+
var STORY_CATEGORY_LABELS = {
|
|
1710
|
+
leadership: "Leadership",
|
|
1711
|
+
technical_excellence: "Technical Excellence",
|
|
1712
|
+
problem_solving: "Problem Solving",
|
|
1713
|
+
innovation: "Innovation",
|
|
1714
|
+
teamwork: "Teamwork",
|
|
1715
|
+
communication: "Communication",
|
|
1716
|
+
project_delivery: "Project Delivery",
|
|
1717
|
+
customer_impact: "Customer Impact",
|
|
1718
|
+
mentorship: "Mentorship",
|
|
1719
|
+
process_improvement: "Process Improvement"
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// src/tools/story-tool-shared.ts
|
|
1723
|
+
var NOT_AUTHENTICATED = {
|
|
1724
|
+
content: [{ type: "text", text: 'Not authenticated with BragDuck. Please call the "login" tool first.' }],
|
|
1725
|
+
isError: true
|
|
1726
|
+
};
|
|
1727
|
+
var toolError = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
1728
|
+
async function runForUser(failure, run) {
|
|
1729
|
+
const planError = await requirePaidPlan();
|
|
1730
|
+
if (planError) return planError;
|
|
1731
|
+
try {
|
|
1732
|
+
const userInfo = getUserInfo();
|
|
1733
|
+
if (!userInfo) return NOT_AUTHENTICATED;
|
|
1734
|
+
return await run(userInfo.id);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
1737
|
+
return NOT_AUTHENTICATED;
|
|
1738
|
+
}
|
|
1739
|
+
return toolError(`${failure}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// src/tools/get-story.ts
|
|
1744
|
+
var getStoryTool = {
|
|
1745
|
+
name: "get_story",
|
|
1746
|
+
description: "Get one STAR interview story from BragDuck in full: Situation, Task, Action, Result, competencies, category, Strength/Impact scores (1-10) and the AI summary of how to improve it. Call this before update_story so the improvement builds on the current text. Story IDs come from list_stories.",
|
|
1747
|
+
inputSchema: {
|
|
1748
|
+
type: "object",
|
|
1749
|
+
properties: {
|
|
1750
|
+
story_id: { type: "string", description: "ID of the story (from list_stories)" }
|
|
1751
|
+
},
|
|
1752
|
+
required: ["story_id"]
|
|
1753
|
+
},
|
|
1754
|
+
annotations: { title: "Get story", readOnlyHint: true }
|
|
1755
|
+
};
|
|
1756
|
+
function formatStory(story) {
|
|
1757
|
+
const category = story.category ? STORY_CATEGORY_LABELS[story.category] : null;
|
|
1758
|
+
return [
|
|
1759
|
+
`**${story.title}** (ID: ${story.id})`,
|
|
1760
|
+
`${category ? `Category: ${category} | ` : ""}Strength: ${story.strengthScore ?? "-"}/10 | Impact: ${story.impactScore ?? "-"}/10`,
|
|
1761
|
+
`How to improve: ${story.aiSummary || "Not rated yet"}`,
|
|
1762
|
+
`Competencies: ${story.competencies.length > 0 ? story.competencies.join(", ") : "None"}`,
|
|
1763
|
+
"",
|
|
1764
|
+
`Situation: ${story.situation}`,
|
|
1765
|
+
`Task: ${story.task}`,
|
|
1766
|
+
`Action: ${story.action}`,
|
|
1767
|
+
`Result: ${story.result}`
|
|
1768
|
+
].join("\n");
|
|
1769
|
+
}
|
|
1770
|
+
function handleGetStory(input) {
|
|
1771
|
+
return runForUser("Failed to get story", async (userId) => {
|
|
1772
|
+
const story = await apiRequest(`/v1/users/${userId}/stories/${input.story_id}`);
|
|
1773
|
+
return { content: [{ type: "text", text: formatStory(story) }] };
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/tools/update-story.ts
|
|
1778
|
+
var updateStoryTool = {
|
|
1779
|
+
name: "update_story",
|
|
1780
|
+
description: "Update a STAR interview story in BragDuck, for example to make it stronger: a sharper Situation, concrete first-person Actions, and a Result with real, measurable impact. Read the story with get_story first and only send the fields you change; the rest are kept. Never invent metrics or facts \u2014 ask the user for missing numbers. Show the changes as a markdown table (Field | Current | Proposed) and call this in the same response \u2014 the tool permission prompt (Allow/Deny) IS the confirmation. Changing the title or STAR text re-scores the story shortly after.",
|
|
1781
|
+
inputSchema: {
|
|
1782
|
+
type: "object",
|
|
1783
|
+
properties: {
|
|
1784
|
+
story_id: { type: "string", description: "ID of the story (from list_stories)" },
|
|
1785
|
+
title: { type: "string", minLength: 5, maxLength: 200, description: "New title" },
|
|
1786
|
+
situation: { type: "string", minLength: 10, description: "New Situation: context and background" },
|
|
1787
|
+
task: { type: "string", minLength: 10, description: "New Task: your responsibility or challenge" },
|
|
1788
|
+
action: { type: "string", minLength: 10, description: "New Action: what you specifically did" },
|
|
1789
|
+
result: { type: "string", minLength: 10, description: "New Result: the outcome and measurable impact" },
|
|
1790
|
+
competencies: {
|
|
1791
|
+
type: "array",
|
|
1792
|
+
items: { type: "string", minLength: 2, maxLength: 50 },
|
|
1793
|
+
maxItems: 20,
|
|
1794
|
+
description: "Replaces the competencies the story demonstrates"
|
|
1795
|
+
},
|
|
1796
|
+
bragIds: {
|
|
1797
|
+
type: "array",
|
|
1798
|
+
items: { type: "string" },
|
|
1799
|
+
maxItems: 20,
|
|
1800
|
+
description: "Replaces the IDs of the brags that back the story"
|
|
1801
|
+
},
|
|
1802
|
+
category: {
|
|
1803
|
+
type: ["string", "null"],
|
|
1804
|
+
description: "Interview category (e.g. leadership, technical_excellence, problem_solving, innovation, teamwork, communication, project_delivery, customer_impact, mentorship, process_improvement), or null to clear it"
|
|
1805
|
+
},
|
|
1806
|
+
orgId: {
|
|
1807
|
+
type: ["string", "null"],
|
|
1808
|
+
description: "Organisation ID to move the story to, or null to remove it from any organisation"
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
required: ["story_id"]
|
|
1812
|
+
},
|
|
1813
|
+
annotations: { title: "Update story", destructiveHint: false, idempotentHint: true }
|
|
1814
|
+
};
|
|
1815
|
+
var UPDATABLE_FIELDS = [
|
|
1816
|
+
"title",
|
|
1817
|
+
"situation",
|
|
1818
|
+
"task",
|
|
1819
|
+
"action",
|
|
1820
|
+
"result",
|
|
1821
|
+
"competencies",
|
|
1822
|
+
"bragIds",
|
|
1823
|
+
"category",
|
|
1824
|
+
"orgId"
|
|
1825
|
+
];
|
|
1826
|
+
var RATED_FIELDS = /* @__PURE__ */ new Set(["title", "situation", "task", "action", "result"]);
|
|
1827
|
+
function handleUpdateStory(input) {
|
|
1828
|
+
const body = Object.fromEntries(
|
|
1829
|
+
UPDATABLE_FIELDS.filter((field) => input[field] !== void 0).map((field) => [field, input[field]])
|
|
1830
|
+
);
|
|
1831
|
+
if (Object.keys(body).length === 0) {
|
|
1832
|
+
return Promise.resolve(toolError("Nothing to update: pass at least one field to change."));
|
|
1833
|
+
}
|
|
1834
|
+
return runForUser("Failed to update story", async (userId) => {
|
|
1835
|
+
const story = await apiRequest(`/v1/users/${userId}/stories/${input.story_id}`, {
|
|
1836
|
+
method: "PATCH",
|
|
1837
|
+
body
|
|
1838
|
+
});
|
|
1839
|
+
const rescored = Object.keys(body).some((field) => RATED_FIELDS.has(field)) ? "\nThe story is being re-scored; call get_story in a moment to see the new scores." : "";
|
|
1840
|
+
return { content: [{ type: "text", text: `Story updated: "${story.title}"${rescored}` }] };
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/tools/delete-story.ts
|
|
1845
|
+
var deleteStoryTool = {
|
|
1846
|
+
name: "delete_story",
|
|
1847
|
+
description: "Delete a STAR interview story from BragDuck. It cannot be restored from the agent. Before calling, name the story (title) and ask the user for an explicit yes in plain text; never delete as part of improving a story \u2014 use update_story for that.",
|
|
1848
|
+
inputSchema: {
|
|
1849
|
+
type: "object",
|
|
1850
|
+
properties: {
|
|
1851
|
+
story_id: { type: "string", description: "ID of the story to delete (from list_stories)" }
|
|
1852
|
+
},
|
|
1853
|
+
required: ["story_id"]
|
|
1854
|
+
},
|
|
1855
|
+
annotations: { title: "Delete story", destructiveHint: true, idempotentHint: true }
|
|
1856
|
+
};
|
|
1857
|
+
function handleDeleteStory(input) {
|
|
1858
|
+
return runForUser("Failed to delete story", async (userId) => {
|
|
1859
|
+
await apiRequest(`/v1/users/${userId}/stories/${input.story_id}`, { method: "DELETE" });
|
|
1860
|
+
return { content: [{ type: "text", text: "Story deleted." }] };
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/tools/list-stories.ts
|
|
1865
|
+
var listStoriesTool = {
|
|
1866
|
+
name: "list_stories",
|
|
1867
|
+
description: "List STAR interview stories from BragDuck. Use when the user wants to review their interview stories or search for stories by category.",
|
|
1868
|
+
inputSchema: {
|
|
1869
|
+
type: "object",
|
|
1870
|
+
properties: {
|
|
1871
|
+
limit: {
|
|
1872
|
+
type: "number",
|
|
1873
|
+
minimum: 1,
|
|
1874
|
+
maximum: 100,
|
|
1875
|
+
description: "Max stories to return (default 10)"
|
|
1876
|
+
},
|
|
1877
|
+
category: {
|
|
1878
|
+
type: "string",
|
|
1879
|
+
description: "Filter by category (e.g. leadership, technical_excellence, problem_solving)"
|
|
1880
|
+
},
|
|
1881
|
+
orgId: {
|
|
1882
|
+
type: "string",
|
|
1883
|
+
description: "Filter by organisation ID"
|
|
1884
|
+
},
|
|
1885
|
+
search: {
|
|
1886
|
+
type: "string",
|
|
1887
|
+
description: "Search stories by keyword"
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
async function handleListStories(input) {
|
|
1893
|
+
const planError = await requirePaidPlan();
|
|
1894
|
+
if (planError) return planError;
|
|
1895
|
+
try {
|
|
1896
|
+
const userInfo = getUserInfo();
|
|
1897
|
+
if (!userInfo) {
|
|
1898
|
+
return {
|
|
1899
|
+
content: [{
|
|
1900
|
+
type: "text",
|
|
1901
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1902
|
+
}],
|
|
1903
|
+
isError: true
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
const params = {
|
|
1907
|
+
page: "1",
|
|
1908
|
+
perPage: String(input.limit || 10)
|
|
1909
|
+
};
|
|
1910
|
+
if (input.category) params.category = input.category;
|
|
1911
|
+
if (input.orgId) params.orgId = input.orgId;
|
|
1912
|
+
if (input.search) params.search = input.search;
|
|
1913
|
+
const response = await apiRequest(
|
|
1914
|
+
`/v1/users/${userInfo.id}/stories`,
|
|
1915
|
+
{ params }
|
|
1916
|
+
);
|
|
1917
|
+
if (response.items.length === 0) {
|
|
1918
|
+
return {
|
|
1919
|
+
content: [{ type: "text", text: "No stories found." }]
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
const lines = response.items.map((story) => {
|
|
1923
|
+
const categoryLabel = story.category ? STORY_CATEGORY_LABELS[story.category] : null;
|
|
1924
|
+
const categoryPart = categoryLabel ? `Category: ${categoryLabel} | ` : "";
|
|
1925
|
+
const strengthPart = `Strength: ${story.strengthScore ?? "-"}/10`;
|
|
1926
|
+
const impactPart = `Impact: ${story.impactScore ?? "-"}/10`;
|
|
1927
|
+
const summary2 = story.aiSummary || "No AI summary yet";
|
|
1928
|
+
const competencies = story.competencies.length > 0 ? story.competencies.join(", ") : "None";
|
|
1929
|
+
return `**${story.title}** (ID: ${story.id})
|
|
1930
|
+
${categoryPart}${strengthPart} | ${impactPart}
|
|
1931
|
+
${summary2}
|
|
1932
|
+
Competencies: ${competencies}`;
|
|
1933
|
+
});
|
|
1934
|
+
const summary = `Found ${response.total} stories (showing ${response.items.length}):`;
|
|
1935
|
+
return {
|
|
1936
|
+
content: [{ type: "text", text: `${summary}
|
|
1937
|
+
|
|
1938
|
+
${lines.join("\n\n")}` }]
|
|
1939
|
+
};
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
|
|
1942
|
+
return {
|
|
1943
|
+
content: [{
|
|
1944
|
+
type: "text",
|
|
1945
|
+
text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
|
|
1946
|
+
}],
|
|
1947
|
+
isError: true
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
content: [{
|
|
1952
|
+
type: "text",
|
|
1953
|
+
text: `Failed to list stories: ${error instanceof Error ? error.message : String(error)}`
|
|
1954
|
+
}],
|
|
1955
|
+
isError: true
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/index.ts
|
|
1961
|
+
var server = new Server(
|
|
1962
|
+
{
|
|
1963
|
+
name: "bragduck",
|
|
1964
|
+
version: "1.0.0"
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
capabilities: {
|
|
1968
|
+
tools: {}
|
|
1969
|
+
},
|
|
1970
|
+
instructions: 'BragDuck tracks career achievements ("brags") that feed into periodic performance reports. A strong brag needs: a concrete accomplishment, measurable or observable impact, and ideally a reference link. \n\nRESOLVING "THIS" IN "BRAG WITH THIS"\nWhen the user says "brag with this" (or "brag about this", "log this", etc.) without elaborating, "this" refers to the most recent work visible in the current session: the latest git diff, the last commit message, the most recently discussed code change, or a PR just mentioned. Look for that context first before asking anything. Only start the interview (see below) if no such context exists in the session.\n\nSTEP 1 \u2014 ASSESS BRAGABILITY\nBefore doing anything else, judge whether the subject is worth logging:\n- NOT bragable: vague status updates, trivial tasks with no impact, or things a reasonable person would not list on a performance review. Tell the user directly that it is not bragable and explain why in one sentence. Note that brags feed the AI report generator \u2014 weak entries dilute the signal.\n- POTENTIALLY bragable but thin: start the interview (see below).\n- Clearly bragable with full context: skip the interview, go straight to STEP 2.\n\nTHE INTERVIEW\nConduct a short conversational interview to gather what you need. Ask ONE question at a time \u2014 do not list multiple questions at once. Each question should follow naturally from the previous answer, like a real conversation. Suggested question order (adapt based on what you already know):\n 1. "What did you work on?" (skip if already known)\n 2. "What was the outcome or result?"\n 3. "Who was affected \u2014 just you, the team, or wider?"\n 4. "Do you have a link \u2014 PR, ticket, commit, or doc?" (skip if already have one)\nStop the interview as soon as you have enough to write a strong brag. Never ask more than 4 questions total.\n\nSTEP 2 \u2014 SELECT ORGANISATION\nBefore composing the brag, determine which company it belongs to:\n1. Call list_organisations. It returns the org list AND any saved preference for the current directory.\n2. If a saved preference exists, use that orgId silently \u2014 do not ask the user.\n3. If no saved preference, present the org list to the user and ask: "Which company does this belong to?" Wait for their answer, then call save_org_preference with the chosen orgId and orgName.\n4. If the user has only one organisation, select it automatically and save the preference.\n5. If the user has no organisations, skip this step and proceed without an orgId.\n\nSTEP 3 \u2014 PREVIEW + LOG\nCompose the brag from all gathered context. Do NOT call preview_brag. Instead, in your response text render the brag fields as a markdown table with two columns (Field | Value), one row per field, in this order: Title, Type, Impact Level, Impact Details, Description, Tags, References, Organisation. In the very same response, after the table, call log_brag with those field values (including orgId). The native tool-permission prompt (Allow / Deny) that appears below the table IS the confirmation \u2014 do not add any text question. If the user denies or asks for changes, adjust and repeat this step.\n\nSTEP 4 \u2014 DONE\nOnce log_brag is allowed and succeeds, confirm with one short sentence.\n\nQUICK BRAG (via /brag or raw text)\nWhen the user says "/brag" followed by text, or pastes a long block of text and asks to log it, use the quick_brag tool instead of the interview flow. quick_brag accepts raw text and uses AI to extract and structure it into a proper brag automatically. Skip the interview, skip the preview table \u2014 just call quick_brag with the text.\n\nINTERVIEW STORIES (log_story)\nWhen the user discusses interview prep, behavioral questions, or asks to save a STAR story, use log_story instead of log_brag. Stories are structured with Situation/Task/Action/Result fields and competency tags. Follow the same org-selection flow (list_organisations \u2192 saved preference or ask). Preview the story as a markdown table (Field | Value) before calling log_story in the same response.\nUse list_stories to browse existing stories. Supports filters: category, orgId, search, limit. Stories include AI-generated strength and impact scores.\n\nIMPROVING STORIES (get_story, update_story, delete_story)\nTo make a story stronger: find it with list_stories, read it in full with get_story (its AI summary says what is weak), then propose a stronger version \u2014 a clear Situation, first-person Actions, and a Result with measurable impact. Never invent numbers or facts; ask the user for anything missing. Show the changes as a markdown table (Field | Current | Proposed) and call update_story in the same response with only the changed fields; the permission prompt IS the confirmation. Only call delete_story after the user explicitly says yes to deleting that named story.\n\nGENERAL RULES\n- Never fabricate details, metrics, or links. If unsure, ask.\n- "Is this bragable?" deserves a direct honest answer \u2014 not a tool call.\n- Call login first if any tool returns an authentication error.'
|
|
1971
|
+
}
|
|
1972
|
+
);
|
|
1973
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1974
|
+
return {
|
|
1975
|
+
tools: [loginTool, logBragTool, quickBragTool, listBragsTool, listObjectivesTool, linkBragToKeyResultTool, listOrganisationsTool, saveOrgPreferenceTool, logStoryTool, listStoriesTool, getStoryTool, updateStoryTool, deleteStoryTool]
|
|
1976
|
+
};
|
|
99
1977
|
});
|
|
100
1978
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1979
|
+
const { name, arguments: args } = request.params;
|
|
1980
|
+
switch (name) {
|
|
1981
|
+
case "login":
|
|
1982
|
+
return handleLogin(args);
|
|
1983
|
+
case "preview_brag":
|
|
1984
|
+
return handlePreviewBrag(args);
|
|
1985
|
+
case "log_brag":
|
|
1986
|
+
return handleLogBrag(args);
|
|
1987
|
+
case "quick_brag":
|
|
1988
|
+
return handleQuickBrag(args);
|
|
1989
|
+
case "list_brags":
|
|
1990
|
+
return handleListBrags(args);
|
|
1991
|
+
case "list_objectives":
|
|
1992
|
+
return handleListObjectives();
|
|
1993
|
+
case "link_brag_to_key_result":
|
|
1994
|
+
return handleLinkBragToKeyResult(args);
|
|
1995
|
+
case "list_organisations":
|
|
1996
|
+
return handleListOrganisations();
|
|
1997
|
+
case "save_org_preference":
|
|
1998
|
+
return handleSaveOrgPreference(args);
|
|
1999
|
+
case "log_story":
|
|
2000
|
+
return handleLogStory(args);
|
|
2001
|
+
case "list_stories":
|
|
2002
|
+
return handleListStories(args);
|
|
2003
|
+
case "get_story":
|
|
2004
|
+
return handleGetStory(args);
|
|
2005
|
+
case "update_story":
|
|
2006
|
+
return handleUpdateStory(args);
|
|
2007
|
+
case "delete_story":
|
|
2008
|
+
return handleDeleteStory(args);
|
|
2009
|
+
default:
|
|
2010
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
2011
|
+
}
|
|
128
2012
|
});
|
|
129
|
-
|
|
2013
|
+
var transport = new StdioServerTransport();
|
|
130
2014
|
await server.connect(transport);
|
|
131
2015
|
//# sourceMappingURL=index.js.map
|