exponential-mcp 0.4.0 → 0.6.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 CHANGED
@@ -106,6 +106,8 @@ exponential-mcp serve
106
106
  |------|-------------|
107
107
  | `get_workspaces` | List all workspaces |
108
108
  | `get_projects` | List projects (optionally by workspace) |
109
+ | `get_project` | One project in full: linked objectives & key results, DRI, team, dates |
110
+ | `update_project` | Rename, change status/priority, set dates, re-link to OKRs |
109
111
  | `get_actions` | List actions/tasks (filter by project or status; no date filtering) |
110
112
  | `get_todays_actions` | **What's on your plate now** — overdue / today / inbox, across all workspaces |
111
113
  | `get_overdue_triage` | Why the overdue pile is that size: bulk-created cohorts vs real debt |
@@ -114,7 +116,13 @@ exponential-mcp serve
114
116
  | `defer_actions` | Amnesty: clear dates, back to the project backlog untimed |
115
117
  | `reschedule_actions` | Move actions to a new do-date |
116
118
  | `complete_action` | Mark an action as done |
117
- | `get_goals` | List OKRs with progress |
119
+ | `get_goals` | List objectives, flat or as the annual → quarterly tree |
120
+ | `get_key_results` | List key results, grouped by objective or flat |
121
+ | `get_meetings` | List meetings, newest first, with a summary preview (no notes/transcript) |
122
+ | `get_meeting` | One meeting with notes; transcript on request |
123
+ | `create_meeting` | Record a meeting from a transcript or notes |
124
+ | `update_meeting` | Edit title, description, summary, date, or replace notes |
125
+ | `append_meeting_notes` | Add to a meeting's notes without overwriting them |
118
126
  | `search` | Search across everything |
119
127
 
120
128
  ### Asking about the day
package/dist/cli.js CHANGED
File without changes
package/dist/index.js CHANGED
@@ -106,6 +106,49 @@ function parseDateArg(raw) {
106
106
  }
107
107
  return parsed;
108
108
  }
109
+ const SUMMARY_PREVIEW_CHARS = 400;
110
+ const DEFAULT_MEETING_LIMIT = 30;
111
+ /**
112
+ * A meeting row as it comes off the list read is ~3k characters of metadata
113
+ * plus the full transcript, summary and notes bodies — a single hour-long
114
+ * transcript is easily 50k+ characters, and the server offers no limit or
115
+ * projection. The list tool sends a compact row with a summary preview; the
116
+ * detail tool sends everything except the transcript unless asked.
117
+ */
118
+ function meetingListRow(m) {
119
+ const summary = m.summary ?? null;
120
+ const truncated = summary !== null && summary.length > SUMMARY_PREVIEW_CHARS;
121
+ return {
122
+ id: m.id,
123
+ title: m.title,
124
+ description: m.description,
125
+ meetingDate: m.meetingDate,
126
+ createdAt: m.createdAt,
127
+ durationSeconds: m.durationSeconds ?? null,
128
+ participantCount: m.participantCount ?? null,
129
+ projectId: m.projectId,
130
+ project: m.project ?? null,
131
+ workspaceId: m.workspaceId,
132
+ archivedAt: m.archivedAt,
133
+ source: m.sourceIntegration?.provider ?? null,
134
+ participants: (m.participants ?? []).map((p) => ({ id: p.id, name: p.name, email: p.email })),
135
+ actions: m.actions ?? [],
136
+ summaryPreview: truncated ? summary.slice(0, SUMMARY_PREVIEW_CHARS) + '…' : summary,
137
+ summaryTruncated: truncated,
138
+ hasNotes: Boolean(m.notes),
139
+ hasTranscript: Boolean(m.transcription),
140
+ };
141
+ }
142
+ function meetingDetail(m, includeTranscript) {
143
+ // sentencesJson / analyticsJson are the server's internal processing
144
+ // artefacts; they are not part of the SDK type and never useful to a model.
145
+ const { transcription, sentencesJson, analyticsJson, ...rest } = m;
146
+ return {
147
+ ...rest,
148
+ hasTranscript: Boolean(transcription),
149
+ ...(includeTranscript ? { transcription } : {}),
150
+ };
151
+ }
109
152
  // Tool definitions
110
153
  const TOOLS = [
111
154
  {
@@ -121,6 +164,64 @@ const TOOLS = [
121
164
  }
122
165
  }
123
166
  },
