@idea-base/mcp-server 2.1.0 → 2.3.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 +19 -3
- package/package.json +1 -1
- package/src/index.js +206 -12
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Ask Claude to manage your projects:
|
|
|
66
66
|
| `list_projects` | List all projects with task counts and progress |
|
|
67
67
|
| `get_project` | Get project details and statistics |
|
|
68
68
|
| `create_project` | Create a new project or sub-project |
|
|
69
|
-
| `update_project` | Update project name, description, or
|
|
69
|
+
| `update_project` | Update a project name, description, status, parent (re-parent, or `null` to detach) or product |
|
|
70
70
|
|
|
71
71
|
### Tasks
|
|
72
72
|
|
|
@@ -85,8 +85,24 @@ Ask Claude to manage your projects:
|
|
|
85
85
|
| Tool | Description |
|
|
86
86
|
|------|-------------|
|
|
87
87
|
| `log_time` | Log time against a task with notes |
|
|
88
|
-
| `start_working` |
|
|
89
|
-
| `
|
|
88
|
+
| `start_working` | Open a timed work session on a task, and mark yourself actively working (surfaces the saved resume context + recent work notes so a cold session re-orients). Calling it twice never opens a second session — it resumes or reports the one you have |
|
|
89
|
+
| `pause_working` | Pause the open session without ending it, with a required `reason` |
|
|
90
|
+
| `resume_working` | Close the pause and carry on in the same session |
|
|
91
|
+
| `stop_working` | Close the session with a UTC end instant and drop the active flag (optionally capture a `note` and/or `resume_context` on the way out). Fails if you have no open session |
|
|
92
|
+
|
|
93
|
+
**Sessions are measured, and the pause reason decides the number.** `start_working`
|
|
94
|
+
records a UTC start instant, `stop_working` a UTC end instant, and each pause in
|
|
95
|
+
between records both its boundaries plus why work stopped:
|
|
96
|
+
|
|
97
|
+
| `reason` | Effect on worked time |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `waiting_on_human` | **Excluded.** A person has to act before you can continue |
|
|
100
|
+
| `waiting_on_agent` | **Counted.** A sub-agent blocked on another *active* sub-agent is still working |
|
|
101
|
+
| `other` | Counted |
|
|
102
|
+
|
|
103
|
+
Only waiting on the human is not working. Use `pause_working` rather than
|
|
104
|
+
`stop_working` whenever you intend to carry on — `stop_working` ends the session
|
|
105
|
+
and the reason is lost.
|
|
90
106
|
|
|
91
107
|
### Activity & Audit Trail
|
|
92
108
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -93,6 +93,18 @@ function redact(text) {
|
|
|
93
93
|
const MAX_STRING_LEN = 20000;
|
|
94
94
|
const STRING_LIMITS = { query: 500, date: 32, start_date: 32, due_date: 32 };
|
|
95
95
|
|
|
96
|
+
// A schema type may be a UNION, e.g. ['number', 'null'] — the only way to say
|
|
97
|
+
// "a number, or null meaning clear this". Both the null handling in
|
|
98
|
+
// validateArgs and coerce's branch selection have to read that union rather
|
|
99
|
+
// than compare `spec.type` with ===, or a nullable field silently behaves as a
|
|
100
|
+
// string field and its null never reaches the handler at all.
|
|
101
|
+
const typesOf = (spec) => {
|
|
102
|
+
const t = spec?.type;
|
|
103
|
+
return Array.isArray(t) ? t : (t ? [t] : []);
|
|
104
|
+
};
|
|
105
|
+
const allowsNull = (spec) => typesOf(spec).includes('null');
|
|
106
|
+
const primaryType = (spec) => typesOf(spec).find((t) => t !== 'null');
|
|
107
|
+
|
|
96
108
|
function validateArgs(toolName, schema, rawArgs) {
|
|
97
109
|
const args = rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs) ? rawArgs : {};
|
|
98
110
|
const props = schema?.properties || {};
|
|
@@ -107,7 +119,16 @@ function validateArgs(toolName, schema, rawArgs) {
|
|
|
107
119
|
for (const [name, value] of Object.entries(args)) {
|
|
108
120
|
const spec = props[name];
|
|
109
121
|
if (!spec) continue; // drop anything the tool does not declare
|
|
110
|
-
if (value === undefined
|
|
122
|
+
if (value === undefined) continue;
|
|
123
|
+
if (value === null) {
|
|
124
|
+
// null is dropped unless the tool DECLARES it meaningful. For
|
|
125
|
+
// update_project.parent_project_id it means "detach this project", and
|
|
126
|
+
// this unconditional drop is why un-parenting stayed impossible through
|
|
127
|
+
// this server: the field reached here and vanished, so the request body
|
|
128
|
+
// came out empty and the API answered "No fields to update".
|
|
129
|
+
if (allowsNull(spec)) clean[name] = null;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
111
132
|
clean[name] = coerce(toolName, name, spec, value);
|
|
112
133
|
}
|
|
113
134
|
|
|
@@ -116,8 +137,9 @@ function validateArgs(toolName, schema, rawArgs) {
|
|
|
116
137
|
|
|
117
138
|
function coerce(toolName, name, spec, value) {
|
|
118
139
|
const label = `${toolName}: "${name}"`;
|
|
140
|
+
const type = primaryType(spec);
|
|
119
141
|
|
|
120
|
-
if (
|
|
142
|
+
if (type === 'number' || type === 'integer') {
|
|
121
143
|
const n = typeof value === 'number' ? value : Number(String(value).trim());
|
|
122
144
|
if (!Number.isFinite(n)) throw new Error(`${label} must be a number`);
|
|
123
145
|
if (name === 'id' || name.endsWith('_id')) {
|
|
@@ -128,9 +150,9 @@ function coerce(toolName, name, spec, value) {
|
|
|
128
150
|
return n;
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
if (
|
|
153
|
+
if (type === 'boolean') return Boolean(value);
|
|
132
154
|
|
|
133
|
-
if (
|
|
155
|
+
if (type === 'array') {
|
|
134
156
|
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
135
157
|
if (value.length > 500) throw new Error(`${label} has too many entries (max 500)`);
|
|
136
158
|
return value.map((item, i) => coerce(toolName, `${name}[${i}]`, spec.items || {}, item));
|
|
@@ -226,18 +248,33 @@ async function apiRequest(endpoint, options = {}) {
|
|
|
226
248
|
|
|
227
249
|
if (!response.ok) {
|
|
228
250
|
let message = `API error: ${response.status}`;
|
|
251
|
+
let code = null;
|
|
229
252
|
const contentType = (response.headers.get('content-type') || '');
|
|
230
253
|
if (contentType.includes('json')) {
|
|
231
254
|
try {
|
|
232
255
|
const body = JSON.parse(text);
|
|
233
256
|
if (typeof body?.error === 'string' && body.error) {
|
|
234
|
-
|
|
257
|
+
// `error` is the headline and `details` is what to do about it — a
|
|
258
|
+
// plan-limit or product-inheritance refusal is not actionable without
|
|
259
|
+
// the second half, which used to be dropped here.
|
|
260
|
+
const parts = [body.error];
|
|
261
|
+
if (typeof body?.details === 'string' && body.details) parts.push(body.details);
|
|
262
|
+
message = parts.join(' — ').slice(0, MAX_ERROR_CHARS);
|
|
235
263
|
}
|
|
264
|
+
// Machine-readable discriminator, when the endpoint supplies one. A
|
|
265
|
+
// handler needs to tell "you have no open session" (a real answer the
|
|
266
|
+
// caller must see) apart from "that endpoint is not deployed yet"
|
|
267
|
+
// (an infrastructure fact it should degrade around), and string-matching
|
|
268
|
+
// a prose error message to decide that is how quiet bugs get written.
|
|
269
|
+
if (typeof body?.code === 'string' && body.code) code = body.code;
|
|
236
270
|
} catch {
|
|
237
271
|
// fall through to the generic status message
|
|
238
272
|
}
|
|
239
273
|
}
|
|
240
|
-
|
|
274
|
+
const error = new Error(redact(message));
|
|
275
|
+
error.status = response.status;
|
|
276
|
+
if (code) error.code = code;
|
|
277
|
+
throw error;
|
|
241
278
|
}
|
|
242
279
|
|
|
243
280
|
return parseJsonBody(text, response);
|
|
@@ -317,7 +354,7 @@ const tools = [
|
|
|
317
354
|
},
|
|
318
355
|
{
|
|
319
356
|
name: 'update_project',
|
|
320
|
-
description: 'Update project
|
|
357
|
+
description: 'Update a project: its name, description, status, or where it sits in the hierarchy. Omitted fields are left exactly as they are. Set parent_project_id to re-parent the project, or to null to DETACH it into a top-level project; a detached project keeps the product it had inherited, so product_id is only needed to move it to a DIFFERENT product. A sub-project inherits its parent\'s product, so re-parenting under a parent in a different product moves the project there too. The API refuses a move that is circular, crosses accounts, or exceeds the plan\'s nesting depth or sub-project count, and says which.',
|
|
321
358
|
inputSchema: {
|
|
322
359
|
type: 'object',
|
|
323
360
|
properties: {
|
|
@@ -338,6 +375,14 @@ const tools = [
|
|
|
338
375
|
enum: ['active', 'completed', 'archived'],
|
|
339
376
|
description: 'New status for the project',
|
|
340
377
|
},
|
|
378
|
+
parent_project_id: {
|
|
379
|
+
type: ['number', 'null'],
|
|
380
|
+
description: 'Move the project under this parent. Pass null to detach it and make it top-level — it keeps whatever product it had, so no product_id is needed unless you also want to move it. Omit to leave the parent unchanged.',
|
|
381
|
+
},
|
|
382
|
+
product_id: {
|
|
383
|
+
type: 'number',
|
|
384
|
+
description: 'Move the project to this product. Only valid for a project with no parent — a sub-project cannot hold a product of its own, it inherits its parent\'s. Find ids via list_products.',
|
|
385
|
+
},
|
|
341
386
|
},
|
|
342
387
|
required: ['project_id'],
|
|
343
388
|
},
|
|
@@ -750,7 +795,7 @@ const tools = [
|
|
|
750
795
|
},
|
|
751
796
|
{
|
|
752
797
|
name: 'start_working',
|
|
753
|
-
description: '
|
|
798
|
+
description: 'Start work on a task. Two things happen: you are shown to the team as actively working on it (and a todo task moves to in_progress), and a TIMED WORK SESSION is opened with a UTC start instant, so how long the task was actually worked becomes measurable instead of guessed.\n\nCalling it a second time on the same task NEVER opens a second session. If your session is paused it is resumed; if it is already running you are told so, with how long it has been going. The response always says which of the three happened.\n\nIt also orients a cold session: the response carries the task\'s resume context and its recent work notes, so read the whole thing before you start. Pair it with stop_working, and use pause_working when you have to wait on something.',
|
|
754
799
|
inputSchema: {
|
|
755
800
|
type: 'object',
|
|
756
801
|
properties: {
|
|
@@ -764,7 +809,7 @@ const tools = [
|
|
|
764
809
|
},
|
|
765
810
|
{
|
|
766
811
|
name: 'stop_working',
|
|
767
|
-
description: '
|
|
812
|
+
description: 'Stop working on a task: close your open work session with a UTC end instant, and drop the "actively working" flag (you stay assigned). The closed session is what makes the task\'s worked duration reportable.\n\nRequires an open session — if you have none this FAILS and writes nothing, rather than inventing a session with no start. Call start_working first.\n\nThis ENDS the session. If you are only waiting on something and intend to carry on afterwards, use pause_working instead: a pause keeps the session open and records why you stopped, which is what decides whether the interval counts as worked time.\n\nOptionally capture your state on the way out: pass `note` to append a work note and/or `resume_context` to overwrite the resume block, so the next session can pick up where you left off.',
|
|
768
813
|
inputSchema: {
|
|
769
814
|
type: 'object',
|
|
770
815
|
properties: {
|
|
@@ -784,6 +829,43 @@ const tools = [
|
|
|
784
829
|
required: ['task_id'],
|
|
785
830
|
},
|
|
786
831
|
},
|
|
832
|
+
{
|
|
833
|
+
name: 'pause_working',
|
|
834
|
+
description: 'Pause your open work session without ending it, and record WHY. Use this whenever work stops but is not finished — you asked the human a question, you are waiting on another agent, you are blocked on a build.\n\nTHE REASON IS THE POINT, and it decides the number:\n waiting_on_human — you are blocked on a person and CANNOT proceed. The paused interval is EXCLUDED from worked time.\n waiting_on_agent — you are blocked on another agent or an automated run that is itself active. The interval IS COUNTED as worked time: a sub-agent blocked on another active sub-agent is still working.\n other — anything else. Counted as worked.\n\nOnly waiting on the human is not working. Choose waiting_on_human ONLY when a person has to act before you can continue; if a machine or another agent is doing the work, it is waiting_on_agent.\n\nResume with resume_working. Do NOT use stop_working for a pause — that ends the session and the reason is lost.',
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: 'object',
|
|
837
|
+
properties: {
|
|
838
|
+
task_id: {
|
|
839
|
+
type: 'number',
|
|
840
|
+
description: 'The ID of the task whose session to pause',
|
|
841
|
+
},
|
|
842
|
+
reason: {
|
|
843
|
+
type: 'string',
|
|
844
|
+
enum: ['waiting_on_human', 'waiting_on_agent', 'other'],
|
|
845
|
+
description: 'Why work stopped. waiting_on_human EXCLUDES this interval from worked time; waiting_on_agent and other INCLUDE it. Required — there is no default, because the whole worked-time figure turns on this value.',
|
|
846
|
+
},
|
|
847
|
+
detail: {
|
|
848
|
+
type: 'string',
|
|
849
|
+
description: 'Optional free text: what specifically you are waiting on (e.g. "asked whether to merge PR #280").',
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
required: ['task_id', 'reason'],
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: 'resume_working',
|
|
857
|
+
description: 'Resume your paused work session — closes the pause by recording the UTC instant you came back, so the paused interval has both boundaries and can be measured. The session itself was never closed and keeps its original start.\n\nFails if the session is not paused, rather than pretending to resume something. start_working also resumes a paused session, so use whichever reads better; this one does not re-fetch the orientation block.',
|
|
858
|
+
inputSchema: {
|
|
859
|
+
type: 'object',
|
|
860
|
+
properties: {
|
|
861
|
+
task_id: {
|
|
862
|
+
type: 'number',
|
|
863
|
+
description: 'The ID of the task whose session to resume',
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
required: ['task_id'],
|
|
867
|
+
},
|
|
868
|
+
},
|
|
787
869
|
{
|
|
788
870
|
name: 'add_work_note',
|
|
789
871
|
description: 'Append a timestamped work note (status update) to a task\'s activity log. Use this to record progress so a future session can pick up where you left off (e.g. "finished auth handler, tests green, next: wire the callback"). Notes are append-only and never overwrite each other.',
|
|
@@ -965,11 +1047,18 @@ const toolHandlers = {
|
|
|
965
1047
|
};
|
|
966
1048
|
},
|
|
967
1049
|
|
|
968
|
-
async update_project({ project_id, name, description, status }) {
|
|
1050
|
+
async update_project({ project_id, name, description, status, parent_project_id, product_id }) {
|
|
969
1051
|
const updates = {};
|
|
970
1052
|
if (name !== undefined) updates.name = name;
|
|
971
1053
|
if (description !== undefined) updates.description = description;
|
|
972
1054
|
if (status !== undefined) updates.status = status;
|
|
1055
|
+
// These two are forwarded on `!== undefined`, never on truthiness: null is
|
|
1056
|
+
// the MEANINGFUL value for parent_project_id (it detaches the project), and
|
|
1057
|
+
// PUT /api/projects/:id only writes a column it was actually sent — so an
|
|
1058
|
+
// omitted key leaves parent and product exactly as they were, and a
|
|
1059
|
+
// name-only update can never silently detach anything.
|
|
1060
|
+
if (parent_project_id !== undefined) updates.parent_project_id = parent_project_id;
|
|
1061
|
+
if (product_id !== undefined) updates.product_id = product_id;
|
|
973
1062
|
|
|
974
1063
|
const project = await apiRequest(`/projects/${project_id}`, {
|
|
975
1064
|
method: 'PUT',
|
|
@@ -1293,11 +1382,36 @@ const toolHandlers = {
|
|
|
1293
1382
|
},
|
|
1294
1383
|
|
|
1295
1384
|
async start_working({ task_id }) {
|
|
1385
|
+
// Presence first, and unchanged: this is what writes the `started_working`
|
|
1386
|
+
// task_events row and promotes a todo task to in_progress. The timed session
|
|
1387
|
+
// below sits ALONGSIDE that audit trail, it does not replace it.
|
|
1296
1388
|
await apiRequest(`/tasks/${task_id}/assignments`, {
|
|
1297
1389
|
method: 'POST',
|
|
1298
1390
|
body: JSON.stringify({ is_active: true }),
|
|
1299
1391
|
});
|
|
1300
1392
|
|
|
1393
|
+
// Open (or resume, or report) the work session. Non-fatal by design: the
|
|
1394
|
+
// presence write already succeeded, and a session failure must not make
|
|
1395
|
+
// start_working look like it did nothing.
|
|
1396
|
+
let timing = '';
|
|
1397
|
+
try {
|
|
1398
|
+
const result = await apiRequest(`/tasks/${task_id}/work-session`, {
|
|
1399
|
+
method: 'POST',
|
|
1400
|
+
body: JSON.stringify({}),
|
|
1401
|
+
});
|
|
1402
|
+
const s = result.session || {};
|
|
1403
|
+
if (result.action === 'resumed') {
|
|
1404
|
+
const p = result.resumed_pause || {};
|
|
1405
|
+
timing = `\n\nResumed work session #${s.id}, open since ${s.started_at} — the ${p.reason} pause that began at ${p.paused_at} is now closed. No second session was opened.`;
|
|
1406
|
+
} else if (result.action === 'already_running') {
|
|
1407
|
+
timing = `\n\nWork session #${s.id} was ALREADY running (started ${s.started_at}, ${s.worked_minutes} min worked so far). No second session was opened.`;
|
|
1408
|
+
} else {
|
|
1409
|
+
timing = `\n\nWork session #${s.id} opened at ${s.started_at} (UTC). Close it with stop_working; if you have to wait on something, use pause_working so the reason is recorded.`;
|
|
1410
|
+
}
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
timing = `\n\n(Work session NOT recorded: ${error.message} — your presence flag is set, but no duration will be measured for this stretch.)`;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1301
1415
|
// Orient a cold-starting session: surface the resume context + recent work notes.
|
|
1302
1416
|
let orientation = '';
|
|
1303
1417
|
try {
|
|
@@ -1330,7 +1444,7 @@ const toolHandlers = {
|
|
|
1330
1444
|
content: [
|
|
1331
1445
|
{
|
|
1332
1446
|
type: 'text',
|
|
1333
|
-
text: `Started working on task #${task_id}. Other team members can now see you are actively working on this task.${orientation}`,
|
|
1447
|
+
text: `Started working on task #${task_id}. Other team members can now see you are actively working on this task.${timing}${orientation}`,
|
|
1334
1448
|
},
|
|
1335
1449
|
],
|
|
1336
1450
|
};
|
|
@@ -1353,6 +1467,42 @@ const toolHandlers = {
|
|
|
1353
1467
|
captured.push('resume context');
|
|
1354
1468
|
}
|
|
1355
1469
|
|
|
1470
|
+
// Close the session BEFORE dropping presence, so a task with no open
|
|
1471
|
+
// session fails here and nothing at all is changed.
|
|
1472
|
+
//
|
|
1473
|
+
// A missing session is a real answer, not an infrastructure hiccup: it means
|
|
1474
|
+
// the caller believes it was timing work that was never timed. Report it and
|
|
1475
|
+
// stop, rather than writing a session row with no start or silently clearing
|
|
1476
|
+
// the flag as if a duration had been recorded. Any OTHER session failure
|
|
1477
|
+
// (the endpoint being unreachable, say) must not strand the presence flag,
|
|
1478
|
+
// so that one is reported and the stop continues.
|
|
1479
|
+
let timing = '';
|
|
1480
|
+
let sessionError = null;
|
|
1481
|
+
try {
|
|
1482
|
+
const result = await apiRequest(`/tasks/${task_id}/work-session`, {
|
|
1483
|
+
method: 'DELETE',
|
|
1484
|
+
body: JSON.stringify({ state: 'completed' }),
|
|
1485
|
+
});
|
|
1486
|
+
const s = result.session || {};
|
|
1487
|
+
const paused = Number(s.paused_minutes || 0);
|
|
1488
|
+
const pausedText = paused > 0
|
|
1489
|
+
? ` (${s.elapsed_minutes} min elapsed, ${paused} min of that paused)`
|
|
1490
|
+
: '';
|
|
1491
|
+
timing = ` Work session #${s.id} closed at ${s.ended_at} — ${s.worked_minutes} minutes worked${pausedText}.`;
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
sessionError = error;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (sessionError?.code === 'NO_OPEN_SESSION') {
|
|
1497
|
+
throw new Error(
|
|
1498
|
+
`${sessionError.message} Nothing was changed: no session row was written and ` +
|
|
1499
|
+
'your active-work flag was left as it was.'
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
if (sessionError) {
|
|
1503
|
+
timing = ` (Work session NOT closed: ${sessionError.message})`;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1356
1506
|
await apiRequest(`/tasks/${task_id}/assignments?stop_only=true`, {
|
|
1357
1507
|
method: 'DELETE',
|
|
1358
1508
|
});
|
|
@@ -1362,7 +1512,51 @@ const toolHandlers = {
|
|
|
1362
1512
|
content: [
|
|
1363
1513
|
{
|
|
1364
1514
|
type: 'text',
|
|
1365
|
-
text: `Stopped working on task #${task_id}. You are still assigned to the task but no longer marked as actively working.${capturedText}`,
|
|
1515
|
+
text: `Stopped working on task #${task_id}. You are still assigned to the task but no longer marked as actively working.${timing}${capturedText}`,
|
|
1516
|
+
},
|
|
1517
|
+
],
|
|
1518
|
+
};
|
|
1519
|
+
},
|
|
1520
|
+
|
|
1521
|
+
async pause_working({ task_id, reason, detail }) {
|
|
1522
|
+
const result = await apiRequest(`/tasks/${task_id}/work-session/pause`, {
|
|
1523
|
+
method: 'POST',
|
|
1524
|
+
body: JSON.stringify({ reason, note: detail }),
|
|
1525
|
+
});
|
|
1526
|
+
const p = result.pause || {};
|
|
1527
|
+
const s = result.session || {};
|
|
1528
|
+
const counted = result.counts_as_worked
|
|
1529
|
+
? 'This interval WILL count as worked time.'
|
|
1530
|
+
: 'This interval will NOT count as worked time.';
|
|
1531
|
+
return {
|
|
1532
|
+
content: [
|
|
1533
|
+
{
|
|
1534
|
+
type: 'text',
|
|
1535
|
+
text:
|
|
1536
|
+
`Paused work session #${s.id} on task #${task_id} at ${p.paused_at} (reason: ${reason}). ` +
|
|
1537
|
+
`${counted} ${s.worked_minutes} minutes worked so far. ` +
|
|
1538
|
+
'The session is still open — call resume_working when you are back.',
|
|
1539
|
+
},
|
|
1540
|
+
],
|
|
1541
|
+
};
|
|
1542
|
+
},
|
|
1543
|
+
|
|
1544
|
+
async resume_working({ task_id }) {
|
|
1545
|
+
const result = await apiRequest(`/tasks/${task_id}/work-session/resume`, {
|
|
1546
|
+
method: 'POST',
|
|
1547
|
+
body: JSON.stringify({}),
|
|
1548
|
+
});
|
|
1549
|
+
const p = result.resumed_pause || {};
|
|
1550
|
+
const s = result.session || {};
|
|
1551
|
+
return {
|
|
1552
|
+
content: [
|
|
1553
|
+
{
|
|
1554
|
+
type: 'text',
|
|
1555
|
+
text:
|
|
1556
|
+
`Resumed work session #${s.id} on task #${task_id} at ${p.resumed_at}. ` +
|
|
1557
|
+
`The ${p.reason} pause ran ${p.minutes} minutes and ` +
|
|
1558
|
+
`${p.counts_as_worked ? 'COUNTS as worked time' : 'is EXCLUDED from worked time'}. ` +
|
|
1559
|
+
`${s.worked_minutes} minutes worked on this session so far.`,
|
|
1366
1560
|
},
|
|
1367
1561
|
],
|
|
1368
1562
|
};
|