tascan-mcp 3.5.0 → 3.7.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/package.json +1 -1
- package/tools.cjs +255 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"mcpName": "io.github.snowbikemike/tascan-mcp",
|
|
5
5
|
"description": "TaScan MCP Server — Closed-loop autonomous operations protocol. 50 tools for projects, tasks, workers, QR codes, NFC tags, geofenced zones, AI condition assessment, worker passports, verification-gated gig payments, cross-entity search, duplicate-worker detection, AI issue analysis, and autonomous remediation dispatch. 14 patents filed (435+ claims). Task. Scan. Done.",
|
|
6
6
|
"type": "module",
|
package/tools.cjs
CHANGED
|
@@ -252,8 +252,25 @@ const TOOLS = [
|
|
|
252
252
|
title: { type: 'string', description: 'Task title' },
|
|
253
253
|
description: { type: 'string', description: 'Task description' },
|
|
254
254
|
response_type: { type: 'string', enum: ['checkbox', 'photo', 'text', 'number', 'date', 'choice'], description: 'CRITICAL: "text" for names, phones, emails, notes, addresses, any free-form input. "photo" for tasks needing photographic proof (inspections, serial numbers, packed cases). "checkbox" ONLY for simple yes/no confirmations. "number" for numeric values. "date" for dates. "choice" for multiple-choice (needs response_config.options). Most info-collection tasks should be "text", most verification tasks should be "photo".' },
|
|
255
|
+
response_config: { type: 'object', description: 'Response configuration. For "choice": {options: ["A","B","C"]}. For "text": {placeholder, multiline}. For "number": {placeholder}. For "date": {label, include_text}.' },
|
|
255
256
|
is_safety_checkpoint: { type: 'boolean', description: 'Safety-critical task flag' },
|
|
256
|
-
requires_photo: { type: 'boolean', description: 'Require photo on completion' }
|
|
257
|
+
requires_photo: { type: 'boolean', description: 'Require photo on completion' },
|
|
258
|
+
sort_order: { type: 'number', description: 'Explicit sort position (defaults to end of list, in array order)' },
|
|
259
|
+
required_equipment: { type: 'string', description: 'Equipment this task depends on (free text, e.g. "88 lb kettlebells")' },
|
|
260
|
+
subtasks: {
|
|
261
|
+
type: 'array',
|
|
262
|
+
items: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
title: { type: 'string', description: 'Subtask title (e.g. "Set 1")' },
|
|
266
|
+
response_type: { type: 'string', enum: ['checkbox', 'number', 'text', 'choice'], description: 'Typed subtask response. "number" for per-set values (reps/weight), "text" for notes, "choice" needs response_config.options. Default checkbox.' },
|
|
267
|
+
response_config: { type: 'object', description: 'Same shape as task response_config' },
|
|
268
|
+
requires_photo: { type: 'boolean' }
|
|
269
|
+
},
|
|
270
|
+
required: ['title']
|
|
271
|
+
},
|
|
272
|
+
description: 'Subtasks created with the task. Example set logging: [{title:"Set 1",response_type:"number"},{title:"Set 2",response_type:"number"},{title:"Set 3",response_type:"number"}]'
|
|
273
|
+
}
|
|
257
274
|
},
|
|
258
275
|
required: ['title']
|
|
259
276
|
},
|
|
@@ -386,9 +403,11 @@ const TOOLS = [
|
|
|
386
403
|
title: { type: 'string', description: 'New title' },
|
|
387
404
|
description: { type: 'string', description: 'New description' },
|
|
388
405
|
response_type: { type: 'string', enum: ['checkbox', 'photo', 'text', 'number', 'date', 'choice'], description: 'See tascan_add_tasks for guidance. "text" for info collection, "photo" for visual proof, "checkbox" for yes/no only.' },
|
|
406
|
+
response_config: { type: 'object', description: 'Response configuration. For "choice": {options: [...]}. See tascan_add_tasks.' },
|
|
389
407
|
is_safety_checkpoint: { type: 'boolean', description: 'Safety-critical flag' },
|
|
390
408
|
requires_photo: { type: 'boolean', description: 'Require photo' },
|
|
391
|
-
sort_order: { type: 'number', description: 'Sort position' }
|
|
409
|
+
sort_order: { type: 'number', description: 'Sort position' },
|
|
410
|
+
required_equipment: { type: 'string', description: 'Equipment this task depends on (free text)' }
|
|
392
411
|
},
|
|
393
412
|
required: ['task_id']
|
|
394
413
|
},
|
|
@@ -400,6 +419,157 @@ const TOOLS = [
|
|
|
400
419
|
return `Task updated: ${result.data.title}\n\n${JSON.stringify(result.data, null, 2)}`;
|
|
401
420
|
}
|
|
402
421
|
},
|
|
422
|
+
{
|
|
423
|
+
name: 'tascan_reply_with_list',
|
|
424
|
+
description: 'Reply to a task list WITH a task list — the two-way tasking primitive. Creates a new list linked into the parent\'s thread, aimed back at whoever sent the original (e.g. "Grant access — pick a window" with response_type date, or an info request with response_type text). The org gets pinged; the thread shows in both the worker portal and Simple Mode. Use tascan_get_thread-style follow-up via tascan_list_projects/tascan_get_report to read answers.',
|
|
425
|
+
inputSchema: {
|
|
426
|
+
type: 'object',
|
|
427
|
+
properties: {
|
|
428
|
+
parent_list_id: { type: 'string', description: 'The list being replied to' },
|
|
429
|
+
title: { type: 'string', description: 'Reply list title (e.g. "Before I can start...")' },
|
|
430
|
+
author_name: { type: 'string', description: 'Who is replying (shown in the thread)' },
|
|
431
|
+
tasks: {
|
|
432
|
+
type: 'array',
|
|
433
|
+
items: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
title: { type: 'string' },
|
|
437
|
+
description: { type: 'string' },
|
|
438
|
+
response_type: { type: 'string', enum: ['checkbox', 'text', 'number', 'date', 'choice', 'photo'], description: '"date" for scheduling windows, "text" for info requests, "choice" needs response_config.options' },
|
|
439
|
+
response_config: { type: 'object' }
|
|
440
|
+
},
|
|
441
|
+
required: ['title']
|
|
442
|
+
},
|
|
443
|
+
description: 'Items the recipient answers — each becomes a typed task'
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
required: ['parent_list_id', 'title', 'tasks']
|
|
447
|
+
},
|
|
448
|
+
annotations: { title: 'Reply With a List', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
449
|
+
handler: async (args) => {
|
|
450
|
+
const resp = await fetch('https://app.tascan.io/.netlify/functions/list-reply', {
|
|
451
|
+
method: 'POST',
|
|
452
|
+
headers: { 'Content-Type': 'application/json' },
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
parent_list_id: args.parent_list_id,
|
|
455
|
+
title: args.title,
|
|
456
|
+
author_name: args.author_name,
|
|
457
|
+
tasks: args.tasks
|
|
458
|
+
})
|
|
459
|
+
});
|
|
460
|
+
const data = await resp.json();
|
|
461
|
+
if (!resp.ok) throw new Error(data.error || 'Reply failed');
|
|
462
|
+
return `Reply list created and linked into the thread!\n List: ${args.title} (${data.list_id})\n Thread: ${data.thread_id}\n Tasks: ${data.task_count}\n Worker link: ${data.worker_url}\nThe org has been pinged via app_messages.`;
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: 'tascan_add_subtasks',
|
|
467
|
+
description: 'Add one or more subtasks to a task (bulk). Subtasks support typed responses: "number" for per-set data (reps, weight, distance), "text" for notes, "choice" for options, "checkbox" for simple steps. Set-logging example: task "Bench Press" with subtasks Set 1/Set 2/Set 3 each response_type "number" — each completed set stores its value and timestamp, giving per-set timing for progression tracking.',
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: 'object',
|
|
470
|
+
properties: {
|
|
471
|
+
task_id: { type: 'string', description: 'Parent task ID' },
|
|
472
|
+
subtasks: {
|
|
473
|
+
type: 'array',
|
|
474
|
+
items: {
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
title: { type: 'string', description: 'Subtask title (e.g. "Set 1")' },
|
|
478
|
+
description: { type: 'string', description: 'Optional detail' },
|
|
479
|
+
response_type: { type: 'string', enum: ['checkbox', 'number', 'text', 'choice'], description: 'Default checkbox. "number" for per-set values. "choice" needs response_config.options.' },
|
|
480
|
+
response_config: { type: 'object', description: 'For "choice": {options: [...]}. For "number"/"text": {placeholder}.' },
|
|
481
|
+
requires_photo: { type: 'boolean', description: 'Require photo on completion' },
|
|
482
|
+
sort_order: { type: 'number', description: 'Explicit position (defaults to end, in array order)' }
|
|
483
|
+
},
|
|
484
|
+
required: ['title']
|
|
485
|
+
},
|
|
486
|
+
description: 'Array of subtasks to create'
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
required: ['task_id', 'subtasks']
|
|
490
|
+
},
|
|
491
|
+
annotations: { title: 'Add Subtasks', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
492
|
+
handler: async (args, api) => {
|
|
493
|
+
const result = await api('POST', `/tasks/${args.task_id}/subtasks`, args.subtasks);
|
|
494
|
+
return `${result.count} subtask(s) created on task ${args.task_id}\n\n${JSON.stringify(result.data, null, 2)}`;
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: 'tascan_list_subtasks',
|
|
499
|
+
description: 'List the subtasks of a task, including completion state, stored response values, and completion timestamps (per-set timing).',
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: { task_id: { type: 'string', description: 'Parent task ID' } },
|
|
503
|
+
required: ['task_id']
|
|
504
|
+
},
|
|
505
|
+
annotations: { title: 'List Subtasks', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
506
|
+
handler: async (args, api) => {
|
|
507
|
+
const result = await api('GET', `/tasks/${args.task_id}/subtasks`);
|
|
508
|
+
return JSON.stringify(result.data, null, 2);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: 'tascan_update_subtask',
|
|
513
|
+
description: 'Update a subtask (title, description, response_type, response_config, requires_photo, sort_order).',
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
subtask_id: { type: 'string', description: 'Subtask ID' },
|
|
518
|
+
title: { type: 'string' },
|
|
519
|
+
description: { type: 'string' },
|
|
520
|
+
response_type: { type: 'string', enum: ['checkbox', 'number', 'text', 'choice'] },
|
|
521
|
+
response_config: { type: 'object' },
|
|
522
|
+
requires_photo: { type: 'boolean' },
|
|
523
|
+
sort_order: { type: 'number' }
|
|
524
|
+
},
|
|
525
|
+
required: ['subtask_id']
|
|
526
|
+
},
|
|
527
|
+
annotations: { title: 'Update Subtask', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
528
|
+
handler: async (args, api) => {
|
|
529
|
+
const { subtask_id, ...updates } = args;
|
|
530
|
+
const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
|
|
531
|
+
const result = await api('PUT', `/subtasks/${subtask_id}`, body);
|
|
532
|
+
return `Subtask updated: ${result.data.title}\n\n${JSON.stringify(result.data, null, 2)}`;
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'tascan_complete_subtask',
|
|
537
|
+
description: 'Complete a subtask, optionally recording a typed response_value (e.g. the weight or reps for that set). Each completion is timestamped, so consecutive set completions yield per-set durations. Returns progress including all_subtasks_complete — when true, complete the parent task with tascan_complete_task.',
|
|
538
|
+
inputSchema: {
|
|
539
|
+
type: 'object',
|
|
540
|
+
properties: {
|
|
541
|
+
subtask_id: { type: 'string', description: 'Subtask ID to complete' },
|
|
542
|
+
worker_id: { type: 'string', description: 'Worker performing the completion (optional)' },
|
|
543
|
+
response_value: { type: 'string', description: 'Typed response value (for number/text/choice subtasks), e.g. "165"' },
|
|
544
|
+
notes: { type: 'string', description: 'Optional notes' }
|
|
545
|
+
},
|
|
546
|
+
required: ['subtask_id']
|
|
547
|
+
},
|
|
548
|
+
annotations: { title: 'Complete Subtask', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
549
|
+
handler: async (args, api) => {
|
|
550
|
+
const result = await api('POST', `/subtasks/${args.subtask_id}/complete`, {
|
|
551
|
+
worker_id: args.worker_id,
|
|
552
|
+
response_value: args.response_value,
|
|
553
|
+
notes: args.notes
|
|
554
|
+
});
|
|
555
|
+
const d = result.data;
|
|
556
|
+
return `Subtask completed!\n ${d.subtask_title}${d.response_value ? ' = ' + d.response_value : ''}\n Task: ${d.task_title}\n Progress: ${d.subtasks_done}/${d.subtasks_total}${d.all_subtasks_complete ? '\n ALL SUBTASKS COMPLETE — parent task is ready to complete.' : ''}`;
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: 'tascan_delete_subtask',
|
|
561
|
+
description: 'Delete a subtask and its completions. This action is irreversible.',
|
|
562
|
+
inputSchema: {
|
|
563
|
+
type: 'object',
|
|
564
|
+
properties: { subtask_id: { type: 'string', description: 'Subtask ID to delete' } },
|
|
565
|
+
required: ['subtask_id']
|
|
566
|
+
},
|
|
567
|
+
annotations: { title: 'Delete Subtask', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
568
|
+
handler: async (args, api) => {
|
|
569
|
+
await api('DELETE', `/subtasks/${args.subtask_id}`);
|
|
570
|
+
return `Subtask ${args.subtask_id} deleted successfully.`;
|
|
571
|
+
}
|
|
572
|
+
},
|
|
403
573
|
{
|
|
404
574
|
name: 'tascan_delete_task',
|
|
405
575
|
description: 'Delete a specific task and its completions. This action is irreversible.',
|
|
@@ -571,7 +741,7 @@ const TOOLS = [
|
|
|
571
741
|
},
|
|
572
742
|
{
|
|
573
743
|
name: 'tascan_query_responses',
|
|
574
|
-
description: "Query one task's submitted responses across every list in a project — e.g. the same exercise repeated across many workout lists returns one chronological progression series instead of N report lookups. Match by task title pattern or exact task ID.",
|
|
744
|
+
description: "Query one task's submitted responses across every list in a project — e.g. the same exercise repeated across many workout lists returns one chronological progression series instead of N report lookups. Match by task title pattern or exact task ID. Subtask completions interleave into the same series labeled 'Task › Subtask' (e.g. per-set values Set 1/2/3 with their own timestamps), so set-level progression chains across lists automatically.",
|
|
575
745
|
inputSchema: {
|
|
576
746
|
type: 'object',
|
|
577
747
|
properties: {
|
|
@@ -592,7 +762,8 @@ const TOOLS = [
|
|
|
592
762
|
const { value: v0, note } = fmtResponse(resp.response_value, resp.notes);
|
|
593
763
|
const value = (v0 == null && resp.photo_url) ? '[photo submitted]' : (v0 != null && resp.photo_url) ? v0 + ' [+photo]' : v0;
|
|
594
764
|
const when = (resp.completed_at || '').slice(0, 16).replace('T', ' ');
|
|
595
|
-
|
|
765
|
+
const label = resp.subtask_title ? `${resp.task_title} › ${resp.subtask_title}` : resp.task_title;
|
|
766
|
+
text += `${when} · ${resp.list_name} · ${label}: ${value != null ? value : '(no value)'}`;
|
|
596
767
|
if (note) text += ` — ${note}`;
|
|
597
768
|
text += ` (${resp.worker_name})\n`;
|
|
598
769
|
}
|
|
@@ -896,31 +1067,97 @@ const TOOLS = [
|
|
|
896
1067
|
},
|
|
897
1068
|
{
|
|
898
1069
|
name: 'tascan_get_scan_history',
|
|
899
|
-
description: '
|
|
1070
|
+
description: 'Scan accountability data. Two modes: (1) tag_id — scans of a registered NFC tag; (2) task_list_id or project_id — every QR/link page-open stamp: when the code was scanned, GPS + IP + channel (qr/nfc/sms/email/link), who the scanner turned out to be, and the scan→start delta (how long between scanning and actually identifying + starting work — the sign-in-and-vanish metric).',
|
|
900
1071
|
inputSchema: {
|
|
901
1072
|
type: 'object',
|
|
902
1073
|
properties: {
|
|
903
|
-
tag_id: { type: 'string', description: '
|
|
904
|
-
|
|
1074
|
+
tag_id: { type: 'string', description: 'NFC tag registry ID (from tascan_list_tags) — tag mode' },
|
|
1075
|
+
task_list_id: { type: 'string', description: 'Task list ID — page-scan mode' },
|
|
1076
|
+
project_id: { type: 'string', description: 'Project ID — page-scan mode, all lists in the project' },
|
|
1077
|
+
source: { type: 'string', description: 'Filter page scans by channel: qr | nfc | sms | email | link | unknown' },
|
|
1078
|
+
since: { type: 'string', description: 'ISO timestamp — only scans after this' },
|
|
1079
|
+
limit: { type: 'number', description: 'Max results (default 50, max 200)' }
|
|
905
1080
|
},
|
|
906
|
-
required: [
|
|
1081
|
+
required: []
|
|
907
1082
|
},
|
|
908
1083
|
annotations: { title: 'Get Scan History', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
909
1084
|
handler: async (args, api) => {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1085
|
+
if (args.tag_id) {
|
|
1086
|
+
const path = `/tags/${args.tag_id}/scans` + (args.limit ? `?limit=${args.limit}` : '');
|
|
1087
|
+
const result = await api('GET', path);
|
|
1088
|
+
const scans = result.data;
|
|
1089
|
+
if (!scans.length) return 'No scans recorded for this tag.';
|
|
1090
|
+
let text = `${scans.length} tag scan(s):\n\n`;
|
|
1091
|
+
scans.forEach((s, i) => {
|
|
1092
|
+
text += `${i + 1}. ${s.scanned_at}`;
|
|
1093
|
+
if (s.gps_lat && s.gps_lng) text += ` — GPS: ${s.gps_lat.toFixed(6)}, ${s.gps_lng.toFixed(6)} (±${Math.round(s.gps_accuracy || 0)}m)`;
|
|
1094
|
+
text += '\n';
|
|
1095
|
+
});
|
|
1096
|
+
return text;
|
|
1097
|
+
}
|
|
1098
|
+
if (!args.task_list_id && !args.project_id) {
|
|
1099
|
+
return 'Provide tag_id (NFC tag mode) or task_list_id / project_id (page-scan mode).';
|
|
1100
|
+
}
|
|
1101
|
+
const qs = new URLSearchParams();
|
|
1102
|
+
if (args.task_list_id) qs.set('task_list_id', args.task_list_id);
|
|
1103
|
+
else qs.set('project_id', args.project_id);
|
|
1104
|
+
if (args.source) qs.set('source', args.source);
|
|
1105
|
+
if (args.since) qs.set('since', args.since);
|
|
1106
|
+
if (args.limit) qs.set('limit', String(args.limit));
|
|
1107
|
+
const result = await api('GET', '/scans?' + qs.toString());
|
|
1108
|
+
const scans = result.data || [];
|
|
1109
|
+
if (!scans.length) return 'No page scans recorded yet for this scope.';
|
|
1110
|
+
const converted = scans.filter(s => s.converted);
|
|
1111
|
+
const deltas = converted.map(s => s.scan_to_start_secs).filter(n => n != null);
|
|
1112
|
+
const median = deltas.length ? deltas.sort((a, b) => a - b)[Math.floor(deltas.length / 2)] : null;
|
|
1113
|
+
let text = `${scans.length} scan(s) — ${converted.length} converted to a working session (${Math.round(converted.length / scans.length * 100)}%)`;
|
|
1114
|
+
if (median != null) text += ` · median scan→start ${median}s`;
|
|
1115
|
+
text += '\n\n';
|
|
915
1116
|
scans.forEach((s, i) => {
|
|
916
|
-
text += `${i + 1}. ${s.scanned_at}`;
|
|
917
|
-
if (s.
|
|
1117
|
+
text += `${i + 1}. ${s.scanned_at} · ${s.scan_source || 'unknown'}`;
|
|
1118
|
+
if (s.worker_name) text += ` · ${s.worker_name}`;
|
|
1119
|
+
text += s.converted ? ` · started +${s.scan_to_start_secs}s` : ' · NEVER STARTED';
|
|
1120
|
+
if (s.ip) text += ` · ip ${s.ip}`;
|
|
1121
|
+
if (s.gps_lat && s.gps_lng) text += ` · GPS ${s.gps_lat.toFixed(5)}, ${s.gps_lng.toFixed(5)}`;
|
|
918
1122
|
text += '\n';
|
|
919
1123
|
});
|
|
920
1124
|
return text;
|
|
921
1125
|
}
|
|
922
1126
|
},
|
|
923
1127
|
|
|
1128
|
+
// ─── Worker Marketplace (Patent 4: portable reputation) ──────────
|
|
1129
|
+
{
|
|
1130
|
+
name: 'tascan_search_marketplace',
|
|
1131
|
+
description: 'Search the cross-org Worker Marketplace: workers who opted in (discoverable=true on their passport), ranked by passkey trust tier + verified completion volume. Skills are AI-inferred from REAL completed work, not resumes — each carries a verified_task_count and a civilian_equivalent job title. Returns sanitized public cards only (first name + last initial, skills, stats, passport URL) — never phone, email, or org membership.',
|
|
1132
|
+
inputSchema: {
|
|
1133
|
+
type: 'object',
|
|
1134
|
+
properties: {
|
|
1135
|
+
q: { type: 'string', description: 'Skill, category, name, or civilian job title — e.g. "forklift", "LED wall", "AV technician"' },
|
|
1136
|
+
min_completions: { type: 'number', description: 'Only workers with at least this many verified completions' },
|
|
1137
|
+
limit: { type: 'number', description: 'Max cards (default 25, max 50)' }
|
|
1138
|
+
},
|
|
1139
|
+
required: []
|
|
1140
|
+
},
|
|
1141
|
+
annotations: { title: 'Search Worker Marketplace', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1142
|
+
handler: async (args, api) => {
|
|
1143
|
+
const qs = new URLSearchParams();
|
|
1144
|
+
if (args.q) qs.set('q', args.q);
|
|
1145
|
+
if (args.min_completions) qs.set('min_completions', String(args.min_completions));
|
|
1146
|
+
if (args.limit) qs.set('limit', String(args.limit));
|
|
1147
|
+
const result = await api('GET', '/marketplace' + (qs.toString() ? '?' + qs.toString() : ''));
|
|
1148
|
+
const cards = result.data || [];
|
|
1149
|
+
if (!cards.length) return 'No discoverable workers match. Workers appear after opting in on their own passport (profile page → "List me").';
|
|
1150
|
+
return cards.map(w => {
|
|
1151
|
+
let t = `${w.name}${w.passkey_verified ? ' ✓ID-VERIFIED' : ''} — ${w.completions} verified completions · ${w.points} pts · since ${w.member_since}`;
|
|
1152
|
+
if (w.skills.length) t += '\n Skills: ' + w.skills.map(s => `${s.skill} (${s.verified_task_count}×)`).join(', ');
|
|
1153
|
+
const civ = [...new Set(w.skills.map(s => s.civilian_equivalent).filter(Boolean))];
|
|
1154
|
+
if (civ.length) t += '\n Civilian equivalent: ' + civ.join(' · ');
|
|
1155
|
+
t += '\n Passport: ' + w.passport_url;
|
|
1156
|
+
return t;
|
|
1157
|
+
}).join('\n\n');
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
|
|
924
1161
|
// ─── Geofenced Zones (Patent 7: location-triggered delivery) ─────
|
|
925
1162
|
{
|
|
926
1163
|
name: 'tascan_create_zone',
|
|
@@ -940,6 +1177,7 @@ const TOOLS = [
|
|
|
940
1177
|
auto_clock_in: { type: 'boolean', description: 'Entering the zone writes a shift_start clock-in event' },
|
|
941
1178
|
auto_clock_out: { type: 'boolean', description: 'Leaving the zone writes a shift_end clock-out event' },
|
|
942
1179
|
notify_email: { type: 'string', description: 'Alert recipient override — defaults to all org admins' },
|
|
1180
|
+
polygon: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'Polygon/rectangle zone instead of a circle: vertices as [[lat,lng], ...], at least 3. lat/lng/radius_m are then computed (centroid + bounding radius) — still pass lat/lng but they are overridden.' },
|
|
943
1181
|
description: { type: 'string', description: 'Shown to workers on the Site Gate page' }
|
|
944
1182
|
},
|
|
945
1183
|
required: ['name', 'lat', 'lng']
|
|
@@ -984,7 +1222,8 @@ const TOOLS = [
|
|
|
984
1222
|
enforce_on_list: { type: 'boolean' }, is_active: { type: 'boolean' },
|
|
985
1223
|
notify_on_enter: { type: 'boolean' }, notify_on_exit: { type: 'boolean' },
|
|
986
1224
|
auto_clock_in: { type: 'boolean' }, auto_clock_out: { type: 'boolean' },
|
|
987
|
-
notify_email: { type: 'string' }
|
|
1225
|
+
notify_email: { type: 'string' },
|
|
1226
|
+
polygon: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'Replace geometry with a polygon ([[lat,lng],...], ≥3 vertices); pass null to revert to a circle' }
|
|
988
1227
|
},
|
|
989
1228
|
required: ['zone_id']
|
|
990
1229
|
},
|