167
+ {
168
+ name: 'get_project',
169
+ description: 'Fetch one project with its relations: the objectives and key results it is linked to, DRI, team, dates, description, and linked meetings. get_projects returns none of the OKR links, so this is the tool for "what is this project driving?". It does NOT include the project\'s tasks — use get_actions with projectId for those. Accepts a project ID, a slug, or the slug-id form from app URLs.',
170
+ inputSchema: {
171
+ type: 'object',
172
+ properties: {
173
+ id: {
174
+ type: 'string',
175
+ description: 'Project ID, slug, or the compound slug-id from an app URL'
176
+ }
177
+ },
178
+ required: ['id']
179
+ }
180
+ },
181
+ {
182
+ name: 'update_project',
183
+ description: 'Update a project: rename, change status or priority, edit the description, set dates, or re-link it to objectives / key results. Only the fields you pass are changed. goalIds and keyResultIds REPLACE the existing links wholesale — read get_project first and pass the full set you want to keep. An empty keyResultIds clears the key-result links; an empty goalIds is ignored by the server.',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ id: { type: 'string', description: 'Project ID' },
188
+ name: { type: 'string', description: 'New name' },
189
+ description: {
190
+ type: 'string',
191
+ description: 'New description. Pass an empty string to blank it.'
192
+ },
193
+ status: {
194
+ type: 'string',
195
+ enum: ['ACTIVE', 'ON_HOLD', 'COMPLETED', 'CANCELLED'],
196
+ description: 'New lifecycle status'
197
+ },
198
+ priority: {
199
+ type: 'string',
200
+ enum: ['HIGH', 'MEDIUM', 'LOW', 'NONE'],
201
+ description: 'New priority'
202
+ },
203
+ driId: {
204
+ type: 'string',
205
+ description: 'User ID of the directly responsible individual, or null to clear'
206
+ },
207
+ goalIds: {
208
+ type: 'array',
209
+ items: { type: 'string' },
210
+ description: 'Objective IDs (integers, passed as strings) to link — replaces the current set'
211
+ },
212
+ keyResultIds: {
213
+ type: 'array',
214
+ items: { type: 'string' },
215
+ description: 'Key result IDs to link — replaces the current set; [] clears'
216
+ },
217
+ startDate: { type: 'string', description: 'ISO datetime, or null to clear' },
218
+ endDate: { type: 'string', description: 'ISO datetime, or null to clear' },
219
+ reviewDate: { type: 'string', description: 'ISO datetime, or null to clear' },
220
+ nextActionDate: { type: 'string', description: 'ISO datetime, or null to clear' }
221
+ },
222
+ required: ['id']
223
+ }
224
+ },
124
225
  {
125
226
  name: 'get_actions',
126
227
  description: 'List actions/tasks, optionally filtered by project or status. This is a flat list with no date filtering — to answer "what should I work on today" or "what am I behind on", use get_todays_actions instead.',
@@ -260,17 +361,166 @@ const TOOLS = [
260
361
  },
261
362
  {
262
363
  name: 'get_goals',
263
- description: 'List OKRs (Objectives and Key Results) with their progress',
364
+ description: "List objectives (goals). Each objective carries its linked projects, which is how work ladders up to an OKR: every action has a projectId. Objectives have INTEGER ids — key results, which are separate, have CUIDs (see get_key_results). Use tree: true for the annual → quarterly cascade.",
365
+ inputSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ workspaceId: {
369
+ type: 'string',
370
+ description: 'Optional workspace ID. With it the list is workspace-wide (every member\'s objectives); without it you get your own.'
371
+ },
372
+ period: {
373
+ type: 'string',
374
+ description: 'Optional period filter, e.g. "Q3-2026" or "Annual-2026"'
375
+ },
376
+ status: {
377
+ type: 'string',
378
+ description: 'Optional status: planned, active, completed, archived, on-hold'
379
+ },
380
+ tree: {
381
+ type: 'boolean',
382
+ description: 'Return objectives nested parent → child (up to 5 levels), each with its projects and key results, instead of a flat list'
383
+ }
384
+ }
385
+ }
386
+ },
387
+ {
388
+ name: 'get_key_results',
389
+ description: "List key results — the measurable half of an OKR. By default groups them under their objectives (the richest read: one call for 'how is the quarter going'). Pass flat: true for a bare list. Key result ids are CUIDs; the goalId tying one to its objective is an integer.",
264
390
  inputSchema: {
265
391
  type: 'object',
266
392
  properties: {
267
393
  workspaceId: {
268
394
  type: 'string',
269
- description: 'Optional workspace ID'
395
+ description: "Optional workspace ID. With it the list is workspace-wide (every member's key results); without it you get your own."
396
+ },
397
+ period: {
398
+ type: 'string',
399
+ description: 'Optional period filter, e.g. "Q3-2026"'
400
+ },
401
+ goalId: {
402
+ type: 'number',
403
+ description: 'Optional objective ID (an integer). Only applies with flat: true.'
404
+ },
405
+ status: {
406
+ type: 'string',
407
+ description: 'Optional status: not-started, on-track, at-risk, off-track, achieved. Only applies with flat: true.'
408
+ },
409
+ flat: {
410
+ type: 'boolean',
411
+ description: 'Return a flat list of key results rather than grouping them by objective'
412
+ }
413
+ }
414
+ }
415
+ },
416
+ {
417
+ name: 'get_meetings',
418
+ description: 'List meetings the user can see — ones they own or attended, plus those on their projects and workspaces — newest first. Each row has metadata, participants, linked actions and a short preview of the AI summary; the full summary, notes and transcript are only in get_meeting. Use this to answer "when did I last meet Y" or to find a meeting ID, then get_meeting for what was actually said.',
419
+ inputSchema: {
420
+ type: 'object',
421
+ properties: {
422
+ workspaceId: {
423
+ type: 'string',
424
+ description: 'Optional workspace ID to restrict to one workspace'
425
+ },
426
+ meetingType: {
427
+ type: 'string',
428
+ enum: ['all', 'mine', 'one_on_one'],
429
+ description: 'all (default) — everything visible; mine — meetings the user owns or attended; one_on_one — exactly two participants'
430
+ },
431
+ includeArchived: {
432
+ type: 'boolean',
433
+ description: 'Include archived meetings (default false)'
434
+ },
435
+ limit: {
436
+ type: 'number',
437
+ description: 'Max meetings to return, newest first (default 30)'
270
438
  }
271
439
  }
272
440
  }
273
441
  },
