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 +32 -10
- package/dist/api-client.d.ts +26 -6
- package/dist/api-client.js +217 -100
- package/dist/auth/auth-service.d.ts +19 -0
- package/dist/auth/auth-service.js +148 -0
- package/dist/auth/credentials.d.ts +18 -0
- package/dist/auth/credentials.js +30 -0
- package/dist/auth/permissions.d.ts +18 -0
- package/dist/auth/permissions.js +68 -0
- package/dist/auth/token-store.d.ts +12 -0
- package/dist/auth/token-store.js +189 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +15 -0
- package/dist/index.js +195 -72
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,14 +16,36 @@ Or install globally:
|
|
|
16
16
|
npm install -g viviscape-mcp
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
##
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
|
package/dist/api-client.d.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
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>;
|
package/dist/api-client.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
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 ??
|
|
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:
|
|
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 ??
|
|
153
|
+
account_id: data.account_id ?? ctx.accountId,
|
|
145
154
|
service_id: data.service_id ?? 0,
|
|
146
|
-
user_id: data.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
|
-
|
|
183
|
-
|
|
184
|
-
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
185
|
-
|
|
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
|
-
|
|
223
|
+
requireFeature(featureForPath(path), this.creds);
|
|
224
|
+
const url = `${this.baseUrl}${API_PREFIX}/${path.replace(/^\//, '')}`;
|
|
189
225
|
const headers = {
|
|
190
|
-
'
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
288
|
+
return this.get(`prospect/id/${prospectId}`);
|
|
218
289
|
}
|
|
219
290
|
async getProspectsByRep(repId) {
|
|
220
|
-
return this.
|
|
291
|
+
return this.get(`prospects/rep/${repId}/${this.accountId}`);
|
|
221
292
|
}
|
|
222
293
|
async getProspectsByAccount() {
|
|
223
|
-
return this.
|
|
294
|
+
return this.get(`prospects/account/${this.accountId}?rep_id=0`);
|
|
224
295
|
}
|
|
225
296
|
async queryProspects(query, accountId, userId, stages) {
|
|
226
|
-
return this.
|
|
227
|
-
query
|
|
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.
|
|
305
|
+
return this.get(`prospect/remove/${prospectId}`);
|
|
232
306
|
}
|
|
233
|
-
// ── Prospect
|
|
307
|
+
// ── Prospect notes ─────────────────────────────────────────
|
|
234
308
|
async addProspectNote(data) {
|
|
235
|
-
return this.
|
|
309
|
+
return this.post('prospect/note/add', { user_id: this.userId, ...data });
|
|
236
310
|
}
|
|
237
311
|
async updateProspectNote(data) {
|
|
238
|
-
return this.
|
|
312
|
+
return this.post('prospect/note/update', data);
|
|
239
313
|
}
|
|
240
314
|
async getProspectNotes(prospectId) {
|
|
241
|
-
return this.
|
|
315
|
+
return this.get(`prospect/notes/${prospectId}`);
|
|
242
316
|
}
|
|
243
317
|
async removeProspectNote(noteId) {
|
|
244
|
-
return this.
|
|
318
|
+
return this.get(`prospect/note/remove/${noteId}`);
|
|
245
319
|
}
|
|
246
|
-
// ── Prospect
|
|
320
|
+
// ── Prospect follow-ups ────────────────────────────────────
|
|
247
321
|
async addFollowup(data) {
|
|
248
|
-
return this.
|
|
322
|
+
return this.post('prospect/followup/add', { user_id: this.userId, ...data });
|
|
249
323
|
}
|
|
250
324
|
async updateFollowup(data) {
|
|
251
|
-
return this.
|
|
325
|
+
return this.post('prospect/followup/update', data);
|
|
252
326
|
}
|
|
253
327
|
async getFollowups(prospectId) {
|
|
254
|
-
return this.
|
|
328
|
+
return this.get(`prospect/followups/list/${prospectId}`);
|
|
255
329
|
}
|
|
256
|
-
// ── Companies
|
|
330
|
+
// ── Companies & clients ────────────────────────────────────
|
|
257
331
|
async listCompanies() {
|
|
258
|
-
return this.
|
|
332
|
+
return this.get(`user/accounts?user_id=${this.userId}&account_id=${this.accountId}`);
|
|
259
333
|
}
|
|
260
334
|
async addCompany(data) {
|
|
261
|
-
return this.
|
|
335
|
+
return this.post('account/add', { account_id: this.accountId, ...data });
|
|
262
336
|
}
|
|
263
337
|
async updateCompany(data) {
|
|
264
|
-
return this.
|
|
338
|
+
return this.post('account/update', { account_id: this.accountId, ...data });
|
|
265
339
|
}
|
|
266
340
|
async getClientsByCompany(companyId) {
|
|
267
|
-
return this.
|
|
341
|
+
return this.get(`clients/company/list/${companyId}`);
|
|
268
342
|
}
|
|
269
343
|
async addClient(data) {
|
|
270
|
-
return this.
|
|
344
|
+
return this.post('client/add', data);
|
|
271
345
|
}
|
|
272
346
|
async getClientById(clientId) {
|
|
273
|
-
return this.
|
|
347
|
+
return this.get(`client/id/${clientId}`);
|
|
274
348
|
}
|
|
275
349
|
async getClientByEmail(data) {
|
|
276
|
-
return this.
|
|
350
|
+
return this.post('client/email', data);
|
|
277
351
|
}
|
|
278
|
-
// ── Projects
|
|
352
|
+
// ── Projects (groups) ──────────────────────────────────────
|
|
279
353
|
async getMyProjects() {
|
|
280
|
-
return this.
|
|
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.
|
|
284
|
-
|
|
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.
|
|
363
|
+
return this.get(`group/id/${projectId}`);
|
|
288
364
|
}
|
|
289
365
|
async getProjectStaff(projectId) {
|
|
290
|
-
return this.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
383
|
+
return this.post('tasks/update', buildTaskUpdatePayload(data));
|
|
303
384
|
}
|
|
304
385
|
async getProjectTasks(projectId) {
|
|
305
|
-
return this.
|
|
386
|
+
return this.get(`tasks/group?group_id=${projectId}`);
|
|
306
387
|
}
|
|
307
388
|
async getOpenTasks() {
|
|
308
|
-
return this.
|
|
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.
|
|
400
|
+
return this.get(`tasks/get?task_id=${taskId}`);
|
|
312
401
|
}
|
|
313
402
|
async getPendingTasks(userId) {
|
|
314
|
-
return this.
|
|
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.
|
|
406
|
+
return this.get(`tasks/milestones?milestone_id=${milestoneId}`);
|
|
318
407
|
}
|
|
319
408
|
async getTasksByGroupAndUser(groupId, userId) {
|
|
320
|
-
return this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
423
|
+
return this.get('notes/me');
|
|
345
424
|
}
|
|
346
425
|
async getNoteById(noteId) {
|
|
347
|
-
return this.
|
|
426
|
+
return this.get(`note/id/${noteId}`);
|
|
348
427
|
}
|
|
349
428
|
async addNote(data) {
|
|
350
|
-
return this.
|
|
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.
|
|
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.
|
|
440
|
+
return this.get(`note/remove/${noteId}`);
|
|
357
441
|
}
|
|
358
442
|
async queryNotes(data) {
|
|
359
|
-
return this.
|
|
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.
|
|
456
|
+
return this.get(`account/id/${this.accountId}`);
|
|
364
457
|
}
|
|
365
458
|
async getActiveServices() {
|
|
366
|
-
return this.
|
|
459
|
+
return this.get(`services/${this.accountId}/active`);
|
|
367
460
|
}
|
|
368
461
|
async getAllServices() {
|
|
369
|
-
return this.
|
|
462
|
+
return this.get(`services/${this.accountId}`);
|
|
370
463
|
}
|
|
371
464
|
async getUsers() {
|
|
372
|
-
return this.
|
|
465
|
+
return this.get(`account/users/${this.accountId}`);
|
|
373
466
|
}
|
|
374
467
|
// ── Insights ───────────────────────────────────────────────
|
|
375
468
|
async getHoursByPerson(data) {
|
|
376
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
513
|
+
return this.get(`insights/eval/person/week/${userId || this.userId}/${this.accountId}`);
|
|
401
514
|
}
|
|
402
515
|
async getPersonHoursThisMonth(userId) {
|
|
403
|
-
return this.
|
|
516
|
+
return this.get(`insights/eval/person/month/${userId || this.userId}/${this.accountId}`);
|
|
404
517
|
}
|
|
405
518
|
async getPersonHoursLastMonth(userId) {
|
|
406
|
-
return this.
|
|
519
|
+
return this.get(`insights/eval/person/lastmonth/${userId || this.userId}/${this.accountId}`);
|
|
407
520
|
}
|
|
408
521
|
async getProjectTimeTotal(projectId) {
|
|
409
|
-
return this.
|
|
522
|
+
return this.get(`insights/eval/project/all/${projectId}/${this.accountId}`);
|
|
410
523
|
}
|
|
411
524
|
async getAllUsersTime() {
|
|
412
|
-
return this.
|
|
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
|
+
}
|