viviscape-mcp 1.0.1 → 2.0.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
@@ -16,14 +16,36 @@ Or install globally:
16
16
  npm install -g viviscape-mcp
17
17
  ```
18
18
 
19
- ## Configuration
19
+ ## Sign in
20
+
21
+ Authentication is per-user, the same browser flow the ViviScape CLI uses — no
22
+ API keys. Sign in once:
23
+
24
+ ```bash
25
+ npx viviscape-mcp login
26
+ ```
27
+
28
+ A browser tab opens on ViviScape Work; confirm **Allow access** and the session
29
+ is stored in Windows Credential Manager (or `~/.viviscape/mcp-credentials.json`
30
+ on macOS/Linux). Every tool call then runs as *you*: your user id, account,
31
+ role, and plan come from the session, so tools see exactly the data you can see
32
+ in the app.
20
33
 
21
- Set the following environment variables:
34
+ ```bash
35
+ npx viviscape-mcp status # who am I, which account, role, plan
36
+ npx viviscape-mcp logout # clear the stored session
37
+ ```
38
+
39
+ If you already ran `vs auth login` with the [ViviScape CLI](https://viviscape.io),
40
+ that session is picked up automatically. Inside an MCP client you can also call
41
+ the `auth_login`, `auth_status`, and `auth_logout` tools. Sessions expire; when
42
+ one does, tools report it and you re-run `login`.
43
+
44
+ ## Configuration
22
45
 
23
46
  | Variable | Required | Default | Description |
24
47
  |----------|----------|---------|-------------|
25
- | `VIVISCAPE_API_KEY` | yes | | Your ViviScape API key |
26
- | `VIVISCAPE_API_URL` | no | `https://api.viviscape.io` | API base URL |
48
+ | `VIVISCAPE_BASE_URL` | no | `https://work.viviscape.io` | ViviScape Work base URL |
27
49
 
28
50
  ## Claude Desktop / Claude Code
29
51
 
@@ -34,10 +56,7 @@ Add to your MCP client config (e.g. `claude_desktop_config.json` or `.mcp.json`)
34
56
  "mcpServers": {
35
57
  "viviscape": {
36
58
  "command": "npx",
37
- "args": ["-y", "viviscape-mcp"],
38
- "env": {
39
- "VIVISCAPE_API_KEY": "your-api-key-here"
40
- }
59
+ "args": ["-y", "viviscape-mcp"]
41
60
  }
42
61
  }
43
62
  }
@@ -51,7 +70,6 @@ Codex accepts stdio MCP servers directly. Add to `~/.codex/config.toml`:
51
70
  [mcp_servers.viviscape]
52
71
  command = "npx"
53
72
  args = ["-y", "viviscape-mcp"]
54
- env = { VIVISCAPE_API_KEY = "your-api-key-here" }
55
73
  ```
56
74
 
57
75
  ## ChatGPT (Developer Mode / Apps SDK)
@@ -65,10 +83,13 @@ publicly (Cloudflare Tunnel, ngrok, or deploy to a host).
65
83
  Example with `supergateway`:
66
84
 
67
85
  ```bash
68
- VIVISCAPE_API_KEY=your-api-key-here \
86
+ npx viviscape-mcp login # once, on the host running the gateway
69
87
  npx -y supergateway --stdio "npx -y viviscape-mcp" --port 8000
70
88
  ```
71
89
 
90
+ The gateway shares the host's stored session, so everyone reaching that endpoint
91
+ acts as the signed-in user — put your own auth in front of it.
92
+
72
93
  Expose `http://localhost:8000/sse` publicly, then in ChatGPT:
73
94
 
74
95
  **Settings → Connectors → Create → Custom MCP server**
@@ -88,6 +109,7 @@ The server exposes tools across these domains:
88
109
  - **Notes** — `note_add`, `note_get`, `note_update`, `note_remove`, `notes_mine`, `notes_query`
89
110
  - **Insights** — hours by person/service/project, AI summary, person stats, time totals
90
111
  - **Account** — `account_info`, `account_services`, `account_users`
112
+ - **Auth** — `auth_login`, `auth_status`, `auth_logout`
91
113
 
92
114
  ## Development
93
115
 