442
+ {
443
+ name: 'get_meeting',
444
+ description: 'Fetch one meeting in full: participants with speaker labels, linked actions, the AI summary, and the meeting notes. The raw transcript is omitted unless includeTranscript is true — it can be tens of thousands of characters, so only pull it when the summary and notes are not enough.',
445
+ inputSchema: {
446
+ type: 'object',
447
+ properties: {
448
+ id: { type: 'string', description: 'Meeting ID' },
449
+ includeTranscript: {
450
+ type: 'boolean',
451
+ description: 'Also return the raw transcript text (default false)'
452
+ }
453
+ },
454
+ required: ['id']
455
+ }
456
+ },
457
+ {
458
+ name: 'create_meeting',
459
+ description: 'Record a meeting in Exponential from a transcript or notes the user gives you. transcription is required by the server — if the user only has notes, put the notes text there too. A project-linked meeting inherits its project\'s workspace.',
460
+ inputSchema: {
461
+ type: 'object',
462
+ properties: {
463
+ title: { type: 'string', description: 'Meeting title' },
464
+ transcription: {
465
+ type: 'string',
466
+ description: 'Raw transcript text (required, at least one character)'
467
+ },
468
+ notes: { type: 'string', description: 'Meeting notes (Markdown)' },
469
+ description: { type: 'string', description: 'Short description' },
470
+ meetingDate: {
471
+ type: 'string',
472
+ description: 'When the meeting happened, as an ISO datetime (defaults to now)'
473
+ },
474
+ projectId: { type: 'string', description: 'Link to this project' },
475
+ workspaceId: { type: 'string', description: 'Workspace ID (ignored when projectId is set)' },
476
+ participants: {
477
+ type: 'array',
478
+ description: 'Who attended. Each entry needs at least one of userId, contactId, or email.',
479
+ items: {
480
+ type: 'object',
481
+ properties: {
482
+ userId: { type: 'string' },
483
+ contactId: { type: 'string' },
484
+ email: { type: 'string' },
485
+ name: { type: 'string' }
486
+ }
487
+ }
488
+ }
489
+ },
490
+ required: ['title', 'transcription']
491
+ }
492
+ },
493
+ {
494
+ name: 'update_meeting',
495
+ description: 'Edit a meeting\'s title, description, summary, date, or REPLACE its notes wholesale. To add to existing notes without rewriting them, use append_meeting_notes instead. Only the fields you pass are changed.',
496
+ inputSchema: {
497
+ type: 'object',
498
+ properties: {
499
+ id: { type: 'string', description: 'Meeting ID' },
500
+ title: { type: 'string', description: 'New title' },
501
+ description: { type: 'string', description: 'New description' },
502
+ summary: { type: 'string', description: 'New summary' },
503
+ notes: { type: 'string', description: 'Replacement notes body (Markdown)' },
504
+ meetingDate: {
505
+ type: 'string',
506
+ description: 'When the meeting happened, as an ISO datetime, or null to clear'
507
+ }
508
+ },
509
+ required: ['id']
510
+ }
511
+ },
512
+ {
513
+ name: 'append_meeting_notes',
514
+ description: 'Append a block of text to a meeting\'s notes, separated by a blank line, creating the notes if there are none yet. This is the safe way to add a follow-up, decision, or action list to a meeting without clobbering what is already there.',
515
+ inputSchema: {
516
+ type: 'object',
517
+ properties: {
518
+ id: { type: 'string', description: 'Meeting ID' },
519
+ text: { type: 'string', description: 'Text to append (Markdown)' }
520
+ },
521
+ required: ['id', 'text']
522
+ }
523
+ },
274
524
  {
275
525
  name: 'search',
276
526
  description: "Global text search across everything the user can access — projects, actions/tasks, goals, workspaces (and more entity types as the API grows). Same coverage as the app's Cmd+K palette. Returns typed results with ids, workspace, and app URL.",
@@ -339,6 +589,45 @@ async function main() {
339
589
  ],
340
590
  };
