innov-mcp-tasks 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/index.mjs +191 -0
- package/lib/agents.mjs +97 -0
- package/lib/serverMonitoring.mjs +98 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -75,6 +75,20 @@ Na raiz do repo existe [`.cursor/mcp.json`](../.cursor/mcp.json) com **dois** se
|
|
|
75
75
|
- `notebooks_list` — lista com filtro opcional `project_id`
|
|
76
76
|
- `notebook_get`, `notebook_create`, `notebook_update`, `notebook_delete`
|
|
77
77
|
- `notebook_documentation` — notas + fontes do caderno
|
|
78
|
+
- `annotations_semantic_search` — busca semântica em notas e metadados (`q` ≥ 2 caracteres)
|
|
79
|
+
|
|
80
|
+
### Agentes de IA (SPEC-015)
|
|
81
|
+
|
|
82
|
+
- `agents_list` — agentes utilizáveis pelo token (`GET /api/v1/ai-agents?scope=usable`); opcional `scope=manageable` para gestão
|
|
83
|
+
- `agent_chat` — envia `message` a um agente (`POST /api/v1/ai-agents/{ulid}/chat`); devolve `response` e `citations`; opcional `conversation_ulid`, `project_id`, `notebook_id`
|
|
84
|
+
|
|
85
|
+
### Infraestrutura / servidores (SPEC-022)
|
|
86
|
+
|
|
87
|
+
- `servers_dashboard` — resumo online/offline, alertas e aplicações (`GET /server-monitoring/dashboard`)
|
|
88
|
+
- `servers_list` — lista servidores com métricas; filtros opcionais `status`, `search`
|
|
89
|
+
- `server_get` — detalhe por `server_ulid` (métricas + aplicações com tipo/versão)
|
|
90
|
+
- `server_alerts` — alertas ativos de CPU, RAM, disco e aplicações
|
|
91
|
+
- `server_applications_search` — busca apps (`search`, `type`, `server_ulid`, `env_production`, `debug_enabled`, paginação)
|
|
78
92
|
|
|
79
93
|
## Publicar no npm (mantenedor)
|
|
80
94
|
|
package/index.mjs
CHANGED
|
@@ -6,6 +6,14 @@ import 'dotenv/config';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { agentChat, agentsList } from './lib/agents.mjs';
|
|
10
|
+
import {
|
|
11
|
+
serverAlerts,
|
|
12
|
+
serverApplicationsSearch,
|
|
13
|
+
serverGet,
|
|
14
|
+
serversDashboard,
|
|
15
|
+
serversList,
|
|
16
|
+
} from './lib/serverMonitoring.mjs';
|
|
9
17
|
|
|
10
18
|
const base = (process.env.INNOV_API_BASE_URL || '').replace(/\/$/, '');
|
|
11
19
|
const token = (process.env.INNOV_API_TOKEN || '').trim();
|
|
@@ -757,6 +765,40 @@ server.registerTool(
|
|
|
757
765
|
},
|
|
758
766
|
);
|
|
759
767
|
|
|
768
|
+
server.registerTool(
|
|
769
|
+
'annotations_semantic_search',
|
|
770
|
+
{
|
|
771
|
+
description:
|
|
772
|
+
'Busca semântica em notas e metadados de cadernos (GET /annotations/semantic-search).',
|
|
773
|
+
inputSchema: {
|
|
774
|
+
q: z.string().min(2),
|
|
775
|
+
notebook_id: z.number().int().positive().optional(),
|
|
776
|
+
limit: z.number().int().min(1).max(30).optional(),
|
|
777
|
+
min_score: z.number().min(0).max(1).optional(),
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
async (args) => {
|
|
781
|
+
try {
|
|
782
|
+
const params = new URLSearchParams({ q: args.q });
|
|
783
|
+
if (args.notebook_id != null) {
|
|
784
|
+
params.set('notebook_id', String(args.notebook_id));
|
|
785
|
+
}
|
|
786
|
+
if (args.limit != null) {
|
|
787
|
+
params.set('limit', String(args.limit));
|
|
788
|
+
}
|
|
789
|
+
if (args.min_score != null) {
|
|
790
|
+
params.set('min_score', String(args.min_score));
|
|
791
|
+
}
|
|
792
|
+
const data = await apiFetch(
|
|
793
|
+
`/api/v1/annotations/semantic-search?${params.toString()}`,
|
|
794
|
+
);
|
|
795
|
+
return jsonText(data);
|
|
796
|
+
} catch (e) {
|
|
797
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
);
|
|
801
|
+
|
|
760
802
|
server.registerTool(
|
|
761
803
|
'notebook_documentation',
|
|
762
804
|
{
|
|
@@ -776,5 +818,154 @@ server.registerTool(
|
|
|
776
818
|
},
|
|
777
819
|
);
|
|
778
820
|
|
|
821
|
+
server.registerTool(
|
|
822
|
+
'agents_list',
|
|
823
|
+
{
|
|
824
|
+
description:
|
|
825
|
+
'Lista agentes de IA (GET /ai-agents). Padrão scope=usable — agentes que o token pode usar no chat (ACL). ' +
|
|
826
|
+
'Use scope=manageable para gestão (/agentes; requer can_manage_agents).',
|
|
827
|
+
inputSchema: {
|
|
828
|
+
scope: z
|
|
829
|
+
.enum(['usable', 'manageable'])
|
|
830
|
+
.optional()
|
|
831
|
+
.describe('Padrão: usable (selector de chat conforme ACL)'),
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
async (args) => {
|
|
835
|
+
try {
|
|
836
|
+
const data = await agentsList(apiFetch, args.scope ?? 'usable');
|
|
837
|
+
return jsonText(data);
|
|
838
|
+
} catch (e) {
|
|
839
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
server.registerTool(
|
|
845
|
+
'agent_chat',
|
|
846
|
+
{
|
|
847
|
+
description:
|
|
848
|
+
'Envia mensagem a um agente de IA (POST /ai-agents/{ulid}/chat). ' +
|
|
849
|
+
'Devolve resposta do agente e citações quando ferramentas de KB forem usadas. ' +
|
|
850
|
+
'Opcional conversation_ulid para continuar conversa; project_id/notebook_id para contexto.',
|
|
851
|
+
inputSchema: {
|
|
852
|
+
agent_ulid: z.string().min(1).describe('ULID do agente (ver agents_list)'),
|
|
853
|
+
message: z.string().min(1),
|
|
854
|
+
conversation_ulid: z
|
|
855
|
+
.string()
|
|
856
|
+
.optional()
|
|
857
|
+
.describe('ULID da conversa para continuar o turno'),
|
|
858
|
+
project_id: z.number().int().positive().optional(),
|
|
859
|
+
notebook_id: z.number().int().positive().optional(),
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
async (args) => {
|
|
863
|
+
try {
|
|
864
|
+
const data = await agentChat(apiFetch, args);
|
|
865
|
+
return jsonText(data);
|
|
866
|
+
} catch (e) {
|
|
867
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
server.registerTool(
|
|
873
|
+
'servers_dashboard',
|
|
874
|
+
{
|
|
875
|
+
description:
|
|
876
|
+
'Resumo do monitoramento de servidores (GET /server-monitoring/dashboard): online/offline, alertas e aplicações.',
|
|
877
|
+
inputSchema: {},
|
|
878
|
+
},
|
|
879
|
+
async () => {
|
|
880
|
+
try {
|
|
881
|
+
const data = await serversDashboard(apiFetch);
|
|
882
|
+
return jsonText(data);
|
|
883
|
+
} catch (e) {
|
|
884
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
server.registerTool(
|
|
890
|
+
'servers_list',
|
|
891
|
+
{
|
|
892
|
+
description:
|
|
893
|
+
'Lista servidores monitorados com métricas recentes (GET /server-monitoring/servers).',
|
|
894
|
+
inputSchema: {
|
|
895
|
+
status: z.enum(['active', 'inactive']).optional(),
|
|
896
|
+
search: z.string().optional().describe('Hostname, IP ou descrição'),
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
async (args) => {
|
|
900
|
+
try {
|
|
901
|
+
const data = await serversList(apiFetch, args);
|
|
902
|
+
return jsonText(data);
|
|
903
|
+
} catch (e) {
|
|
904
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
server.registerTool(
|
|
910
|
+
'server_get',
|
|
911
|
+
{
|
|
912
|
+
description:
|
|
913
|
+
'Detalhe de um servidor: métricas, histórico e aplicações detectadas (GET /server-monitoring/servers/{ulid}).',
|
|
914
|
+
inputSchema: {
|
|
915
|
+
server_ulid: z.string().min(1).describe('ULID do servidor'),
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
async (args) => {
|
|
919
|
+
try {
|
|
920
|
+
const data = await serverGet(apiFetch, args);
|
|
921
|
+
return jsonText(data);
|
|
922
|
+
} catch (e) {
|
|
923
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
server.registerTool(
|
|
929
|
+
'server_alerts',
|
|
930
|
+
{
|
|
931
|
+
description:
|
|
932
|
+
'Alertas ativos de infraestrutura: CPU, RAM, disco e aplicações (GET /server-monitoring/alerts).',
|
|
933
|
+
inputSchema: {},
|
|
934
|
+
},
|
|
935
|
+
async () => {
|
|
936
|
+
try {
|
|
937
|
+
const data = await serverAlerts(apiFetch);
|
|
938
|
+
return jsonText(data);
|
|
939
|
+
} catch (e) {
|
|
940
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
server.registerTool(
|
|
946
|
+
'server_applications_search',
|
|
947
|
+
{
|
|
948
|
+
description:
|
|
949
|
+
'Busca aplicações nos servidores: stack (laravel, vuejs…), versão, ambiente (GET /server-monitoring/applications/search).',
|
|
950
|
+
inputSchema: {
|
|
951
|
+
search: z.string().optional(),
|
|
952
|
+
type: z.string().optional().describe('laravel, vuejs, symfony, wordpress, etc.'),
|
|
953
|
+
server_ulid: z.string().optional(),
|
|
954
|
+
env_production: z.boolean().optional(),
|
|
955
|
+
debug_enabled: z.boolean().optional(),
|
|
956
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
957
|
+
page: z.number().int().min(1).optional(),
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
async (args) => {
|
|
961
|
+
try {
|
|
962
|
+
const data = await serverApplicationsSearch(apiFetch, args);
|
|
963
|
+
return jsonText(data);
|
|
964
|
+
} catch (e) {
|
|
965
|
+
return jsonError(e instanceof Error ? e.message : String(e));
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
);
|
|
969
|
+
|
|
779
970
|
const transport = new StdioServerTransport();
|
|
780
971
|
await server.connect(transport);
|
package/lib/agents.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Helpers e handlers testáveis para tools de agentes de IA (SPEC-015 Fase 8). */
|
|
2
|
+
|
|
3
|
+
export const AGENT_SCOPES = ['usable', 'manageable'];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {'usable' | 'manageable'} [scope]
|
|
7
|
+
*/
|
|
8
|
+
export function agentsListPath(scope = 'usable') {
|
|
9
|
+
if (!AGENT_SCOPES.includes(scope)) {
|
|
10
|
+
throw new Error(`scope inválido: ${scope}. Use manageable ou usable.`);
|
|
11
|
+
}
|
|
12
|
+
return `/api/v1/ai-agents?scope=${encodeURIComponent(scope)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} ulid
|
|
17
|
+
*/
|
|
18
|
+
export function agentChatPath(ulid) {
|
|
19
|
+
const trimmed = String(ulid ?? '').trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
throw new Error('agent_ulid é obrigatório');
|
|
22
|
+
}
|
|
23
|
+
return `/api/v1/ai-agents/${encodeURIComponent(trimmed)}/chat`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {{
|
|
28
|
+
* message: string;
|
|
29
|
+
* conversation_ulid?: string;
|
|
30
|
+
* project_id?: number;
|
|
31
|
+
* notebook_id?: number;
|
|
32
|
+
* }} args
|
|
33
|
+
*/
|
|
34
|
+
export function buildAgentChatBody(args) {
|
|
35
|
+
const body = { message: args.message };
|
|
36
|
+
if (args.conversation_ulid != null) {
|
|
37
|
+
body.conversation_ulid = args.conversation_ulid;
|
|
38
|
+
}
|
|
39
|
+
if (args.project_id != null) {
|
|
40
|
+
body.project_id = args.project_id;
|
|
41
|
+
}
|
|
42
|
+
if (args.notebook_id != null) {
|
|
43
|
+
body.notebook_id = args.notebook_id;
|
|
44
|
+
}
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normaliza resposta do chat para o consumidor MCP (resposta + citações).
|
|
50
|
+
* @param {unknown} data
|
|
51
|
+
*/
|
|
52
|
+
export function formatAgentChatResponse(data) {
|
|
53
|
+
if (!data || typeof data !== 'object') {
|
|
54
|
+
return { response: null, citations: [] };
|
|
55
|
+
}
|
|
56
|
+
const payload = /** @type {Record<string, unknown>} */ (data);
|
|
57
|
+
return {
|
|
58
|
+
success: payload.success,
|
|
59
|
+
response:
|
|
60
|
+
typeof payload.response === 'string'
|
|
61
|
+
? payload.response
|
|
62
|
+
: typeof payload.message === 'string'
|
|
63
|
+
? payload.message
|
|
64
|
+
: null,
|
|
65
|
+
citations: Array.isArray(payload.citations) ? payload.citations : [],
|
|
66
|
+
conversation_ulid: payload.conversation_ulid ?? null,
|
|
67
|
+
actions: payload.actions,
|
|
68
|
+
token_usage: payload.token_usage,
|
|
69
|
+
pending_approval: payload.pending_approval ?? null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
|
|
75
|
+
* @param {'usable' | 'manageable'} [scope]
|
|
76
|
+
*/
|
|
77
|
+
export async function agentsList(apiFetch, scope = 'usable') {
|
|
78
|
+
return apiFetch(agentsListPath(scope));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
|
|
83
|
+
* @param {{
|
|
84
|
+
* agent_ulid: string;
|
|
85
|
+
* message: string;
|
|
86
|
+
* conversation_ulid?: string;
|
|
87
|
+
* project_id?: number;
|
|
88
|
+
* notebook_id?: number;
|
|
89
|
+
* }} args
|
|
90
|
+
*/
|
|
91
|
+
export async function agentChat(apiFetch, args) {
|
|
92
|
+
const data = await apiFetch(agentChatPath(args.agent_ulid), {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
body: JSON.stringify(buildAgentChatBody(args)),
|
|
95
|
+
});
|
|
96
|
+
return formatAgentChatResponse(data);
|
|
97
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Helpers e handlers para ferramentas de monitoramento de servidores (SPEC-022). */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Record<string, string | number | boolean | undefined | null>} [params]
|
|
5
|
+
*/
|
|
6
|
+
export function serverMonitoringQueryString(params = {}) {
|
|
7
|
+
const search = new URLSearchParams();
|
|
8
|
+
for (const [key, value] of Object.entries(params)) {
|
|
9
|
+
if (value === undefined || value === null || value === '') {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
search.set(key, String(value));
|
|
13
|
+
}
|
|
14
|
+
const qs = search.toString();
|
|
15
|
+
return qs ? `?${qs}` : '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function serversDashboardPath() {
|
|
19
|
+
return '/api/v1/server-monitoring/dashboard';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{ status?: string, search?: string }} [args]
|
|
24
|
+
*/
|
|
25
|
+
export function serversListPath(args = {}) {
|
|
26
|
+
return `/api/v1/server-monitoring/servers${serverMonitoringQueryString({
|
|
27
|
+
status: args.status,
|
|
28
|
+
search: args.search,
|
|
29
|
+
})}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} serverUlid
|
|
34
|
+
*/
|
|
35
|
+
export function serverGetPath(serverUlid) {
|
|
36
|
+
const trimmed = String(serverUlid ?? '').trim();
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
throw new Error('server_ulid é obrigatório');
|
|
39
|
+
}
|
|
40
|
+
return `/api/v1/server-monitoring/servers/${encodeURIComponent(trimmed)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function serverAlertsPath() {
|
|
44
|
+
return '/api/v1/server-monitoring/alerts';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Record<string, string | number | boolean | undefined | null>} [args]
|
|
49
|
+
*/
|
|
50
|
+
export function serverApplicationsSearchPath(args = {}) {
|
|
51
|
+
const { limit, page, ...rest } = args;
|
|
52
|
+
return `/api/v1/server-monitoring/applications/search${serverMonitoringQueryString({
|
|
53
|
+
...rest,
|
|
54
|
+
per_page: limit ?? rest.per_page,
|
|
55
|
+
page,
|
|
56
|
+
})}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
|
|
61
|
+
*/
|
|
62
|
+
export async function serversDashboard(apiFetch) {
|
|
63
|
+
return apiFetch(serversDashboardPath());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
|
|
68
|
+
* @param {{ status?: string, search?: string }} [args]
|
|
69
|
+
*/
|
|
70
|
+
export async function serversList(apiFetch, args = {}) {
|
|
71
|
+
const data = await apiFetch(serversListPath(args));
|
|
72
|
+
return data?.data ?? data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
|
|
77
|
+
* @param {{ server_ulid: string }} args
|
|
78
|
+
*/
|
|
79
|
+
export async function serverGet(apiFetch, args) {
|
|
80
|
+
const data = await apiFetch(serverGetPath(args.server_ulid));
|
|
81
|
+
return data?.data ?? data;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
|
|
86
|
+
*/
|
|
87
|
+
export async function serverAlerts(apiFetch) {
|
|
88
|
+
const data = await apiFetch(serverAlertsPath());
|
|
89
|
+
return data?.data ?? data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
|
|
94
|
+
* @param {Record<string, string | number | boolean | undefined | null>} [args]
|
|
95
|
+
*/
|
|
96
|
+
export async function serverApplicationsSearch(apiFetch, args = {}) {
|
|
97
|
+
return apiFetch(serverApplicationsSearchPath(args));
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "innov-mcp-tasks",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP stdio — tarefas e
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "MCP stdio — tarefas, anotações e agentes de IA Innov (INNOV_API_BASE_URL + token Sanctum)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
7
7
|
"bin": {
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"index.mjs",
|
|
12
|
+
"lib/**",
|
|
12
13
|
"README.md",
|
|
13
14
|
".env.example",
|
|
14
15
|
"scripts/postinstall-notice.mjs"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"start": "node index.mjs",
|
|
18
|
-
"test": "node --check index.mjs",
|
|
19
|
+
"test": "node --check index.mjs && node --test test/*.test.mjs",
|
|
19
20
|
"prepack": "node --check index.mjs",
|
|
20
21
|
"postinstall": "node ./scripts/postinstall-notice.mjs"
|
|
21
22
|
},
|
|
@@ -28,6 +29,8 @@
|
|
|
28
29
|
"tasks",
|
|
29
30
|
"notes",
|
|
30
31
|
"annotations",
|
|
32
|
+
"ai-agents",
|
|
33
|
+
"agents",
|
|
31
34
|
"innov"
|
|
32
35
|
],
|
|
33
36
|
"author": "",
|