prism-mcp-server 20.0.7 → 20.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,148 +0,0 @@
1
- export const ADAPTIVE_GET_PROFILE_TOOL = {
2
- name: "adaptive_get_profile",
3
- description: "Get the user's current adaptive profile and a compact signals snapshot. " +
4
- "Use this when you need the user's dominant mood, motor rhythm, noise " +
5
- "environment, or vocabulary preferences — typically before generating a " +
6
- "response that should match their state. Returns the full profile plus a " +
7
- "small `signals` block that is safe to embed in a prompt.",
8
- inputSchema: {
9
- type: "object",
10
- properties: {
11
- user_id: {
12
- type: "string",
13
- description: "Optional. User identifier. If omitted, the server uses the authenticated user from context.",
14
- },
15
- include_history: {
16
- type: "boolean",
17
- description: "If true, include the toneHistory and full timeOfDayPatterns. Defaults to false (signals + summary only) to keep payload small.",
18
- },
19
- },
20
- },
21
- };
22
- export const ADAPTIVE_SET_PROFILE_TOOL = {
23
- name: "adaptive_set_profile",
24
- description: "Replace the user's adaptive profile in full. Intended for caregivers " +
25
- "syncing across devices, restoring from backup, or admin migration. " +
26
- "Schema must match version 2 of the AdaptiveProfile (see " +
27
- "src/shared/adaptiveEngine.ts).",
28
- inputSchema: {
29
- type: "object",
30
- properties: {
31
- user_id: {
32
- type: "string",
33
- description: "Optional. User identifier (defaults to authenticated user).",
34
- },
35
- profile: {
36
- type: "object",
37
- description: "Full AdaptiveProfile object. Must include version: 2.",
38
- },
39
- },
40
- required: ["profile"],
41
- },
42
- };
43
- export const ADAPTIVE_RECORD_EVENT_TOOL = {
44
- name: "adaptive_record_event",
45
- description: "Record a single behavioral event into the adaptive profile. " +
46
- "Use this from any surface that observes user behavior (voice agents, " +
47
- "AAC dwell triggers, message logs). Events are written incrementally — " +
48
- "no need to fetch+modify+save the whole profile. " +
49
- "Event types: " +
50
- "tone (text → AdaptiveTone, also records to toneHistory), " +
51
- "dwell (dwellMs sample for motor rhythm), " +
52
- "move_speed (px/sec sample for cursor smoothing), " +
53
- "noise (rmsDb sample for environment), " +
54
- "message (text + optional categoryId for vocab + frequency tracking), " +
55
- "mispronunciation (heard → intended; emergency words always pass through).",
56
- inputSchema: {
57
- type: "object",
58
- properties: {
59
- user_id: { type: "string", description: "Optional user id." },
60
- event: {
61
- type: "string",
62
- enum: ["tone", "dwell", "move_speed", "noise", "message", "mispronunciation"],
63
- description: "Event type.",
64
- },
65
- text: {
66
- type: "string",
67
- description: "For tone/message: the utterance text. For mispronunciation: the heard form.",
68
- },
69
- intended: {
70
- type: "string",
71
- description: "For mispronunciation: the corrected form.",
72
- },
73
- value: {
74
- type: "number",
75
- description: "For dwell/move_speed/noise: the numeric sample.",
76
- },
77
- category_id: {
78
- type: "string",
79
- description: "For message: optional category id (e.g. 'food', 'feelings').",
80
- },
81
- },
82
- required: ["event"],
83
- },
84
- };
85
- export const ADAPTIVE_DETECT_TONE_TOOL = {
86
- name: "adaptive_detect_tone",
87
- description: "Detect emotional tone from a piece of text WITHOUT recording it. " +
88
- "Pure function — useful when you want to route a response (e.g. choose " +
89
- "a TTS voice style or shape an LLM system prompt) but don't want to " +
90
- "perturb the user's profile. Returns one of: " +
91
- "neutral | friendly | excited | empathetic | serious. Emergency words " +
92
- "(help/hurt/scared/911/etc) always map to 'serious'.",
93
- inputSchema: {
94
- type: "object",
95
- properties: {
96
- text: { type: "string", description: "Text to analyze." },
97
- },
98
- required: ["text"],
99
- },
100
- };
101
- export const ADAPTIVE_RESET_TOOL = {
102
- name: "adaptive_reset",
103
- description: "Wipe the user's adaptive profile. Caregiver-initiated reset only — " +
104
- "resets all learned dwell, motor speed, vocabulary, mispronunciation " +
105
- "corrections, tone history, and noise calibration. Returns the fresh " +
106
- "default profile.",
107
- inputSchema: {
108
- type: "object",
109
- properties: {
110
- user_id: { type: "string", description: "Optional user id." },
111
- confirm: {
112
- type: "boolean",
113
- description: "Must be true. Defends against accidental reset.",
114
- },
115
- },
116
- required: ["confirm"],
117
- },
118
- };
119
- // ─── Type guards (mirror the patterns used elsewhere in this repo) ───
120
- export function isAdaptiveGetProfileArgs(args) {
121
- return typeof args === "object" && args !== null;
122
- }
123
- export function isAdaptiveSetProfileArgs(args) {
124
- return (typeof args === "object" &&
125
- args !== null &&
126
- "profile" in args &&
127
- typeof args.profile === "object" &&
128
- args.profile !== null);
129
- }
130
- export function isAdaptiveRecordEventArgs(args) {
131
- if (typeof args !== "object" || args === null)
132
- return false;
133
- const a = args;
134
- return (typeof a.event === "string" &&
135
- ["tone", "dwell", "move_speed", "noise", "message", "mispronunciation"].includes(a.event));
136
- }
137
- export function isAdaptiveDetectToneArgs(args) {
138
- return (typeof args === "object" &&
139
- args !== null &&
140
- "text" in args &&
141
- typeof args.text === "string");
142
- }
143
- export function isAdaptiveResetArgs(args) {
144
- return (typeof args === "object" &&
145
- args !== null &&
146
- "confirm" in args &&
147
- args.confirm === true);
148
- }
@@ -1,87 +0,0 @@
1
- // ─── session_instant_write ─────────────────────────────────
2
- export const SESSION_INSTANT_WRITE_TOOL = {
3
- name: "session_instant_write",
4
- description: "Instantly overwrite a file with new code to bypass manual IDE diffs and maintain flow state.\n\n" +
5
- "**How it works:**\n" +
6
- "1. Copies the current state of the target file into a hidden snapshot in `.prism/snapshots`.\n" +
7
- "2. Immediately replaces the target file contents with the desired code.\n" +
8
- "3. Returns a `snapshot_id` which can be used to instantly revert the change if it breaks compilation or UI.",
9
- inputSchema: {
10
- type: "object",
11
- properties: {
12
- file_path: {
13
- type: "string",
14
- description: "Absolute file path to write to.",
15
- },
16
- content: {
17
- type: "string",
18
- description: "The complete raw code string to write to the file. Exclude backticks unless part of code.",
19
- },
20
- project: {
21
- type: "string",
22
- description: "Project identifier (for scoping snapshots if needed).",
23
- },
24
- },
25
- required: ["file_path", "content", "project"],
26
- },
27
- };
28
- // ─── session_snapshot_revert ─────────────────────────────────
29
- export const SESSION_SNAPSHOT_REVERT_TOOL = {
30
- name: "session_snapshot_revert",
31
- description: "Revert a file change that was executed via `session_instant_write`.\n\n" +
32
- "Use this tool when the developer clicks 'Revert' or if the background terminal detects a fatal compiler panic immediately after writing.",
33
- inputSchema: {
34
- type: "object",
35
- properties: {
36
- snapshot_id: {
37
- type: "string",
38
- description: "The unique snapshot ID returned by the previous write operation.",
39
- },
40
- file_path: {
41
- type: "string",
42
- description: "The path of the file to revert.",
43
- },
44
- },
45
- required: ["snapshot_id", "file_path"],
46
- },
47
- };
48
- // ─── session_spawn_monitored_process ──────────────────────────
49
- export const SESSION_SPAWN_MONITORED_PROCESS_TOOL = {
50
- name: "session_spawn_monitored_process",
51
- description: "Launch a long-running process (like `npm run dev`) and monitor its output for compilation/critical errors.\n\n" +
52
- "**How it works:**\n" +
53
- "1. Spawns the process in the background.\n" +
54
- "2. Continuously reads `stdout` and `stderr` looking for regex patterns (like Vite errors, TS compiler errors).\n" +
55
- "3. If an error is detected, the MCP server updates the `telemetry://{project}/errors` resource, which triggers a notification to the IDE Client to auto-dispatch a self-correcting prompt.",
56
- inputSchema: {
57
- type: "object",
58
- properties: {
59
- project: { type: "string" },
60
- command: { type: "string" },
61
- cwd: { type: "string" },
62
- },
63
- required: ["project", "command", "cwd"],
64
- },
65
- };
66
- export function isInstantWriteArgs(args) {
67
- if (typeof args !== "object" || args === null)
68
- return false;
69
- const a = args;
70
- return (typeof a.file_path === "string" &&
71
- typeof a.content === "string" &&
72
- typeof a.project === "string");
73
- }
74
- export function isSnapshotRevertArgs(args) {
75
- if (typeof args !== "object" || args === null)
76
- return false;
77
- const a = args;
78
- return typeof a.snapshot_id === "string" && typeof a.file_path === "string";
79
- }
80
- export function isSpawnMonitoredProcessArgs(args) {
81
- if (typeof args !== "object" || args === null)
82
- return false;
83
- const a = args;
84
- return (typeof a.project === "string" &&
85
- typeof a.command === "string" &&
86
- typeof a.cwd === "string");
87
- }
@@ -1,164 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import crypto from "crypto";
4
- import { spawn } from "child_process";
5
- import { isInstantWriteArgs, isSnapshotRevertArgs, isSpawnMonitoredProcessArgs } from "./interactiveDefinitions.js";
6
- // Minimal global mock store for caught telemetry errors (usually connected to MCP Resource Updates)
7
- export const telemetryErrors = {};
8
- // Helper to determine snapshot directory per target file.
9
- function getSnapshotDir(filePath) {
10
- const dir = path.dirname(filePath);
11
- const snapDir = path.join(dir, ".prism", "snapshots");
12
- if (!fs.existsSync(snapDir)) {
13
- fs.mkdirSync(snapDir, { recursive: true });
14
- }
15
- return snapDir;
16
- }
17
- export async function sessionInstantWriteHandler(args) {
18
- if (!isInstantWriteArgs(args)) {
19
- throw new Error("Invalid arguments: expected file_path, content, and project");
20
- }
21
- const { file_path, content, project } = args;
22
- try {
23
- // 1. Snapshot the current file state if it exists.
24
- let snapshotId = "no_snapshot";
25
- if (fs.existsSync(file_path)) {
26
- snapshotId = crypto.randomUUID();
27
- const snapDir = getSnapshotDir(file_path);
28
- const snapshotPath = path.join(snapDir, `${snapshotId}.snap`);
29
- fs.copyFileSync(file_path, snapshotPath);
30
- }
31
- // 2. Write the new content to disk.
32
- // Ensure parent directory exists for new files
33
- const parentDir = path.dirname(file_path);
34
- if (!fs.existsSync(parentDir)) {
35
- fs.mkdirSync(parentDir, { recursive: true });
36
- }
37
- fs.writeFileSync(file_path, content, "utf-8");
38
- return {
39
- content: [
40
- {
41
- type: "text",
42
- text: `SUCCESS: Instantly wrote to ${file_path}\n` +
43
- `Snapshot ID: ${snapshotId}\n` +
44
- `The development server should hot-reload automatically.`
45
- }
46
- ]
47
- };
48
- }
49
- catch (error) {
50
- return {
51
- isError: true,
52
- content: [
53
- {
54
- type: "text",
55
- text: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
56
- }
57
- ]
58
- };
59
- }
60
- }
61
- export async function sessionSnapshotRevertHandler(args) {
62
- if (!isSnapshotRevertArgs(args)) {
63
- throw new Error("Invalid arguments: expected snapshot_id and file_path");
64
- }
65
- const { snapshot_id, file_path } = args;
66
- try {
67
- if (snapshot_id === "no_snapshot") {
68
- // Revert means deleting the newly created file
69
- if (fs.existsSync(file_path)) {
70
- fs.unlinkSync(file_path);
71
- return {
72
- content: [
73
- {
74
- type: "text",
75
- text: `SUCCESS: Reverted the change by deleting newly created file ${file_path}.`
76
- }
77
- ]
78
- };
79
- }
80
- else {
81
- return {
82
- isError: true,
83
- content: [{ type: "text", text: "File did not exist to delete." }]
84
- };
85
- }
86
- }
87
- const snapDir = getSnapshotDir(file_path);
88
- const snapshotPath = path.join(snapDir, `${snapshot_id}.snap`);
89
- if (!fs.existsSync(snapshotPath)) {
90
- throw new Error(`Snapshot ${snapshot_id} not found for this file.`);
91
- }
92
- // 1. Restore file
93
- fs.copyFileSync(snapshotPath, file_path);
94
- // 2. Cleanup snapshot payload
95
- fs.unlinkSync(snapshotPath);
96
- return {
97
- content: [
98
- {
99
- type: "text",
100
- text: `SUCCESS: Reverted ${file_path} to previous state (Snapshot ID: ${snapshot_id}).`
101
- }
102
- ]
103
- };
104
- }
105
- catch (error) {
106
- return {
107
- isError: true,
108
- content: [
109
- {
110
- type: "text",
111
- text: `Failed to revert file: ${error instanceof Error ? error.message : String(error)}`,
112
- }
113
- ]
114
- };
115
- }
116
- }
117
- export async function sessionSpawnMonitoredProcessHandler(args) {
118
- if (!isSpawnMonitoredProcessArgs(args)) {
119
- throw new Error("Invalid arguments: expected project, command, and cwd");
120
- }
121
- const { project, command, cwd } = args;
122
- try {
123
- const [cmd, ...cmdArgs] = command.split(" ");
124
- const child = spawn(cmd, cmdArgs, { cwd, shell: true });
125
- if (!telemetryErrors[project]) {
126
- telemetryErrors[project] = [];
127
- }
128
- const interceptStream = (data) => {
129
- const output = data.toString();
130
- // Look for critical error signatures: NextJS ParseError, Vite Internal Error, TypeScript TS...
131
- if (output.includes("SyntaxError:") ||
132
- output.includes("Parsing error:") ||
133
- output.match(/TS\d{4}:/) ||
134
- output.includes("Internal server error") ||
135
- output.includes("ERR_MODULE_NOT_FOUND")) {
136
- telemetryErrors[project].push(`[${new Date().toISOString()}] TELEMETRY_CRITICAL: ${output.trim()}`);
137
- // NOTE: In a full MCP implementation, this would trigger `notifyResourceUpdate`
138
- // for `telemetry://${project}/errors` to alert the client immediately.
139
- console.error(`[Telemetry Intercept] Caught critical compilation error for project: ${project}`);
140
- }
141
- };
142
- child.stdout.on("data", interceptStream);
143
- child.stderr.on("data", interceptStream);
144
- return {
145
- content: [
146
- {
147
- type: "text",
148
- text: `Spawned monitored process: \`${command}\` in ${cwd}.\nAutonomic telemetry is active. Breaking changes via instant_write will auto-populate telemetry resources.`
149
- }
150
- ]
151
- };
152
- }
153
- catch (error) {
154
- return {
155
- isError: true,
156
- content: [
157
- {
158
- type: "text",
159
- text: `Failed to spawn monitored process: ${error instanceof Error ? error.message : String(error)}`,
160
- }
161
- ]
162
- };
163
- }
164
- }
@@ -1,214 +0,0 @@
1
- /**
2
- * Prism Projects — MCP Tool Handlers (Phase 4)
3
- * ===============================================
4
- *
5
- * CRUD operations for projects and team membership.
6
- * Tier limits enforced via getCloudLimits().
7
- *
8
- * Tools:
9
- * - project_create: Create a new project
10
- * - project_list: List all projects for current user
11
- * - project_update: Update project name/description/status
12
- * - project_delete: Archive/delete a project
13
- * - project_assign_member: Add a member to a project
14
- * - project_remove_member: Remove a member from a project
15
- */
16
- import { randomUUID } from 'crypto';
17
- import { getStorage } from '../storage/index.js';
18
- import { getCloudLimits } from '../prism-cloud.js';
19
- import { PRISM_USER_ID } from '../config.js';
20
- const VALID_STATUSES = ['draft', 'active', 'on_hold', 'completed', 'archived'];
21
- const VALID_ROLES = ['owner', 'editor', 'viewer'];
22
- // Tier limits for projects
23
- const PROJECT_LIMITS = {
24
- free: { maxProjects: 1, maxMembers: 1 },
25
- standard: { maxProjects: 9, maxMembers: 5 },
26
- advanced: { maxProjects: 999, maxMembers: 25 },
27
- enterprise: { maxProjects: 999999, maxMembers: 999999 },
28
- };
29
- function makeResult(text, isError = false) {
30
- return { content: [{ type: 'text', text }], isError };
31
- }
32
- // ─── project_create ──────────────────────────────────────────
33
- export async function projectCreateHandler(args) {
34
- const { name, description, status, team_id } = args;
35
- if (!name || typeof name !== 'string' || name.trim().length === 0) {
36
- return makeResult('❌ Missing required field: name', true);
37
- }
38
- if (status && !VALID_STATUSES.includes(status)) {
39
- return makeResult(`❌ Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}`, true);
40
- }
41
- // Check tier limits
42
- const limits = getCloudLimits();
43
- const tierLimits = PROJECT_LIMITS[limits.tier] || PROJECT_LIMITS.free;
44
- const storage = await getStorage();
45
- const db = storage.db;
46
- if (!db) {
47
- return makeResult('❌ Project management requires SQLite storage.', true);
48
- }
49
- // Count existing projects
50
- const countResult = await db.execute({
51
- sql: 'SELECT COUNT(*) as cnt FROM prism_projects WHERE created_by = ?',
52
- args: [PRISM_USER_ID],
53
- });
54
- const currentCount = Number(countResult.rows[0]?.cnt || 0);
55
- if (currentCount >= tierLimits.maxProjects) {
56
- return makeResult(`❌ Project limit reached (${currentCount}/${tierLimits.maxProjects}). ` +
57
- `Upgrade your plan to create more projects.`, true);
58
- }
59
- const id = randomUUID();
60
- await db.execute({
61
- sql: `INSERT INTO prism_projects (id, name, description, status, created_by, team_id)
62
- VALUES (?, ?, ?, ?, ?, ?)`,
63
- args: [id, name.trim(), description || '', status || 'draft', PRISM_USER_ID, team_id || null],
64
- });
65
- // Auto-assign creator as owner
66
- await db.execute({
67
- sql: `INSERT INTO prism_project_members (project_id, user_id, role)
68
- VALUES (?, ?, 'owner')`,
69
- args: [id, PRISM_USER_ID],
70
- });
71
- return makeResult(`✅ Project created: "${name.trim()}" (${id})\n` +
72
- ` Status: ${status || 'draft'}\n` +
73
- ` Projects used: ${currentCount + 1}/${tierLimits.maxProjects}`);
74
- }
75
- // ─── project_list ────────────────────────────────────────────
76
- export async function projectListHandler(args) {
77
- const storage = await getStorage();
78
- const db = storage.db;
79
- if (!db) {
80
- return makeResult('❌ Project management requires SQLite storage.', true);
81
- }
82
- let sql = `SELECT p.*, m.role as my_role
83
- FROM prism_projects p
84
- LEFT JOIN prism_project_members m ON p.id = m.project_id AND m.user_id = ?
85
- WHERE (p.created_by = ? OR m.user_id = ?)`;
86
- const sqlArgs = [PRISM_USER_ID, PRISM_USER_ID, PRISM_USER_ID];
87
- if (args.status) {
88
- sql += ` AND p.status = ?`;
89
- sqlArgs.push(args.status);
90
- }
91
- if (args.team_id) {
92
- sql += ` AND p.team_id = ?`;
93
- sqlArgs.push(args.team_id);
94
- }
95
- sql += ` ORDER BY p.updated_at DESC`;
96
- const result = await db.execute({ sql, args: sqlArgs });
97
- const projects = result.rows;
98
- if (projects.length === 0) {
99
- return makeResult('📁 No projects found. Use `project_create` to create one.');
100
- }
101
- const lines = projects.map((p) => {
102
- const statusIcon = p.status === 'active' ? '🟢' : p.status === 'draft' ? '📝' : p.status === 'completed' ? '✅' : p.status === 'archived' ? '📦' : '⏸️';
103
- return `${statusIcon} **${p.name}** (${p.id.substring(0, 8)})\n Status: ${p.status} | Role: ${p.my_role || 'owner'} | Updated: ${p.updated_at}`;
104
- });
105
- const limits = getCloudLimits();
106
- const tierLimits = PROJECT_LIMITS[limits.tier] || PROJECT_LIMITS.free;
107
- return makeResult(`📁 **Projects** (${projects.length}/${tierLimits.maxProjects})\n\n` +
108
- lines.join('\n\n'));
109
- }
110
- // ─── project_update ──────────────────────────────────────────
111
- export async function projectUpdateHandler(args) {
112
- const { project_id, name, description, status } = args;
113
- if (!project_id) {
114
- return makeResult('❌ Missing required field: project_id', true);
115
- }
116
- if (status && !VALID_STATUSES.includes(status)) {
117
- return makeResult(`❌ Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}`, true);
118
- }
119
- const storage = await getStorage();
120
- const db = storage.db;
121
- if (!db)
122
- return makeResult('❌ Project management requires SQLite storage.', true);
123
- const sets = ["updated_at = datetime('now')"];
124
- const sqlArgs = [];
125
- if (name) {
126
- sets.push('name = ?');
127
- sqlArgs.push(name.trim());
128
- }
129
- if (description !== undefined) {
130
- sets.push('description = ?');
131
- sqlArgs.push(description);
132
- }
133
- if (status) {
134
- sets.push('status = ?');
135
- sqlArgs.push(status);
136
- }
137
- sqlArgs.push(project_id);
138
- await db.execute({
139
- sql: `UPDATE prism_projects SET ${sets.join(', ')} WHERE id = ?`,
140
- args: sqlArgs,
141
- });
142
- return makeResult(`✅ Project ${project_id.substring(0, 8)} updated.`);
143
- }
144
- // ─── project_delete ──────────────────────────────────────────
145
- export async function projectDeleteHandler(args) {
146
- const { project_id, hard } = args;
147
- if (!project_id) {
148
- return makeResult('❌ Missing required field: project_id', true);
149
- }
150
- const storage = await getStorage();
151
- const db = storage.db;
152
- if (!db)
153
- return makeResult('❌ Project management requires SQLite storage.', true);
154
- if (hard) {
155
- await db.execute({ sql: 'DELETE FROM prism_projects WHERE id = ?', args: [project_id] });
156
- return makeResult(`🗑️ Project ${project_id.substring(0, 8)} permanently deleted.`);
157
- }
158
- else {
159
- await db.execute({
160
- sql: `UPDATE prism_projects SET status = 'archived', updated_at = datetime('now') WHERE id = ?`,
161
- args: [project_id],
162
- });
163
- return makeResult(`📦 Project ${project_id.substring(0, 8)} archived.`);
164
- }
165
- }
166
- // ─── project_assign_member ───────────────────────────────────
167
- export async function projectAssignMemberHandler(args) {
168
- const { project_id, user_id, role } = args;
169
- if (!project_id || !user_id) {
170
- return makeResult('❌ Missing required fields: project_id, user_id', true);
171
- }
172
- const effectiveRole = role || 'viewer';
173
- if (!VALID_ROLES.includes(effectiveRole)) {
174
- return makeResult(`❌ Invalid role. Must be one of: ${VALID_ROLES.join(', ')}`, true);
175
- }
176
- // Check tier limits
177
- const limits = getCloudLimits();
178
- const tierLimits = PROJECT_LIMITS[limits.tier] || PROJECT_LIMITS.free;
179
- const storage = await getStorage();
180
- const db = storage.db;
181
- if (!db)
182
- return makeResult('❌ Project management requires SQLite storage.', true);
183
- const countResult = await db.execute({
184
- sql: 'SELECT COUNT(*) as cnt FROM prism_project_members WHERE project_id = ?',
185
- args: [project_id],
186
- });
187
- const currentMembers = Number(countResult.rows[0]?.cnt || 0);
188
- if (currentMembers >= tierLimits.maxMembers) {
189
- return makeResult(`❌ Member limit reached (${currentMembers}/${tierLimits.maxMembers}). ` +
190
- `Upgrade your plan to add more members.`, true);
191
- }
192
- await db.execute({
193
- sql: `INSERT OR REPLACE INTO prism_project_members (project_id, user_id, role)
194
- VALUES (?, ?, ?)`,
195
- args: [project_id, user_id, effectiveRole],
196
- });
197
- return makeResult(`✅ ${user_id} assigned as ${effectiveRole} to project ${project_id.substring(0, 8)}.`);
198
- }
199
- // ─── project_remove_member ───────────────────────────────────
200
- export async function projectRemoveMemberHandler(args) {
201
- const { project_id, user_id } = args;
202
- if (!project_id || !user_id) {
203
- return makeResult('❌ Missing required fields: project_id, user_id', true);
204
- }
205
- const storage = await getStorage();
206
- const db = storage.db;
207
- if (!db)
208
- return makeResult('❌ Project management requires SQLite storage.', true);
209
- await db.execute({
210
- sql: 'DELETE FROM prism_project_members WHERE project_id = ? AND user_id = ?',
211
- args: [project_id, user_id],
212
- });
213
- return makeResult(`✅ ${user_id} removed from project ${project_id.substring(0, 8)}.`);
214
- }