341
591
  }
592
+ case 'get_project': {
593
+ const project = await client.projects.get(args?.id);
594
+ return {
595
+ content: [
596
+ {
597
+ type: 'text',
598
+ text: JSON.stringify(project, null, 2),
599
+ },
600
+ ],
601
+ };
602
+ }
603
+ case 'update_project': {
604
+ // `null` clears driId; the string "null" is the JSON-as-text form the
605
+ // model sometimes emits, same as parseDateArg handles for dates.
606
+ const rawDri = args?.driId;
607
+ const driId = rawDri === undefined ? undefined : rawDri === null || rawDri === 'null' ? null : rawDri;
608
+ const project = await client.projects.update({
609
+ id: args?.id,
610
+ name: args?.name,
611
+ description: args?.description,
612
+ status: args?.status,
613
+ priority: args?.priority,
614
+ driId,
615
+ goalIds: args?.goalIds,
616
+ keyResultIds: args?.keyResultIds,
617
+ startDate: parseDateArg(args?.startDate),
618
+ endDate: parseDateArg(args?.endDate),
619
+ reviewDate: parseDateArg(args?.reviewDate),
620
+ nextActionDate: parseDateArg(args?.nextActionDate),
621
+ });
622
+ return {
623
+ content: [
624
+ {
625
+ type: 'text',
626
+ text: `Updated project: ${project.name} (ID: ${project.id})`,
627
+ },
628
+ ],
629
+ };
630
+ }
342
631
  case 'get_actions': {
343
632
  const status = args?.status;
344
633
  const projectId = args?.projectId;
@@ -465,15 +754,124 @@ async function main() {
465
754
  ],
466
755
  };
467
756
  }