@@ -1,8 +1,28 @@
1
+ import { Credentials } from './auth/credentials.js';
2
+ /** Raised when the stored session is rejected or bounced to the login page. */
3
+ export declare class SessionExpiredError extends Error {
4
+ constructor();
5
+ }
6
+ export declare class ApiError extends Error {
7
+ status: number;
8
+ constructor(status: number, message: string);
9
+ }
10
+ /**
11
+ * Client for the ViviScape Work core API. Authenticates exactly like the
12
+ * ViviScape CLI: the signed-in user's bearer token plus the user_id / pid
13
+ * context headers the core API falls back to.
14
+ */
1
15
  export declare class ViviScapeClient {
16
+ private creds;
2
17
  private baseUrl;
3
- private apiKey;
4
- constructor(baseUrl: string, apiKey: string);
18
+ constructor(creds: Credentials);
19
+ get userId(): number;
20
+ get accountId(): number;
21
+ get session(): Credentials;
22
+ private get ctx();
5
23
  private request;
24
+ private get;
25
+ private post;
6
26
  addProspect(data: Record<string, unknown>): Promise<unknown>;
7
27
  updateProspect(data: Record<string, unknown>): Promise<unknown>;
8
28
  getProspect(prospectId: number): Promise<unknown>;
@@ -30,6 +50,8 @@ export declare class ViviScapeClient {
30
50
  getActiveProjects(): Promise<Record<string, unknown>[]>;
31
51
  getProjectById(projectId: number): Promise<unknown>;
32
52
  getProjectStaff(projectId: number): Promise<unknown>;
53
+ getProjectsByCompany(userId: number, companyId: number): Promise<unknown>;
54
+ getActiveProjectsByUser(userId: number): Promise<unknown>;
33
55
  addTask(data: Record<string, unknown>): Promise<unknown>;
34
56
  updateTask(data: Record<string, unknown>): Promise<unknown>;
35
57
  getProjectTasks(projectId: number): Promise<unknown>;
@@ -39,15 +61,13 @@ export declare class ViviScapeClient {
39
61
  getTasksByMilestone(milestoneId: number): Promise<unknown>;
40
62
  getTasksByGroupAndUser(groupId: number, userId: number): Promise<unknown>;
41
63
  getCompanyTasks(companyId: number): Promise<unknown>;
42
- getProjectsByCompany(userId: number, companyId: number): Promise<unknown>;
43
- getActiveProjectsByUser(userId: number): Promise<unknown>;
44
64
  addTimeLog(data: Record<string, unknown>): Promise<unknown>;
45
65
  updateTimeLog(data: Record<string, unknown>): Promise<unknown>;
46
66
  getMyNotes(): Promise<unknown>;
47
- getNoteById(noteId: number): Promise<unknown>;
67
+ getNoteById(noteId: number | string): Promise<unknown>;
48
68
  addNote(data: Record<string, unknown>): Promise<unknown>;
49
69
  updateNote(data: Record<string, unknown>): Promise<unknown>;
50
- removeNote(noteId: number): Promise<unknown>;
70
+ removeNote(noteId: number | string): Promise<unknown>;
51
71
  queryNotes(data: Record<string, unknown>): Promise<unknown>;
52
72
  getAccountInfo(): Promise<unknown>;
53
73
  getActiveServices(): Promise<unknown>;
@@ -1,4 +1,18 @@
1
- const DEFAULT_USER_ID = 210; // Art Hicks
1
+ import { API_PREFIX, baseUrl } from './config.js';
2
+ import { featureForPath, requireFeature } from './auth/permissions.js';
3
+ /** Raised when the stored session is rejected or bounced to the login page. */
4
+ export class SessionExpiredError extends Error {
5
+ constructor() {
6
+ super('Your ViviScape session has expired. Run `viviscape-mcp login` to sign in again.');
7
+ }
8
+ }
9
+ export class ApiError extends Error {
10
+ status;
11
+ constructor(status, message) {
12
+ super(message);
13
+ this.status = status;
14
+ }
15
+ }
2
16
  /**
3
17
  * Parse shorthand duration (e.g., "3h", "1h30m", "45m") to HH:MM:SS TimeSpan format.
4
18
  */
@@ -19,10 +33,10 @@ function parseShorthandDuration(input) {
19
33
  /**
20
34
  * Build a full Group_Task payload from simplified MCP tool params.
21
35
  */
22
- function buildTaskPayload(data) {
36
+ function buildTaskPayload(data, ctx) {
23
37
  const now = new Date().toISOString();
24
38
  const assignees = [];
25
- const userId = data.assigned_to ?? DEFAULT_USER_ID;
39
+ const userId = data.assigned_to ?? ctx.userId;
26
40
  assignees.push({
27
41
  assignee_id: 0,
28
42
  task_id: 0,
@@ -70,7 +84,7 @@ function buildTaskPayload(data) {
70
84
  milestone_id: data.milestone_id ?? 0,
71
85
  client_id: 0,
72
86
  event_id: 0,
73
- account_id: 0,
87
+ account_id: ctx.accountId,
74
88
  percentage: 0,
75
89
  attention: false,
76
90
  pinned: false,
@@ -80,11 +94,6 @@ function buildTaskPayload(data) {
80
94
  predecessor_id: 0,
81
95
  predecessor_complete: false,
82
96
  predecessor_name: '',
83
- solution: '',
84
- meta: '',
85
- agent_id: '00000000-0000-0000-0000-000000000000',
86
- unit_id: '00000000-0000-0000-0000-000000000000',
87
- reviewer_id: 0,
88
97
  review_required: false,
89
98
  parts: '',
90
99
  staff_assignees: assignees,
@@ -135,15 +144,15 @@ function buildTaskUpdatePayload(data) {
135
144
  /**
136
145
  * Build a full LOG_Time payload from simplified MCP tool params.
137
146
  */
138
- function buildTimeLogPayload(data) {
147
+ function buildTimeLogPayload(data, ctx) {
139
148
  const now = new Date().toISOString();
140
149
  const durInput = String(data.duration || '0h');
141
150
  const { duration, log_time } = parseShorthandDuration(durInput);
142
151
  return {
143
152
  log_id: data.log_id ?? 0,
144
- account_id: data.account_id ?? 1,
153
+ account_id: data.account_id ?? ctx.accountId,
145
154
  service_id: data.service_id ?? 0,
146
- user_id: data.user_id ?? DEFAULT_USER_ID,
155
+ user_id: data.user_id ?? ctx.userId,
147
156
  company_id: data.company_id ?? 0,
148
157
  client_id: data.client_id ?? 0,
149
158
  group_id: data.group_id ?? 0,
@@ -177,28 +186,74 @@ function buildTimeLogPayload(data) {
177
186
  staff: [],
178
187
  };
179
188
  }
189
+ const EPOCH = '2000-01-01T00:00:00';
190
+ function isoDate(value, fallback) {
191
+ if (typeof value === 'string' && value.trim())
192
+ return value;
193
+ return fallback;
194
+ }
195
+ function nowIso() {
196
+ return new Date().toISOString();
197
+ }
198
+ /**
199
+ * Client for the ViviScape Work core API. Authenticates exactly like the
200
+ * ViviScape CLI: the signed-in user's bearer token plus the user_id / pid
201
+ * context headers the core API falls back to.
202
+ */
180
203
  export class ViviScapeClient {
204
+ creds;
181
205
  baseUrl;
182
- apiKey;
183
- constructor(baseUrl, apiKey) {
184
- this.baseUrl = baseUrl.replace(/\/$/, '');
185
- this.apiKey = apiKey;
206
+ constructor(creds) {
207
+ this.creds = creds;
208
+ this.baseUrl = (creds.base_url || baseUrl()).replace(/\/$/, '');
209
+ }
210
+ get userId() {
211
+ return this.creds.user_id;
212
+ }
213
+ get accountId() {
214
+ return this.creds.pid;
215
+ }
216
+ get session() {
217
+ return this.creds;
218
+ }
219
+ get ctx() {
220
+ return { userId: this.creds.user_id, accountId: this.creds.pid };
186
221
  }
187
222
  async request(method, path, body) {
188
- const url = `${this.baseUrl}${path}`;
223
+ requireFeature(featureForPath(path), this.creds);
224
+ const url = `${this.baseUrl}${API_PREFIX}/${path.replace(/^\//, '')}`;
189
225
  const headers = {
190
- 'ApiKey': this.apiKey,
226
+ 'Authorization': `Bearer ${this.creds.access_token}`,
227
+ 'user_id': String(this.creds.user_id),
228
+ 'pid': String(this.creds.pid),
191
229
  'Content-Type': 'application/json',
192
230
  };
193
231
  const res = await fetch(url, {
194
232
  method,
195
233
  headers,
196
- body: body ? JSON.stringify(body) : undefined,
234
+ body: method === 'GET' ? undefined : JSON.stringify(body ?? {}),
235
+ // A rejected bearer token is answered with a 302 to /login; following it
236
+ // would just fetch the marketing page.
237
+ redirect: 'manual',
197
238
  });
198
239
  const text = await res.text();
240
+ if (res.status === 401 || res.status === 403)
241
+ throw new SessionExpiredError();
242
+ if (res.status >= 300 && res.status < 400)
243
+ throw new SessionExpiredError();
199
244
  if (!res.ok) {
200
- throw new Error(`API ${method} ${path} returned ${res.status}: ${text}`);
245
+ const snippet = text.length > 400 ? `${text.slice(0, 400)}…` : text;
246
+ throw new ApiError(res.status, `API ${method} ${path} returned ${res.status}: ${snippet}`);
247
+ }
248
+ // Cookie-auth middleware bounces unauthenticated API calls to /login, which
249
+ // answers with marketing HTML and a 200. Treat that as an expired session
250
+ // instead of handing back a page body.
251
+ const contentType = res.headers.get('content-type') || '';
252
+ if (contentType.toLowerCase().includes('text/html') || looksLikeHtml(text)) {
253
+ throw new SessionExpiredError();
201
254
  }
255
+ if (!text.trim())
256
+ return null;
202
257
  try {
203
258
  return JSON.parse(text);
204
259
  }
@@ -206,209 +261,271 @@ export class ViviScapeClient {
206
261
  return text;
207
262
  }
208
263
  }
264
+ get(path) {
265
+ return this.request('GET', path);
266
+ }
267
+ post(path, body) {
268
+ return this.request('POST', path, body);
269
+ }
209
270
  // ── Prospects ──────────────────────────────────────────────
210
271
  async addProspect(data) {
211
- return this.request('POST', '/api/v1/crm/prospect/add', data);
272
+ return this.post('prospect/add', {
273
+ ...data,
274
+ account_id: data.account_id ?? this.accountId,
275
+ platform_account_id: data.platform_account_id ?? this.accountId,
276
+ user_id: data.user_id ?? this.userId,
277
+ status: data.status ?? 'new',
278
+ });
212
279
  }
213
280
  async updateProspect(data) {
214
- return this.request('POST', '/api/v1/crm/prospect/update', data);
281
+ return this.post('prospect/update', {
282
+ ...data,
283
+ account_id: data.account_id ?? this.accountId,
284
+ platform_account_id: data.platform_account_id ?? this.accountId,
285
+ });
215
286
  }
216
287
  async getProspect(prospectId) {
217
- return this.request('GET', `/api/v1/crm/prospect/id/${prospectId}`);
288
+ return this.get(`prospect/id/${prospectId}`);
218
289
  }
219
290
  async getProspectsByRep(repId) {
220
- return this.request('GET', `/api/v1/crm/prospects/rep/${repId}`);
291
+ return this.get(`prospects/rep/${repId}/${this.accountId}`);
221
292
  }
222
293
  async getProspectsByAccount() {
223
- return this.request('GET', '/api/v1/crm/prospects/account');
294
+ return this.get(`prospects/account/${this.accountId}?rep_id=0`);
224
295
  }
225
296
  async queryProspects(query, accountId, userId, stages) {
226
- return this.request('POST', '/api/v1/crm/prospects/query', {
227
- query, account_id: accountId, user_id: userId, stages,
297
+ return this.post('prospects/lookup', {
298
+ query: query ?? '',
299
+ account_id: accountId ?? this.accountId,
300
+ user_id: userId ?? this.userId,
301
+ stages: stages ?? [],
228
302
  });
229
303
  }
230
304
  async removeProspect(prospectId) {
231
- return this.request('GET', `/api/v1/crm/prospect/remove/${prospectId}`);
305
+ return this.get(`prospect/remove/${prospectId}`);
232
306
  }
233
- // ── Prospect Notes ─────────────────────────────────────────
307
+ // ── Prospect notes ─────────────────────────────────────────
234
308
  async addProspectNote(data) {
235
- return this.request('POST', '/api/v1/crm/prospect/note/add', data);
309
+ return this.post('prospect/note/add', { user_id: this.userId, ...data });
236
310
  }
237
311
  async updateProspectNote(data) {
238
- return this.request('POST', '/api/v1/crm/prospect/note/update', data);
312
+ return this.post('prospect/note/update', data);
239
313
  }
240
314
  async getProspectNotes(prospectId) {
241
- return this.request('GET', `/api/v1/crm/prospect/notes/${prospectId}`);
315
+ return this.get(`prospect/notes/${prospectId}`);
242
316
  }
243
317
  async removeProspectNote(noteId) {
244
- return this.request('GET', `/api/v1/crm/prospect/note/remove/${noteId}`);
318
+ return this.get(`prospect/note/remove/${noteId}`);
245
319
  }
246
- // ── Prospect Followups ─────────────────────────────────────
320
+ // ── Prospect follow-ups ────────────────────────────────────
247
321
  async addFollowup(data) {
248
- return this.request('POST', '/api/v1/crm/prospect/followup/add', data);
322
+ return this.post('prospect/followup/add', { user_id: this.userId, ...data });
249
323
  }
250
324
  async updateFollowup(data) {
251
- return this.request('POST', '/api/v1/crm/prospect/followup/update', data);
325
+ return this.post('prospect/followup/update', data);
252
326
  }
253
327
  async getFollowups(prospectId) {
254
- return this.request('GET', `/api/v1/crm/prospect/followups/list/${prospectId}`);
328
+ return this.get(`prospect/followups/list/${prospectId}`);
255
329
  }
256
- // ── Companies ──────────────────────────────────────────────
330
+ // ── Companies & clients ────────────────────────────────────
257
331
  async listCompanies() {
258
- return this.request('GET', '/api/v1/companies/list');
332
+ return this.get(`user/accounts?user_id=${this.userId}&account_id=${this.accountId}`);
259
333
  }
260
334
  async addCompany(data) {
261
- return this.request('POST', '/api/v1/companies/add', data);
335
+ return this.post('account/add', { account_id: this.accountId, ...data });
262
336
  }
263
337
  async updateCompany(data) {
264
- return this.request('POST', '/api/v1/companies/update', data);
338
+ return this.post('account/update', { account_id: this.accountId, ...data });
265
339
  }
266
340
  async getClientsByCompany(companyId) {
267
- return this.request('GET', `/api/v1/companies/clients/company/${companyId}`);
341
+ return this.get(`clients/company/list/${companyId}`);
268
342
  }
269
343
  async addClient(data) {
270
- return this.request('POST', '/api/v1/companies/client/add', data);
344
+ return this.post('client/add', data);
271
345
  }
272
346
  async getClientById(clientId) {
273
- return this.request('GET', `/api/v1/companies/client/id/${clientId}`);
347
+ return this.get(`client/id/${clientId}`);
274
348
  }
275
349
  async getClientByEmail(data) {
276
- return this.request('POST', '/api/v1/companies/client/email', data);
350
+ return this.post('client/email', data);
277
351
  }
278
- // ── Projects ───────────────────────────────────────────────
352
+ // ── Projects (groups) ──────────────────────────────────────
279
353
  async getMyProjects() {
280
- return this.request('GET', '/api/v1/projects/user/projects');
354
+ return this.get(`user/groups/company?user_id=${this.userId}&account_id=${this.accountId}&companyid=0`);
281
355
  }
282
356
  async getActiveProjects() {
283
- const projects = await this.request('GET', '/api/v1/projects/user/projects');
284
- return projects.filter(p => p.bitActive === true);
357
+ const projects = await this.getMyProjects();
358
+ if (!Array.isArray(projects))
359
+ return projects;
360
+ return projects.filter((p) => p.bitActive === true || String(p.status ?? '').toLowerCase() === 'active');
285
361
  }
286
362
  async getProjectById(projectId) {
287
- return this.request('GET', `/api/v1/projects/id?project_id=${projectId}`);
363
+ return this.get(`group/id/${projectId}`);
288
364
  }
289
365
  async getProjectStaff(projectId) {
290
- return this.request('GET', `/api/v1/projects/project/staff?project_id=${projectId}`);
366
+ return this.get(`groups/group/staff?group_id=${projectId}`);
367
+ }
368
+ async getProjectsByCompany(userId, companyId) {
369
+ return this.get(`user/groups/company?user_id=${userId || this.userId}&account_id=${this.accountId}&companyid=${companyId}`);
291
370
  }
371
+ async getActiveProjectsByUser(userId) {
372
+ return this.get(`user/groups/active?user_id=${userId || this.userId}&account_id=${this.accountId}`);
373
+ }
374
+ // ── Tasks ──────────────────────────────────────────────────
292
375
  async addTask(data) {
293
- const payload = buildTaskPayload(data);
294
- return this.request('POST', '/api/v1/projects/task/add', payload);
376
+ return this.post('tasks/add', buildTaskPayload(data, this.ctx));
295
377
  }
296
378
  async updateTask(data) {
297
379
  if (data.task_id) {
298
- const existing = await this.getTask(data.task_id);
299
- const payload = { ...existing, ...buildTaskUpdatePayload(data) };
300
- return this.request('POST', '/api/v1/projects/task/update', payload);
380
+ const existing = await this.getTask(Number(data.task_id));
381
+ return this.post('tasks/update', { ...existing, ...buildTaskUpdatePayload(data) });
301
382
  }
302
- return this.request('POST', '/api/v1/projects/task/update', buildTaskUpdatePayload(data));
383
+ return this.post('tasks/update', buildTaskUpdatePayload(data));
303
384
  }
304
385
  async getProjectTasks(projectId) {
305
- return this.request('GET', `/api/v1/projects/tasks/project?project_id=${projectId}`);
386
+ return this.get(`tasks/group?group_id=${projectId}`);
306
387
  }
307
388
  async getOpenTasks() {
308
- return this.request('POST', '/api/v1/projects/account/tasks/open', {});
389
+ return this.post('account/tasks/open', {
390
+ account_id: this.accountId,
391
+ user_id: this.userId,
392
+ start: EPOCH,
393
+ end: nowIso(),
394
+ company_id: 0,
395
+ teamfilter: '',
396
+ onlymytasks: false,
397
+ });
309
398
  }
310
399
  async getTask(taskId) {
311
- return this.request('GET', `/api/v1/projects/task/get?task_id=${taskId}`);
400
+ return this.get(`tasks/get?task_id=${taskId}`);
312
401
  }
313
402
  async getPendingTasks(userId) {
314
- return this.request('GET', `/api/v1/projects/user/tasks/pending?user_id=${userId}`);
403
+ return this.get(`user/tasks/pending?user_id=${userId || this.userId}&account_id=${this.accountId}`);
315
404
  }
316
405
  async getTasksByMilestone(milestoneId) {
317
- return this.request('GET', `/api/v1/projects/tasks/milestones?milestone_id=${milestoneId}`);
406
+ return this.get(`tasks/milestones?milestone_id=${milestoneId}`);
318
407
  }
319
408
  async getTasksByGroupAndUser(groupId, userId) {
320
- return this.request('GET', `/api/v1/projects/tasks/group/user?group_id=${groupId}&user_id=${userId}`);
409
+ return this.get(`tasks/group/user?group_id=${groupId}&user_id=${userId || this.userId}`);
321
410
  }
322
411
  async getCompanyTasks(companyId) {
323
- return this.request('GET', `/api/v1/projects/company/tasks?company_id=${companyId}`);
324
- }
325
- async getProjectsByCompany(userId, companyId) {
326
- return this.request('GET', `/api/v1/projects/user/projects/company?user_id=${userId}&companyid=${companyId}`);
327
- }
328
- async getActiveProjectsByUser(userId) {
329
- return this.request('GET', `/api/v1/projects/user/projects/active?user_id=${userId}`);
412
+ return this.get(`company/tasks?company_id=${companyId}&user_id=${this.userId}`);
330
413
  }
414
+ // ── Time logs ──────────────────────────────────────────────
331
415
  async addTimeLog(data) {
332
- const payload = buildTimeLogPayload(data);
333
- return this.request('POST', '/api/v1/projects/task/logs/add', payload);
416
+ return this.post('logs/add', buildTimeLogPayload(data, this.ctx));
334
417
  }
335
418
  async updateTimeLog(data) {
336
- if (data.log_id) {
337
- const payload = buildTimeLogPayload(data);
338
- return this.request('POST', '/api/v1/projects/task/logs/update', payload);
339
- }
340
- return this.request('POST', '/api/v1/projects/task/logs/update', buildTimeLogPayload(data));
419
+ return this.post('logs/update', buildTimeLogPayload(data, this.ctx));
341
420
  }
342
421
  // ── Notes ──────────────────────────────────────────────────
343
422
  async getMyNotes() {
344
- return this.request('GET', '/api/v1/notes/me');
423
+ return this.get('notes/me');
345
424
  }
346
425
  async getNoteById(noteId) {
347
- return this.request('GET', `/api/v1/notes/id/${noteId}`);
426
+ return this.get(`note/id/${noteId}`);
348
427
  }
349
428
  async addNote(data) {
350
- return this.request('POST', '/api/v1/notes/add', data);
429
+ return this.post('note/add', {
430
+ account_id: this.accountId,
431
+ user_id: this.userId,
432
+ creator_id: this.userId,
433
+ ...data,
434
+ });
351
435
  }
352
436
  async updateNote(data) {
353
- return this.request('POST', '/api/v1/notes/update', data);
437
+ return this.post('note/update', { account_id: this.accountId, user_id: this.userId, ...data });
354
438
  }
355
439
  async removeNote(noteId) {
356
- return this.request('GET', `/api/v1/notes/remove/${noteId}`);
440
+ return this.get(`note/remove/${noteId}`);
357
441
  }
358
442
  async queryNotes(data) {
359
- return this.request('POST', '/api/v1/notes/query', data);
443
+ return this.post('notes/query', {
444
+ query: data.query ?? '',
445
+ account_id: this.accountId,
446
+ user_id: this.userId,
447
+ company_id: data.company_id ?? 0,
448
+ use_notes: true,
449
+ use_tasks: false,
450
+ use_knowledge: true,
451
+ module: 'notes',
452
+ });
360
453
  }
361
454
  // ── Account ────────────────────────────────────────────────
362
455
  async getAccountInfo() {
363
- return this.request('GET', '/api/v1/account/info');
456
+ return this.get(`account/id/${this.accountId}`);
364
457
  }
365
458
  async getActiveServices() {
366
- return this.request('GET', '/api/v1/account/services/active');
459
+ return this.get(`services/${this.accountId}/active`);
367
460
  }
368
461
  async getAllServices() {
369
- return this.request('GET', '/api/v1/account/services/list');
462
+ return this.get(`services/${this.accountId}`);
370
463
  }
371
464
  async getUsers() {
372
- return this.request('GET', '/api/v1/account/users');
465
+ return this.get(`account/users/${this.accountId}`);
373
466
  }
374
467
  // ── Insights ───────────────────────────────────────────────
375
468
  async getHoursByPerson(data) {
376
- return this.request('POST', '/api/v1/insights/logs/hours/person', data);
469
+ const userId = Number(data.user_id) || this.userId;
470
+ return this.get(`insights/logs/hours/person/${userId}?account_id=${this.accountId}` +
471
+ `&start=${encodeURIComponent(isoDate(data.start_date, EPOCH))}` +
472
+ `&end=${encodeURIComponent(isoDate(data.end_date, nowIso()))}`);
377
473
  }
378
474
  async getTimeLogsByDateRange(data) {
379
- return this.request('POST', '/api/v1/insights/logs/account/daterange', data);
475
+ return this.get(`timelog/account?account_id=${this.accountId}` +
476
+ `&start=${encodeURIComponent(isoDate(data.start_date, EPOCH))}` +
477
+ `&end=${encodeURIComponent(isoDate(data.end_date, nowIso()))}`);
380
478
  }
381
479
  async getPersonStats(data) {
382
- return this.request('POST', '/api/v1/insights/person/stats', data);
480
+ const userId = Number(data.user_id) || this.userId;
481
+ return this.get(`insights/person/stats/${userId}/${this.accountId}`);
383
482
  }
384
483
  async getHoursByPersonAndService(data) {
385
- return this.request('POST', '/api/v1/insights/logs/hours/person/service', data);
484
+ const userId = Number(data.user_id) || this.userId;
485
+ return this.get(`insights/logs/hours/person/service/${userId}?account_id=${this.accountId}` +
486
+ `&start=${encodeURIComponent(isoDate(data.start_date, EPOCH))}` +
487
+ `&end=${encodeURIComponent(isoDate(data.end_date, nowIso()))}`);
386
488
  }
387
489
  async getAllTimeHoursByPersonAndService(data) {
388
- return this.request('POST', '/api/v1/insights/logs/hours/alltime/person/service', data);
490
+ const userId = Number(data.user_id) || this.userId;
491
+ return this.get(`insights/logs/hours/alltime/person/service/${userId}/${this.accountId}`);
389
492
  }
390
493
  async getProjectHoursByService(data) {
391
- return this.request('POST', '/api/v1/insights/logs/project/hours/service', data);
494
+ return this.get(`insights/logs/project/hours/service/${this.userId}?account_id=${this.accountId}` +
495
+ `&start=${encodeURIComponent(isoDate(data.start_date, EPOCH))}` +
496
+ `&end=${encodeURIComponent(isoDate(data.end_date, nowIso()))}`);
392
497
  }
393
498
  async getAccountHoursByService(data) {
394
- return this.request('POST', '/api/v1/insights/logs/account/hours/service', data);
499
+ return this.get(`insights/logs/account/hours/service/${this.userId}?account_id=${this.accountId}` +
500
+ `&start=${encodeURIComponent(isoDate(data.start_date, EPOCH))}` +
501
+ `&end=${encodeURIComponent(isoDate(data.end_date, nowIso()))}`);
395
502
  }
396
503
  async getAiLogSummary(data) {
397
- return this.request('POST', '/api/v1/insights/ai/log/summary', data);
504
+ return this.post('ai/insights/logs/account/hours/services', {
505
+ user_id: Number(data.user_id) || this.userId,
506
+ account_id: this.accountId,
507
+ start: isoDate(data.start_date, EPOCH),
508
+ end: isoDate(data.end_date, nowIso()),
509
+ query: data.query ?? '',
510
+ });
398
511
  }
399
512
  async getPersonHoursThisWeek(userId) {
400
- return this.request('GET', `/api/v1/insights/insights/eval/person/week/${userId}`);
513
+ return this.get(`insights/eval/person/week/${userId || this.userId}/${this.accountId}`);
401
514
  }
402
515
  async getPersonHoursThisMonth(userId) {
403
- return this.request('GET', `/api/v1/insights/eval/person/month/${userId}`);
516
+ return this.get(`insights/eval/person/month/${userId || this.userId}/${this.accountId}`);
404
517
  }
405
518
  async getPersonHoursLastMonth(userId) {
406
- return this.request('GET', `/api/v1/insights/eval/person/lastmonth/${userId}`);
519
+ return this.get(`insights/eval/person/lastmonth/${userId || this.userId}/${this.accountId}`);
407
520
  }
408
521
  async getProjectTimeTotal(projectId) {
409
- return this.request('GET', `/api/v1/insights/eval/project/all/${projectId}`);
522
+ return this.get(`insights/eval/project/all/${projectId}/${this.accountId}`);
410
523
  }
411
524
  async getAllUsersTime() {
412
- return this.request('GET', '/api/v1/insights/eval/users/all');
525
+ return this.get(`insights/eval/users/all/${this.userId}/${this.accountId}`);
413
526
  }
414
527
  }
528
+ function looksLikeHtml(text) {
529
+ const t = text.trimStart().toLowerCase();
530
+ return t.startsWith('<!doctype') || t.startsWith('<html');
531
+ }