757
+ // Goes through the SDK's GoalsApi rather than a raw tRPC path. The old
758
+ // implementation called `goal.list`, which does not exist — the tool
759
+ // hard-errored with `No procedure found on path "goal.list"` on every
760
+ // invocation.
468
761
  case 'get_goals': {
469
- const goals = await trpcClient.goal.list.query({
762
+ const workspaceId = args?.workspaceId;
763
+ const period = args?.period;
764
+ const status = args?.status;
765
+ const goals = args?.tree
766
+ ? await client.goals.tree({ workspaceId, status })
767
+ : await client.goals.list({ workspaceId, period, status });
768
+ return {
769
+ content: [
770
+ {
771
+ type: 'text',
772
+ text: JSON.stringify(goals, null, 2),
773
+ },
774
+ ],
775
+ };
776
+ }
777
+ case 'get_key_results': {
778
+ const workspaceId = args?.workspaceId;
779
+ const period = args?.period;
780
+ const keyResults = args?.flat
781
+ ? await client.goals.keyResults.list({
782
+ workspaceId,
783
+ period,
784
+ goalId: args?.goalId,
785
+ status: args?.status,
786
+ })
787
+ : await client.goals.keyResults.byObjective({ workspaceId, period });
788
+ return {
789
+ content: [
790
+ {
791
+ type: 'text',
792
+ text: JSON.stringify(keyResults, null, 2),
793
+ },
794
+ ],
795
+ };
796
+ }
797
+ case 'get_meetings': {
798
+ const meetings = await client.meetings.list({
470
799
  workspaceId: args?.workspaceId,
800
+ meetingType: args?.meetingType,
801
+ includeArchived: args?.includeArchived,
471
802
  });
803
+ const limit = Math.max(1, Number(args?.limit) || DEFAULT_MEETING_LIMIT);
804
+ const when = (m) => new Date(m.meetingDate ?? m.createdAt).getTime();
805
+ const rows = [...meetings]
806
+ .sort((a, b) => when(b) - when(a))
807
+ .slice(0, limit)
808
+ .map(meetingListRow);
472
809
  return {
473
810
  content: [
474
811
  {
475
812
  type: 'text',
476
- text: JSON.stringify(goals, null, 2),
813
+ text: JSON.stringify({ total: meetings.length, returned: rows.length, meetings: rows }, null, 2),
814
+ },
815
+ ],
816
+ };
817
+ }
818
+ case 'get_meeting': {
819
+ const meeting = await client.meetings.get(args?.id);
820
+ return {
821
+ content: [
822
+ {
823
+ type: 'text',
824
+ text: JSON.stringify(meetingDetail(meeting, Boolean(args?.includeTranscript)), null, 2),
825
+ },
826
+ ],
827
+ };
828
+ }
829
+ case 'create_meeting': {
830
+ const meetingDate = parseDateArg(args?.meetingDate);
831
+ const meeting = await client.meetings.create({
832
+ title: args?.title,
833
+ transcription: args?.transcription,
834
+ notes: args?.notes,
835
+ description: args?.description,
836
+ meetingDate: meetingDate ?? undefined,
837
+ projectId: args?.projectId,
838
+ workspaceId: args?.workspaceId,
839
+ participants: args?.participants,
840
+ });
841
+ return {
842
+ content: [
843
+ {
844
+ type: 'text',
845
+ text: `Created meeting: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
846
+ },
847
+ ],
848
+ };
849
+ }
850
+ case 'update_meeting': {
851
+ const meeting = await client.meetings.update({
852
+ id: args?.id,
853
+ title: args?.title,
854
+ description: args?.description,
855
+ summary: args?.summary,
856
+ notes: args?.notes,
857
+ meetingDate: parseDateArg(args?.meetingDate),
858
+ });
859
+ return {
860
+ content: [
861
+ {
862
+ type: 'text',
863
+ text: `Updated meeting: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
864
+ },
865
+ ],
866
+ };
867
+ }
868
+ case 'append_meeting_notes': {
869
+ const meeting = await client.meetings.appendNotes(args?.id, args?.text);
870
+ return {
871
+ content: [
872
+ {
873
+ type: 'text',
874
+ text: `Appended to notes of: ${meeting.title ?? '(untitled)'} (ID: ${meeting.id})`,
477
875
  },
478
876
  ],
479
877
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exponential-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for Exponential - connect Claude to your projects, actions, and goals",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.30.0",
28
28
  "commander": "^12.0.0",
29
- "exponential-sdk": "^1.11.0",
29
+ "exponential-sdk": "^1.16.0",
30
30
  "zod": "^3.22.0"
31
31
  },
32
32
  "devDependencies": {
package/dist/api.d.ts DELETED
@@ -1,84 +0,0 @@
1
- /**
2
- * Exponential API Client
3
- * Handles authenticated requests to the Exponential API
4
- */
5
- export interface ExponentialConfig {
6
- apiKey: string;
7
- baseUrl?: string;
8
- }
9
- export interface Project {
10
- id: string;
11
- name: string;
12
- description?: string;
13
- status: string;
14
- priority: string;
15
- progress: number;
16
- }
17
- export interface Action {
18
- id: string;
19
- name: string;
20
- description?: string;
21
- status: string;
22
- priority: string;
23
- dueDate?: string;
24
- projectId?: string;
25
- project?: {
26
- name: string;
27
- };
28
- }
29
- export interface Goal {
30
- id: number;
31
- title: string;
32
- description?: string;
33
- period?: string;
34
- keyResults: KeyResult[];
35
- }
36
- export interface KeyResult {
37
- id: string;
38
- title: string;
39
- targetValue?: number;
40
- currentValue?: number;
41
- status: string;
42
- }
43
- export interface Workspace {
44
- id: string;
45
- name: string;
46
- slug: string;
47
- }
48
- export declare class ExponentialAPI {
49
- private apiKey;
50
- private baseUrl;
51
- constructor(config: ExponentialConfig);
52
- private request;
53
- private trpcQuery;
54
- private trpcMutation;
55
- getWorkspaces(): Promise<Workspace[]>;
56
- getProjects(workspaceId?: string): Promise<Project[]>;
57
- getProject(id: string): Promise<Project>;
58
- getActions(params?: {
59
- projectId?: string;
60
- status?: string;
61
- }): Promise<Action[]>;
62
- createAction(data: {
63
- name: string;
64
- description?: string;
65
- projectId?: string;
66
- priority?: string;
67
- dueDate?: string;
68
- }): Promise<Action>;
69
- updateAction(id: string, data: {
70
- name?: string;
71
- status?: string;
72
- priority?: string;
73
- dueDate?: string;
74
- }): Promise<Action>;
75
- completeAction(id: string): Promise<Action>;
76
- getGoals(workspaceId?: string): Promise<Goal[]>;
77
- getGoal(id: number): Promise<Goal>;
78
- quickCreateAction(text: string): Promise<Action>;
79
- search(query: string): Promise<{
80
- projects: Project[];
81
- actions: Action[];
82
- goals: Goal[];
83
- }>;
84
- }
package/dist/api.js DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * Exponential API Client
3
- * Handles authenticated requests to the Exponential API
4
- */
5
- const DEFAULT_BASE_URL = 'https://www.exponential.im';
6
- export class ExponentialAPI {
7
- apiKey;
8
- baseUrl;
9
- constructor(config) {
10
- this.apiKey = config.apiKey;
11
- this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
12
- }
13
- async request(endpoint, options = {}) {
14
- const url = `${this.baseUrl}${endpoint}`;
15
- const response = await fetch(url, {
16
- ...options,
17
- headers: {
18
- 'Authorization': `Bearer ${this.apiKey}`,
19
- 'Content-Type': 'application/json',
20
- ...options.headers,
21
- },
22
- });
23
- if (!response.ok) {
24
- const error = await response.text();
25
- throw new Error(`Exponential API error (${response.status}): ${error}`);
26
- }
27
- return response.json();
28
- }
29
- // tRPC-style calls
30
- async trpcQuery(procedure, input) {
31
- const inputStr = input ? encodeURIComponent(JSON.stringify(input)) : '';
32
- const url = `/api/trpc/${procedure}${inputStr ? `?input=${inputStr}` : ''}`;
33
- return this.request(url).then(r => r.result.data);
34
- }
35
- async trpcMutation(procedure, input) {
36
- const url = `/api/trpc/${procedure}`;
37
- return this.request(url, {
38
- method: 'POST',
39
- body: JSON.stringify(input),
40
- }).then(r => r.result.data);
41
- }
42
- // === Workspace ===
43
- async getWorkspaces() {
44
- return this.trpcQuery('workspace.list');
45
- }
46
- // === Projects ===
47
- async getProjects(workspaceId) {
48
- return this.trpcQuery('project.list', { workspaceId });
49
- }
50
- async getProject(id) {
51
- return this.trpcQuery('project.get', { id });
52
- }
53
- // === Actions ===
54
- async getActions(params) {
55
- return this.trpcQuery('action.list', params);
56
- }
57
- async createAction(data) {
58
- return this.trpcMutation('action.create', data);
59
- }
60
- async updateAction(id, data) {
61
- return this.trpcMutation('action.update', { id, ...data });
62
- }
63
- async completeAction(id) {
64
- return this.updateAction(id, { status: 'COMPLETED' });
65
- }
66
- // === Goals (OKRs) ===
67
- async getGoals(workspaceId) {
68
- return this.trpcQuery('goal.list', { workspaceId });
69
- }
70
- async getGoal(id) {
71
- return this.trpcQuery('goal.get', { id });
72
- }
73
- // === Quick Actions ===
74
- async quickCreateAction(text) {
75
- // Uses natural language parsing (dates, project names, etc.)
76
- return this.trpcMutation('action.quickCreate', { text });
77
- }
78
- // === Search ===
79
- async search(query) {
80
- return this.trpcQuery('search.global', { query });
81
- }
82
- }