@nex-ai/nex 0.1.65 → 0.1.67

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.
Files changed (75) hide show
  1. package/dist/commands/upgrade.d.ts +9 -0
  2. package/dist/commands/upgrade.js +87 -0
  3. package/dist/commands/upgrade.js.map +1 -0
  4. package/dist/index.js +7 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/installers.js +3 -2
  7. package/dist/lib/installers.js.map +1 -1
  8. package/dist/mcp/index.js +0 -0
  9. package/dist/plugin/adapters/cline-capture.d.ts +7 -0
  10. package/dist/plugin/adapters/cline-capture.js +25 -0
  11. package/dist/plugin/adapters/cline-capture.js.map +1 -0
  12. package/dist/plugin/adapters/cline-recall.d.ts +7 -0
  13. package/dist/plugin/adapters/cline-recall.js +30 -0
  14. package/dist/plugin/adapters/cline-recall.js.map +1 -0
  15. package/dist/plugin/adapters/cline-task-start.d.ts +7 -0
  16. package/dist/plugin/adapters/cline-task-start.js +30 -0
  17. package/dist/plugin/adapters/cline-task-start.js.map +1 -0
  18. package/dist/plugin/adapters/cursor-recall.d.ts +7 -0
  19. package/dist/plugin/adapters/cursor-recall.js +31 -0
  20. package/dist/plugin/adapters/cursor-recall.js.map +1 -0
  21. package/dist/plugin/adapters/cursor-session-start.d.ts +7 -0
  22. package/dist/plugin/adapters/cursor-session-start.js +30 -0
  23. package/dist/plugin/adapters/cursor-session-start.js.map +1 -0
  24. package/dist/plugin/adapters/cursor-stop.d.ts +7 -0
  25. package/dist/plugin/adapters/cursor-stop.js +25 -0
  26. package/dist/plugin/adapters/cursor-stop.js.map +1 -0
  27. package/dist/plugin/adapters/windsurf-capture.d.ts +7 -0
  28. package/dist/plugin/adapters/windsurf-capture.js +25 -0
  29. package/dist/plugin/adapters/windsurf-capture.js.map +1 -0
  30. package/dist/plugin/adapters/windsurf-recall.d.ts +7 -0
  31. package/dist/plugin/adapters/windsurf-recall.js +31 -0
  32. package/dist/plugin/adapters/windsurf-recall.js.map +1 -0
  33. package/dist/plugin/auto-session-start.js +39 -1
  34. package/dist/plugin/auto-session-start.js.map +1 -1
  35. package/dist/plugin/shared.d.ts +39 -0
  36. package/dist/plugin/shared.js +380 -0
  37. package/dist/plugin/shared.js.map +1 -0
  38. package/dist/plugin/update-check.d.ts +13 -0
  39. package/dist/plugin/update-check.js +134 -0
  40. package/dist/plugin/update-check.js.map +1 -0
  41. package/openclaw-plugin/dist/capture-filter.d.ts +32 -0
  42. package/openclaw-plugin/dist/capture-filter.d.ts.map +1 -0
  43. package/openclaw-plugin/dist/capture-filter.js +117 -0
  44. package/openclaw-plugin/dist/capture-filter.js.map +1 -0
  45. package/openclaw-plugin/dist/config.d.ts +24 -0
  46. package/openclaw-plugin/dist/config.d.ts.map +1 -0
  47. package/openclaw-plugin/dist/config.js +68 -0
  48. package/openclaw-plugin/dist/config.js.map +1 -0
  49. package/openclaw-plugin/dist/context-format.d.ts +23 -0
  50. package/openclaw-plugin/dist/context-format.d.ts.map +1 -0
  51. package/openclaw-plugin/dist/context-format.js +40 -0
  52. package/openclaw-plugin/dist/context-format.js.map +1 -0
  53. package/openclaw-plugin/dist/file-scanner.d.ts +23 -0
  54. package/openclaw-plugin/dist/file-scanner.d.ts.map +1 -0
  55. package/openclaw-plugin/dist/file-scanner.js +119 -0
  56. package/openclaw-plugin/dist/file-scanner.js.map +1 -0
  57. package/openclaw-plugin/dist/index.d.ts +94 -0
  58. package/openclaw-plugin/dist/index.d.ts.map +1 -0
  59. package/openclaw-plugin/dist/index.js +1227 -0
  60. package/openclaw-plugin/dist/index.js.map +1 -0
  61. package/openclaw-plugin/dist/nex-client.d.ts +52 -0
  62. package/openclaw-plugin/dist/nex-client.d.ts.map +1 -0
  63. package/openclaw-plugin/dist/nex-client.js +129 -0
  64. package/openclaw-plugin/dist/nex-client.js.map +1 -0
  65. package/openclaw-plugin/dist/rate-limiter.d.ts +29 -0
  66. package/openclaw-plugin/dist/rate-limiter.d.ts.map +1 -0
  67. package/openclaw-plugin/dist/rate-limiter.js +95 -0
  68. package/openclaw-plugin/dist/rate-limiter.js.map +1 -0
  69. package/openclaw-plugin/dist/session-store.d.ts +15 -0
  70. package/openclaw-plugin/dist/session-store.d.ts.map +1 -0
  71. package/openclaw-plugin/dist/session-store.js +43 -0
  72. package/openclaw-plugin/dist/session-store.js.map +1 -0
  73. package/openclaw-plugin/openclaw.plugin.json +85 -0
  74. package/openclaw-plugin/package.json +29 -0
  75. package/package.json +2 -1
@@ -0,0 +1,1227 @@
1
+ /**
2
+ * Nex Memory Plugin for OpenClaw
3
+ *
4
+ * Gives OpenClaw agents persistent long-term memory powered by the Nex
5
+ * context intelligence layer. Auto-recalls relevant context before each
6
+ * agent turn and auto-captures conversation facts after each turn.
7
+ */
8
+ import { Type } from "@sinclair/typebox";
9
+ import { parseConfig } from "./config.js";
10
+ import { NexClient, NexAuthError } from "./nex-client.js";
11
+ import { RateLimiter } from "./rate-limiter.js";
12
+ import { SessionStore } from "./session-store.js";
13
+ import { formatNexContext } from "./context-format.js";
14
+ import { captureFilter } from "./capture-filter.js";
15
+ import { scanFiles as scanFilesUtil } from "./file-scanner.js";
16
+ // --- TypeBox schemas for tool parameters ---
17
+ const SearchParams = Type.Object({
18
+ query: Type.String({ description: "What to search for in the knowledge base" }),
19
+ });
20
+ const RememberParams = Type.Object({
21
+ content: Type.String({ description: "The information to remember" }),
22
+ label: Type.Optional(Type.String({ description: "Optional context label (e.g. 'meeting notes', 'preference')" })),
23
+ });
24
+ const EntitiesParams = Type.Object({
25
+ query: Type.String({ description: "Search query to find related entities" }),
26
+ });
27
+ const ScanFilesParams = Type.Object({
28
+ dir: Type.String({ description: "Directory path to scan for text files" }),
29
+ extensions: Type.Optional(Type.String({ description: "Comma-separated file extensions (default: .md,.txt,.csv,.json,.yaml,.yml)" })),
30
+ max_files: Type.Optional(Type.Number({ description: "Maximum files to scan per run (default: 5)" })),
31
+ depth: Type.Optional(Type.Number({ description: "Maximum directory depth (default: 2)" })),
32
+ force: Type.Optional(Type.Boolean({ description: "Re-scan all files ignoring manifest (default: false)" })),
33
+ });
34
+ // --- Plugin definition ---
35
+ const plugin = {
36
+ id: "nex",
37
+ name: "Nex Memory",
38
+ description: "Persistent context intelligence for OpenClaw agents, powered by Nex",
39
+ version: "0.1.0",
40
+ kind: "memory",
41
+ register(api) {
42
+ const log = api.logger;
43
+ // --- Config ---
44
+ let cfg;
45
+ try {
46
+ cfg = parseConfig(api.pluginConfig);
47
+ }
48
+ catch (err) {
49
+ log.error("Failed to parse Nex plugin config:", err);
50
+ throw err;
51
+ }
52
+ const client = new NexClient(cfg.apiKey, cfg.baseUrl);
53
+ const rateLimiter = new RateLimiter();
54
+ const sessions = new SessionStore();
55
+ const debug = (...args) => {
56
+ if (cfg.debug && log.debug)
57
+ log.debug("[nex]", ...args);
58
+ };
59
+ debug("Plugin config loaded", { baseUrl: cfg.baseUrl, autoRecall: cfg.autoRecall, autoCapture: cfg.autoCapture });
60
+ // --- Service (health check on start, cleanup on stop) ---
61
+ api.registerService({
62
+ id: "nex",
63
+ async start({ logger }) {
64
+ logger.info("Nex memory plugin starting...");
65
+ try {
66
+ const healthy = await client.healthCheck();
67
+ if (healthy) {
68
+ logger.info("Nex API connection verified");
69
+ }
70
+ else {
71
+ logger.warn("Nex API health check failed — recall/capture may not work");
72
+ }
73
+ }
74
+ catch (err) {
75
+ if (err instanceof NexAuthError) {
76
+ logger.error("Nex API key is invalid. Check your apiKey config or NEX_API_KEY env var.");
77
+ }
78
+ else {
79
+ logger.warn("Could not reach Nex API:", err);
80
+ }
81
+ }
82
+ },
83
+ async stop({ logger }) {
84
+ logger.info("Nex memory plugin stopping — flushing capture queue...");
85
+ try {
86
+ await Promise.race([
87
+ rateLimiter.flush(),
88
+ new Promise((_, reject) => setTimeout(() => reject(new Error("flush timeout")), 5000)),
89
+ ]);
90
+ }
91
+ catch {
92
+ logger.warn("Capture queue flush timed out");
93
+ }
94
+ rateLimiter.destroy();
95
+ sessions.clear();
96
+ },
97
+ });
98
+ // --- Hook: before_agent_start (auto-recall) ---
99
+ if (cfg.autoRecall) {
100
+ api.on("before_agent_start", async (event, ctx) => {
101
+ if (!event.prompt)
102
+ return;
103
+ debug("Auto-recall triggered", { sessionKey: ctx.sessionKey, promptLength: event.prompt.length });
104
+ try {
105
+ // Resolve session ID for multi-turn continuity
106
+ const nexSessionId = ctx.sessionKey && cfg.sessionTracking
107
+ ? sessions.get(ctx.sessionKey)
108
+ : undefined;
109
+ const result = await client.ask(event.prompt, nexSessionId, cfg.recallTimeoutMs);
110
+ if (!result.answer) {
111
+ debug("Recall returned empty answer");
112
+ return;
113
+ }
114
+ // Store session ID for future turns
115
+ if (result.session_id && ctx.sessionKey && cfg.sessionTracking) {
116
+ sessions.set(ctx.sessionKey, result.session_id);
117
+ }
118
+ const entityCount = result.entity_references?.length ?? 0;
119
+ const context = formatNexContext({
120
+ answer: result.answer,
121
+ entityCount,
122
+ sessionId: result.session_id,
123
+ });
124
+ debug("Recall injecting context", { entityCount, answerLength: result.answer.length });
125
+ return { prependContext: context };
126
+ }
127
+ catch (err) {
128
+ // Graceful degradation — never block agent on recall failure
129
+ if (err instanceof Error && err.name === "AbortError") {
130
+ debug("Recall timed out");
131
+ }
132
+ else {
133
+ log.warn("Nex recall failed (agent will proceed without context):", err);
134
+ }
135
+ return;
136
+ }
137
+ }, { priority: 10 });
138
+ }
139
+ // --- Hook: agent_end (auto-capture) ---
140
+ if (cfg.autoCapture) {
141
+ api.on("agent_end", async (event, ctx) => {
142
+ const messages = event.messages;
143
+ const result = captureFilter(messages, cfg, {
144
+ messageProvider: ctx.messageProvider,
145
+ success: event.success,
146
+ });
147
+ if (result.skipped) {
148
+ debug("Capture skipped:", result.reason);
149
+ return;
150
+ }
151
+ debug("Capture enqueued", { textLength: result.text.length });
152
+ // Fire-and-forget via rate limiter
153
+ rateLimiter.enqueue(async () => {
154
+ try {
155
+ const res = await client.ingest(result.text, "openclaw-conversation");
156
+ debug("Capture complete", { artifactId: res.artifact_id });
157
+ }
158
+ catch (err) {
159
+ log.warn("Nex capture failed:", err);
160
+ }
161
+ }).catch(() => {
162
+ // Queue full / dropped — already logged by rate limiter
163
+ });
164
+ });
165
+ }
166
+ // --- Tools ---
167
+ api.registerTool({
168
+ name: "nex_search",
169
+ label: "Search Nex Knowledge",
170
+ description: "Search the user's Nex knowledge base for relevant context. Returns an AI-synthesized answer with entity references.",
171
+ parameters: SearchParams,
172
+ async execute(_toolCallId, params) {
173
+ const { query } = params;
174
+ const result = await client.ask(query);
175
+ const parts = [result.answer];
176
+ if (result.entity_references && result.entity_references.length > 0) {
177
+ parts.push("\n\nRelated entities:");
178
+ for (const ref of result.entity_references) {
179
+ parts.push(`- ${ref.name} (${ref.type})`);
180
+ }
181
+ }
182
+ return {
183
+ content: [{ type: "text", text: parts.join("\n") }],
184
+ details: result,
185
+ };
186
+ },
187
+ });
188
+ api.registerTool({
189
+ name: "nex_remember",
190
+ label: "Remember in Nex",
191
+ description: "Store information in the user's Nex knowledge base for long-term recall. Use this when the user explicitly asks you to remember something.",
192
+ parameters: RememberParams,
193
+ async execute(_toolCallId, params) {
194
+ const { content, label } = params;
195
+ // Enqueue via rate limiter but wait for result
196
+ const res = await new Promise((resolve, reject) => {
197
+ rateLimiter
198
+ .enqueue(async () => {
199
+ const r = await client.ingest(content, label);
200
+ resolve(r);
201
+ })
202
+ .catch(reject);
203
+ });
204
+ return {
205
+ content: [{ type: "text", text: `Remembered. (artifact: ${res.artifact_id})` }],
206
+ details: res,
207
+ };
208
+ },
209
+ });
210
+ api.registerTool({
211
+ name: "nex_entities",
212
+ label: "Find Nex Entities",
213
+ description: "Search for entities (people, companies, topics) in the user's Nex knowledge base. Returns a structured list with types and mention counts.",
214
+ parameters: EntitiesParams,
215
+ async execute(_toolCallId, params) {
216
+ const { query } = params;
217
+ const result = await client.ask(query);
218
+ if (!result.entity_references || result.entity_references.length === 0) {
219
+ return {
220
+ content: [{ type: "text", text: "No matching entities found." }],
221
+ details: { entities: [] },
222
+ };
223
+ }
224
+ const lines = result.entity_references.map((ref) => `- ${ref.name} (${ref.type})${ref.count ? ` — ${ref.count} mentions` : ""}`);
225
+ return {
226
+ content: [{ type: "text", text: `Found ${result.entity_references.length} entities:\n${lines.join("\n")}` }],
227
+ details: { entities: result.entity_references },
228
+ };
229
+ },
230
+ });
231
+ api.registerTool({
232
+ name: "nex_scan_files",
233
+ label: "Scan Files into Nex",
234
+ description: "Scan a directory for text files (.md, .txt, .csv, .json, .yaml, .yml) and ingest new or changed files into the Nex knowledge base. Uses SHA-256 content hashing to skip already-ingested files.",
235
+ parameters: ScanFilesParams,
236
+ async execute(_toolCallId, params) {
237
+ const { dir, extensions, max_files, depth, force } = params;
238
+ const extList = extensions
239
+ ? extensions.split(",").map((e) => e.trim())
240
+ : undefined;
241
+ const result = await scanFilesUtil(dir, client, {
242
+ extensions: extList,
243
+ maxFiles: max_files,
244
+ depth,
245
+ force,
246
+ });
247
+ const summary = `Scanned ${result.scanned} file(s), skipped ${result.skipped}, errors ${result.errors}.`;
248
+ const details = result.files
249
+ .map((f) => `- ${f.path}: ${f.status}${f.reason ? ` (${f.reason})` : ""}`)
250
+ .join("\n");
251
+ return {
252
+ content: [{ type: "text", text: `${summary}\n\n${details}` }],
253
+ details: result,
254
+ };
255
+ },
256
+ });
257
+ // --- Integration tools ---
258
+ const ListIntegrationsParams = Type.Object({});
259
+ api.registerTool({
260
+ name: "nex_list_integrations",
261
+ label: "List Integrations",
262
+ description: "List available third-party integrations and their connection status. Calendar integrations (Google Calendar, Outlook Calendar) enable the Nex Meeting Bot which joins calls on any platform (Google Meet, Zoom, Webex, Teams, etc.) and feeds transcripts into the context graph.",
263
+ parameters: ListIntegrationsParams,
264
+ async execute(_toolCallId, _params) {
265
+ const result = await client.get("/v1/integrations/");
266
+ return {
267
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
268
+ details: result,
269
+ };
270
+ },
271
+ });
272
+ const ConnectIntegrationParams = Type.Object({
273
+ type: Type.String({ description: "Integration type: email, calendar, crm, messaging" }),
274
+ provider: Type.String({ description: "Provider: google, microsoft, attio, slack, salesforce, hubspot" }),
275
+ });
276
+ api.registerTool({
277
+ name: "nex_connect_integration",
278
+ label: "Connect Integration",
279
+ description: "Start connecting a third-party integration via OAuth. Returns an auth_url for the user to open in their browser. Calendar integrations (type: 'calendar') enable the Nex Meeting Bot which auto-joins calls and processes transcripts. Types: email, calendar, crm, messaging. Providers: google, microsoft, attio, slack, salesforce, hubspot.",
280
+ parameters: ConnectIntegrationParams,
281
+ async execute(_toolCallId, params) {
282
+ const { type, provider } = params;
283
+ const result = await client.post(`/v1/integrations/${encodeURIComponent(type)}/${encodeURIComponent(provider)}/connect`);
284
+ return {
285
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
286
+ details: result,
287
+ };
288
+ },
289
+ });
290
+ const DisconnectIntegrationParams = Type.Object({
291
+ connection_id: Type.String({ description: "Connection ID to disconnect" }),
292
+ });
293
+ api.registerTool({
294
+ name: "nex_disconnect_integration",
295
+ label: "Disconnect Integration",
296
+ description: "Disconnect a third-party integration by connection ID. Get connection IDs from nex_list_integrations.",
297
+ parameters: DisconnectIntegrationParams,
298
+ async execute(_toolCallId, params) {
299
+ const { connection_id } = params;
300
+ const result = await client.delete(`/v1/integrations/connections/${encodeURIComponent(connection_id)}`);
301
+ return {
302
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
303
+ details: result,
304
+ };
305
+ },
306
+ });
307
+ // --- Schema tools ---
308
+ const ListObjectsParams = Type.Object({
309
+ include_attributes: Type.Optional(Type.Boolean({ description: "Include attribute definitions in the response" })),
310
+ });
311
+ api.registerTool({
312
+ name: "nex_list_objects",
313
+ label: "List Object Types",
314
+ description: "List all object type definitions in the workspace. Call this first to discover available object types and their schemas.",
315
+ parameters: ListObjectsParams,
316
+ async execute(_toolCallId, params) {
317
+ const { include_attributes } = params;
318
+ const qs = new URLSearchParams();
319
+ if (include_attributes)
320
+ qs.set("include_attributes", "true");
321
+ const q = qs.toString();
322
+ const result = await client.get(`/v1/objects${q ? `?${q}` : ""}`);
323
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
324
+ },
325
+ });
326
+ const GetObjectParams = Type.Object({
327
+ slug: Type.String({ description: "Object type slug (e.g. 'person', 'company', 'deal')" }),
328
+ });
329
+ api.registerTool({
330
+ name: "nex_get_object",
331
+ label: "Get Object Type",
332
+ description: "Get a single object type definition with its attributes.",
333
+ parameters: GetObjectParams,
334
+ async execute(_toolCallId, params) {
335
+ const { slug } = params;
336
+ const result = await client.get(`/v1/objects/${encodeURIComponent(slug)}`);
337
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
338
+ },
339
+ });
340
+ const CreateObjectParams = Type.Object({
341
+ name: Type.String({ description: "Display name for the object type" }),
342
+ slug: Type.String({ description: "URL-safe identifier (lowercase, hyphens)" }),
343
+ name_plural: Type.Optional(Type.String({ description: "Plural display name" })),
344
+ description: Type.Optional(Type.String({ description: "Description of the object type" })),
345
+ type: Type.Optional(Type.String({ description: "Object category: person, company, custom, deal (default: custom)" })),
346
+ });
347
+ api.registerTool({
348
+ name: "nex_create_object",
349
+ label: "Create Object Type",
350
+ description: "Create a new custom object type definition (e.g. 'Project', 'Deal').",
351
+ parameters: CreateObjectParams,
352
+ async execute(_toolCallId, params) {
353
+ const { name, slug, name_plural, description, type } = params;
354
+ const body = { name, slug };
355
+ if (name_plural !== undefined)
356
+ body.name_plural = name_plural;
357
+ if (description !== undefined)
358
+ body.description = description;
359
+ if (type !== undefined)
360
+ body.type = type;
361
+ const result = await client.post("/v1/objects", body);
362
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
363
+ },
364
+ });
365
+ const UpdateObjectParams = Type.Object({
366
+ slug: Type.String({ description: "Object type slug to update" }),
367
+ name: Type.Optional(Type.String({ description: "New display name" })),
368
+ name_plural: Type.Optional(Type.String({ description: "New plural display name" })),
369
+ description: Type.Optional(Type.String({ description: "New description" })),
370
+ });
371
+ api.registerTool({
372
+ name: "nex_update_object",
373
+ label: "Update Object Type",
374
+ description: "Update an existing object type definition (name, description, plural name).",
375
+ parameters: UpdateObjectParams,
376
+ async execute(_toolCallId, params) {
377
+ const { slug, name, name_plural, description } = params;
378
+ const body = {};
379
+ if (name !== undefined)
380
+ body.name = name;
381
+ if (name_plural !== undefined)
382
+ body.name_plural = name_plural;
383
+ if (description !== undefined)
384
+ body.description = description;
385
+ const result = await client.patch(`/v1/objects/${encodeURIComponent(slug)}`, body);
386
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
387
+ },
388
+ });
389
+ const DeleteObjectParams = Type.Object({
390
+ slug: Type.String({ description: "Object type slug to delete" }),
391
+ });
392
+ api.registerTool({
393
+ name: "nex_delete_object",
394
+ label: "Delete Object Type",
395
+ description: "Delete an object type definition and ALL its records. This is destructive and cannot be undone.",
396
+ parameters: DeleteObjectParams,
397
+ async execute(_toolCallId, params) {
398
+ const { slug } = params;
399
+ const result = await client.delete(`/v1/objects/${encodeURIComponent(slug)}`);
400
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
401
+ },
402
+ });
403
+ const CreateAttributeParams = Type.Object({
404
+ object_slug: Type.String({ description: "Object type slug to add the attribute to" }),
405
+ name: Type.String({ description: "Display name for the attribute" }),
406
+ slug: Type.String({ description: "URL-safe identifier for the attribute" }),
407
+ type: Type.String({ description: "Attribute data type: text, number, email, phone, url, date, boolean, currency, location, select, social_profile, domain, full_name" }),
408
+ description: Type.Optional(Type.String({ description: "Description of the attribute" })),
409
+ options: Type.Optional(Type.Object({
410
+ is_required: Type.Optional(Type.Boolean()),
411
+ is_unique: Type.Optional(Type.Boolean()),
412
+ is_multi_value: Type.Optional(Type.Boolean()),
413
+ use_raw_format: Type.Optional(Type.Boolean()),
414
+ is_whole_number: Type.Optional(Type.Boolean()),
415
+ select_options: Type.Optional(Type.Array(Type.Object({ name: Type.String() }))),
416
+ })),
417
+ });
418
+ api.registerTool({
419
+ name: "nex_create_attribute",
420
+ label: "Create Attribute",
421
+ description: "Add a new attribute (field) to an object type. Supports types: text, number, email, phone, url, date, boolean, currency, location, select, social_profile, domain, full_name.",
422
+ parameters: CreateAttributeParams,
423
+ async execute(_toolCallId, params) {
424
+ const { object_slug, name, slug, type, description, options } = params;
425
+ const body = { name, slug, type };
426
+ if (description !== undefined)
427
+ body.description = description;
428
+ if (options)
429
+ body.options = options;
430
+ const result = await client.post(`/v1/objects/${encodeURIComponent(object_slug)}/attributes`, body);
431
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
432
+ },
433
+ });
434
+ const UpdateAttributeParams = Type.Object({
435
+ object_slug: Type.String({ description: "Object type slug" }),
436
+ attribute_id: Type.String({ description: "Attribute ID to update" }),
437
+ name: Type.Optional(Type.String({ description: "New display name" })),
438
+ description: Type.Optional(Type.String({ description: "New description" })),
439
+ options: Type.Optional(Type.Object({
440
+ is_required: Type.Optional(Type.Boolean()),
441
+ select_options: Type.Optional(Type.Array(Type.Object({ name: Type.String() }))),
442
+ use_raw_format: Type.Optional(Type.Boolean()),
443
+ is_whole_number: Type.Optional(Type.Boolean()),
444
+ })),
445
+ });
446
+ api.registerTool({
447
+ name: "nex_update_attribute",
448
+ label: "Update Attribute",
449
+ description: "Update an existing attribute definition on an object type.",
450
+ parameters: UpdateAttributeParams,
451
+ async execute(_toolCallId, params) {
452
+ const { object_slug, attribute_id, name, description, options } = params;
453
+ const body = {};
454
+ if (name !== undefined)
455
+ body.name = name;
456
+ if (description !== undefined)
457
+ body.description = description;
458
+ if (options)
459
+ body.options = options;
460
+ const result = await client.patch(`/v1/objects/${encodeURIComponent(object_slug)}/attributes/${encodeURIComponent(attribute_id)}`, body);
461
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
462
+ },
463
+ });
464
+ const DeleteAttributeParams = Type.Object({
465
+ object_slug: Type.String({ description: "Object type slug" }),
466
+ attribute_id: Type.String({ description: "Attribute ID to delete" }),
467
+ });
468
+ api.registerTool({
469
+ name: "nex_delete_attribute",
470
+ label: "Delete Attribute",
471
+ description: "Delete an attribute from an object type. Removes the field and its data from all records. Cannot be undone.",
472
+ parameters: DeleteAttributeParams,
473
+ async execute(_toolCallId, params) {
474
+ const { object_slug, attribute_id } = params;
475
+ const result = await client.delete(`/v1/objects/${encodeURIComponent(object_slug)}/attributes/${encodeURIComponent(attribute_id)}`);
476
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
477
+ },
478
+ });
479
+ // --- Record tools ---
480
+ const CreateRecordParams = Type.Object({
481
+ object_slug: Type.String({ description: "Object type slug (e.g. 'person', 'company')" }),
482
+ attributes: Type.Record(Type.String(), Type.Unknown(), { description: "Record attributes — must include 'name'" }),
483
+ });
484
+ api.registerTool({
485
+ name: "nex_create_record",
486
+ label: "Create Record",
487
+ description: "Create a new record for an object type. Use only when you have clean, structured data with known attribute slugs.",
488
+ parameters: CreateRecordParams,
489
+ async execute(_toolCallId, params) {
490
+ const { object_slug, attributes } = params;
491
+ const result = await client.post(`/v1/objects/${encodeURIComponent(object_slug)}`, { attributes });
492
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
493
+ },
494
+ });
495
+ const UpsertRecordParams = Type.Object({
496
+ object_slug: Type.String({ description: "Object type slug (e.g. 'person', 'company')" }),
497
+ matching_attribute: Type.String({ description: "Attribute slug or ID to match on for dedup (e.g. 'email')" }),
498
+ attributes: Type.Record(Type.String(), Type.Unknown(), { description: "Record attributes — must include 'name' when creating" }),
499
+ });
500
+ api.registerTool({
501
+ name: "nex_upsert_record",
502
+ label: "Upsert Record",
503
+ description: "Create a record if it doesn't exist, or update it if a match is found on the specified attribute. Useful for deduplication.",
504
+ parameters: UpsertRecordParams,
505
+ async execute(_toolCallId, params) {
506
+ const { object_slug, matching_attribute, attributes } = params;
507
+ const result = await client.put(`/v1/objects/${encodeURIComponent(object_slug)}`, { matching_attribute, attributes });
508
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
509
+ },
510
+ });
511
+ const ListRecordsParams = Type.Object({
512
+ object_slug: Type.String({ description: "Object type slug (e.g. 'person', 'company')" }),
513
+ attributes: Type.Optional(Type.Union([
514
+ Type.String({ description: "'all', 'primary', or 'none'" }),
515
+ Type.Record(Type.String(), Type.Unknown()),
516
+ ])),
517
+ limit: Type.Optional(Type.Number({ description: "Number of records to return" })),
518
+ offset: Type.Optional(Type.Number({ description: "Pagination offset" })),
519
+ sort: Type.Optional(Type.Object({
520
+ attribute: Type.String({ description: "Attribute slug to sort by" }),
521
+ direction: Type.String({ description: "Sort direction: asc or desc" }),
522
+ })),
523
+ });
524
+ api.registerTool({
525
+ name: "nex_list_records",
526
+ label: "List Records",
527
+ description: "List records for an object type with optional filtering, sorting, and pagination.",
528
+ parameters: ListRecordsParams,
529
+ async execute(_toolCallId, params) {
530
+ const { object_slug, attributes, limit, offset, sort } = params;
531
+ const body = {};
532
+ if (attributes !== undefined)
533
+ body.attributes = attributes;
534
+ if (limit !== undefined)
535
+ body.limit = limit;
536
+ if (offset !== undefined)
537
+ body.offset = offset;
538
+ if (sort)
539
+ body.sort = sort;
540
+ const result = await client.post(`/v1/objects/${encodeURIComponent(object_slug)}/records`, body);
541
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
542
+ },
543
+ });
544
+ const GetRecordParams = Type.Object({
545
+ record_id: Type.String({ description: "Record ID" }),
546
+ });
547
+ api.registerTool({
548
+ name: "nex_get_record",
549
+ label: "Get Record",
550
+ description: "Retrieve a specific record by its ID, including all its attributes.",
551
+ parameters: GetRecordParams,
552
+ async execute(_toolCallId, params) {
553
+ const { record_id } = params;
554
+ const result = await client.get(`/v1/records/${encodeURIComponent(record_id)}`);
555
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
556
+ },
557
+ });
558
+ const UpdateRecordParams = Type.Object({
559
+ record_id: Type.String({ description: "Record ID to update" }),
560
+ attributes: Type.Record(Type.String(), Type.Unknown(), { description: "Attributes to update (only provided fields are changed)" }),
561
+ });
562
+ api.registerTool({
563
+ name: "nex_update_record",
564
+ label: "Update Record",
565
+ description: "Update specific attributes on an existing record. Only the provided attributes are changed.",
566
+ parameters: UpdateRecordParams,
567
+ async execute(_toolCallId, params) {
568
+ const { record_id, attributes } = params;
569
+ const result = await client.patch(`/v1/records/${encodeURIComponent(record_id)}`, { attributes });
570
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
571
+ },
572
+ });
573
+ const DeleteRecordParams = Type.Object({
574
+ record_id: Type.String({ description: "Record ID to delete" }),
575
+ });
576
+ api.registerTool({
577
+ name: "nex_delete_record",
578
+ label: "Delete Record",
579
+ description: "Permanently delete a record. This cannot be undone.",
580
+ parameters: DeleteRecordParams,
581
+ async execute(_toolCallId, params) {
582
+ const { record_id } = params;
583
+ const result = await client.delete(`/v1/records/${encodeURIComponent(record_id)}`);
584
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
585
+ },
586
+ });
587
+ const GetRecordTimelineParams = Type.Object({
588
+ record_id: Type.String({ description: "Record ID" }),
589
+ limit: Type.Optional(Type.Number({ description: "Max events (1-100, default: 50)" })),
590
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor from previous response" })),
591
+ });
592
+ api.registerTool({
593
+ name: "nex_get_record_timeline",
594
+ label: "Get Record Timeline",
595
+ description: "Get paginated timeline events for a record (tasks, notes, attribute changes, etc.).",
596
+ parameters: GetRecordTimelineParams,
597
+ async execute(_toolCallId, params) {
598
+ const { record_id, limit, cursor } = params;
599
+ const qs = new URLSearchParams();
600
+ if (limit !== undefined)
601
+ qs.set("limit", String(limit));
602
+ if (cursor)
603
+ qs.set("cursor", cursor);
604
+ const q = qs.toString();
605
+ const result = await client.get(`/v1/records/${encodeURIComponent(record_id)}/timeline${q ? `?${q}` : ""}`);
606
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
607
+ },
608
+ });
609
+ // --- Search tools ---
610
+ const SearchRecordsParams = Type.Object({
611
+ query: Type.String({ description: "Search query (1-500 characters)" }),
612
+ });
613
+ api.registerTool({
614
+ name: "nex_search_records",
615
+ label: "Search Records",
616
+ description: "Search records by name across all object types. Returns matches grouped by object type with relevance scores.",
617
+ parameters: SearchRecordsParams,
618
+ async execute(_toolCallId, params) {
619
+ const { query } = params;
620
+ const result = await client.post("/v1/search", { query });
621
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
622
+ },
623
+ });
624
+ // --- Relationship tools ---
625
+ const ListRelationshipDefsParams = Type.Object({});
626
+ api.registerTool({
627
+ name: "nex_list_relationship_defs",
628
+ label: "List Relationship Definitions",
629
+ description: "List all relationship type definitions in the workspace.",
630
+ parameters: ListRelationshipDefsParams,
631
+ async execute(_toolCallId, _params) {
632
+ const result = await client.get("/v1/relationships");
633
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
634
+ },
635
+ });
636
+ const CreateRelationshipDefParams = Type.Object({
637
+ type: Type.String({ description: "Relationship cardinality: one_to_one, one_to_many, many_to_many" }),
638
+ entity_definition_1_id: Type.String({ description: "First object definition ID" }),
639
+ entity_definition_2_id: Type.String({ description: "Second object definition ID" }),
640
+ entity_1_to_2_predicate: Type.Optional(Type.String({ description: "Label for 1→2 direction (e.g. 'works at')" })),
641
+ entity_2_to_1_predicate: Type.Optional(Type.String({ description: "Label for 2→1 direction (e.g. 'employs')" })),
642
+ });
643
+ api.registerTool({
644
+ name: "nex_create_relationship_def",
645
+ label: "Create Relationship Definition",
646
+ description: "Define a new relationship type between two object types (e.g. person 'works at' company).",
647
+ parameters: CreateRelationshipDefParams,
648
+ async execute(_toolCallId, params) {
649
+ const { type, entity_definition_1_id, entity_definition_2_id, entity_1_to_2_predicate, entity_2_to_1_predicate } = params;
650
+ const body = { type, entity_definition_1_id, entity_definition_2_id };
651
+ if (entity_1_to_2_predicate !== undefined)
652
+ body.entity_1_to_2_predicate = entity_1_to_2_predicate;
653
+ if (entity_2_to_1_predicate !== undefined)
654
+ body.entity_2_to_1_predicate = entity_2_to_1_predicate;
655
+ const result = await client.post("/v1/relationships", body);
656
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
657
+ },
658
+ });
659
+ const DeleteRelationshipDefParams = Type.Object({
660
+ id: Type.String({ description: "Relationship definition ID to delete" }),
661
+ });
662
+ api.registerTool({
663
+ name: "nex_delete_relationship_def",
664
+ label: "Delete Relationship Definition",
665
+ description: "Delete a relationship type definition. Removes all instances of this relationship. Cannot be undone.",
666
+ parameters: DeleteRelationshipDefParams,
667
+ async execute(_toolCallId, params) {
668
+ const { id } = params;
669
+ const result = await client.delete(`/v1/relationships/${encodeURIComponent(id)}`);
670
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
671
+ },
672
+ });
673
+ const CreateRelationshipParams = Type.Object({
674
+ record_id: Type.String({ description: "Record ID to create the relationship from" }),
675
+ definition_id: Type.String({ description: "Relationship definition ID" }),
676
+ entity_1_id: Type.String({ description: "First record ID" }),
677
+ entity_2_id: Type.String({ description: "Second record ID" }),
678
+ });
679
+ api.registerTool({
680
+ name: "nex_create_relationship",
681
+ label: "Create Relationship",
682
+ description: "Link two records using an existing relationship definition.",
683
+ parameters: CreateRelationshipParams,
684
+ async execute(_toolCallId, params) {
685
+ const { record_id, definition_id, entity_1_id, entity_2_id } = params;
686
+ const result = await client.post(`/v1/records/${encodeURIComponent(record_id)}/relationships`, {
687
+ definition_id,
688
+ entity_1_id,
689
+ entity_2_id,
690
+ });
691
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
692
+ },
693
+ });
694
+ const DeleteRelationshipParams = Type.Object({
695
+ record_id: Type.String({ description: "Record ID" }),
696
+ relationship_id: Type.String({ description: "Relationship instance ID to delete" }),
697
+ });
698
+ api.registerTool({
699
+ name: "nex_delete_relationship",
700
+ label: "Delete Relationship",
701
+ description: "Remove a relationship between two records. Cannot be undone.",
702
+ parameters: DeleteRelationshipParams,
703
+ async execute(_toolCallId, params) {
704
+ const { record_id, relationship_id } = params;
705
+ const result = await client.delete(`/v1/records/${encodeURIComponent(record_id)}/relationships/${encodeURIComponent(relationship_id)}`);
706
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
707
+ },
708
+ });
709
+ // --- List tools ---
710
+ const ListListsParams = Type.Object({
711
+ object_slug: Type.String({ description: "Object type slug (e.g. 'person', 'company')" }),
712
+ include_attributes: Type.Optional(Type.Boolean({ description: "Include attribute definitions" })),
713
+ });
714
+ api.registerTool({
715
+ name: "nex_list_lists",
716
+ label: "List Object Lists",
717
+ description: "Get all lists associated with an object type.",
718
+ parameters: ListListsParams,
719
+ async execute(_toolCallId, params) {
720
+ const { object_slug, include_attributes } = params;
721
+ const qs = new URLSearchParams();
722
+ if (include_attributes)
723
+ qs.set("include_attributes", "true");
724
+ const q = qs.toString();
725
+ const result = await client.get(`/v1/objects/${encodeURIComponent(object_slug)}/lists${q ? `?${q}` : ""}`);
726
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
727
+ },
728
+ });
729
+ const CreateListParams = Type.Object({
730
+ object_slug: Type.String({ description: "Object type slug" }),
731
+ name: Type.String({ description: "List display name" }),
732
+ slug: Type.String({ description: "URL-safe identifier" }),
733
+ name_plural: Type.Optional(Type.String({ description: "Plural name" })),
734
+ description: Type.Optional(Type.String({ description: "List description" })),
735
+ });
736
+ api.registerTool({
737
+ name: "nex_create_list",
738
+ label: "Create List",
739
+ description: "Create a new list under an object type.",
740
+ parameters: CreateListParams,
741
+ async execute(_toolCallId, params) {
742
+ const { object_slug, name, slug, name_plural, description } = params;
743
+ const body = { name, slug };
744
+ if (name_plural !== undefined)
745
+ body.name_plural = name_plural;
746
+ if (description !== undefined)
747
+ body.description = description;
748
+ const result = await client.post(`/v1/objects/${encodeURIComponent(object_slug)}/lists`, body);
749
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
750
+ },
751
+ });
752
+ const GetListParams = Type.Object({
753
+ list_id: Type.String({ description: "List ID" }),
754
+ });
755
+ api.registerTool({
756
+ name: "nex_get_list",
757
+ label: "Get List",
758
+ description: "Get a list definition by ID.",
759
+ parameters: GetListParams,
760
+ async execute(_toolCallId, params) {
761
+ const { list_id } = params;
762
+ const result = await client.get(`/v1/lists/${encodeURIComponent(list_id)}`);
763
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
764
+ },
765
+ });
766
+ const DeleteListParams = Type.Object({
767
+ list_id: Type.String({ description: "List ID to delete" }),
768
+ });
769
+ api.registerTool({
770
+ name: "nex_delete_list",
771
+ label: "Delete List",
772
+ description: "Delete a list definition. Cannot be undone.",
773
+ parameters: DeleteListParams,
774
+ async execute(_toolCallId, params) {
775
+ const { list_id } = params;
776
+ const result = await client.delete(`/v1/lists/${encodeURIComponent(list_id)}`);
777
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
778
+ },
779
+ });
780
+ const AddListMemberParams = Type.Object({
781
+ list_id: Type.String({ description: "List ID" }),
782
+ parent_id: Type.String({ description: "ID of the existing record to add" }),
783
+ attributes: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "List-specific attribute values" })),
784
+ });
785
+ api.registerTool({
786
+ name: "nex_add_list_member",
787
+ label: "Add List Member",
788
+ description: "Add an existing record to a list with optional list-specific attributes.",
789
+ parameters: AddListMemberParams,
790
+ async execute(_toolCallId, params) {
791
+ const { list_id, parent_id, attributes } = params;
792
+ const body = { parent_id };
793
+ if (attributes)
794
+ body.attributes = attributes;
795
+ const result = await client.post(`/v1/lists/${encodeURIComponent(list_id)}`, body);
796
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
797
+ },
798
+ });
799
+ const UpsertListMemberParams = Type.Object({
800
+ list_id: Type.String({ description: "List ID" }),
801
+ parent_id: Type.String({ description: "ID of the record" }),
802
+ attributes: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "List-specific attribute values" })),
803
+ });
804
+ api.registerTool({
805
+ name: "nex_upsert_list_member",
806
+ label: "Upsert List Member",
807
+ description: "Add a record to a list, or update its list-specific attributes if already a member.",
808
+ parameters: UpsertListMemberParams,
809
+ async execute(_toolCallId, params) {
810
+ const { list_id, parent_id, attributes } = params;
811
+ const body = { parent_id };
812
+ if (attributes)
813
+ body.attributes = attributes;
814
+ const result = await client.put(`/v1/lists/${encodeURIComponent(list_id)}`, body);
815
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
816
+ },
817
+ });
818
+ const ListListRecordsParams = Type.Object({
819
+ list_id: Type.String({ description: "List ID" }),
820
+ attributes: Type.Optional(Type.Union([
821
+ Type.String({ description: "'all', 'primary', or 'none'" }),
822
+ Type.Record(Type.String(), Type.Unknown()),
823
+ ])),
824
+ limit: Type.Optional(Type.Number({ description: "Number of records to return" })),
825
+ offset: Type.Optional(Type.Number({ description: "Pagination offset" })),
826
+ sort: Type.Optional(Type.Object({
827
+ attribute: Type.String({ description: "Attribute slug to sort by" }),
828
+ direction: Type.String({ description: "Sort direction: asc or desc" }),
829
+ })),
830
+ });
831
+ api.registerTool({
832
+ name: "nex_list_list_records",
833
+ label: "List List Records",
834
+ description: "Get paginated records from a specific list.",
835
+ parameters: ListListRecordsParams,
836
+ async execute(_toolCallId, params) {
837
+ const { list_id, attributes, limit, offset, sort } = params;
838
+ const body = {};
839
+ if (attributes !== undefined)
840
+ body.attributes = attributes;
841
+ if (limit !== undefined)
842
+ body.limit = limit;
843
+ if (offset !== undefined)
844
+ body.offset = offset;
845
+ if (sort)
846
+ body.sort = sort;
847
+ const result = await client.post(`/v1/lists/${encodeURIComponent(list_id)}/records`, body);
848
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
849
+ },
850
+ });
851
+ const UpdateListRecordParams = Type.Object({
852
+ list_id: Type.String({ description: "List ID" }),
853
+ record_id: Type.String({ description: "Record ID within the list" }),
854
+ attributes: Type.Record(Type.String(), Type.Unknown(), { description: "Attributes to update" }),
855
+ });
856
+ api.registerTool({
857
+ name: "nex_update_list_record",
858
+ label: "Update List Record",
859
+ description: "Update list-specific attributes for a record within a list.",
860
+ parameters: UpdateListRecordParams,
861
+ async execute(_toolCallId, params) {
862
+ const { list_id, record_id, attributes } = params;
863
+ const result = await client.patch(`/v1/lists/${encodeURIComponent(list_id)}/records/${encodeURIComponent(record_id)}`, { attributes });
864
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
865
+ },
866
+ });
867
+ const RemoveListRecordParams = Type.Object({
868
+ list_id: Type.String({ description: "List ID" }),
869
+ record_id: Type.String({ description: "Record ID to remove from the list" }),
870
+ });
871
+ api.registerTool({
872
+ name: "nex_remove_list_record",
873
+ label: "Remove List Record",
874
+ description: "Remove a record from a list. The record itself is not deleted.",
875
+ parameters: RemoveListRecordParams,
876
+ async execute(_toolCallId, params) {
877
+ const { list_id, record_id } = params;
878
+ const result = await client.delete(`/v1/lists/${encodeURIComponent(list_id)}/records/${encodeURIComponent(record_id)}`);
879
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
880
+ },
881
+ });
882
+ // --- Task tools ---
883
+ const CreateTaskParams = Type.Object({
884
+ title: Type.String({ description: "Task title" }),
885
+ description: Type.Optional(Type.String({ description: "Task description" })),
886
+ priority: Type.Optional(Type.String({ description: "Task priority: low, medium, high, urgent" })),
887
+ due_date: Type.Optional(Type.String({ description: "Due date in RFC3339 format" })),
888
+ entity_ids: Type.Optional(Type.Array(Type.String({ description: "Record ID" }))),
889
+ assignee_ids: Type.Optional(Type.Array(Type.String({ description: "User ID" }))),
890
+ });
891
+ api.registerTool({
892
+ name: "nex_create_task",
893
+ label: "Create Task",
894
+ description: "Create a new task, optionally linked to records and assigned to users.",
895
+ parameters: CreateTaskParams,
896
+ async execute(_toolCallId, params) {
897
+ const { title, description, priority, due_date, entity_ids, assignee_ids } = params;
898
+ const body = { title };
899
+ if (description !== undefined)
900
+ body.description = description;
901
+ if (priority !== undefined)
902
+ body.priority = priority;
903
+ if (due_date !== undefined)
904
+ body.due_date = due_date;
905
+ if (entity_ids)
906
+ body.entity_ids = entity_ids;
907
+ if (assignee_ids)
908
+ body.assignee_ids = assignee_ids;
909
+ const result = await client.post("/v1/tasks", body);
910
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
911
+ },
912
+ });
913
+ const ListTasksParams = Type.Object({
914
+ entity_id: Type.Optional(Type.String({ description: "Filter by associated record ID" })),
915
+ assignee_id: Type.Optional(Type.String({ description: "Filter by assignee user ID" })),
916
+ search: Type.Optional(Type.String({ description: "Search task titles" })),
917
+ is_completed: Type.Optional(Type.Boolean({ description: "Filter by completion status" })),
918
+ limit: Type.Optional(Type.Number({ description: "Max results (1-500, default: 100)" })),
919
+ offset: Type.Optional(Type.Number({ description: "Pagination offset" })),
920
+ });
921
+ api.registerTool({
922
+ name: "nex_list_tasks",
923
+ label: "List Tasks",
924
+ description: "List tasks with optional filtering by record, assignee, completion status, or search query.",
925
+ parameters: ListTasksParams,
926
+ async execute(_toolCallId, params) {
927
+ const { entity_id, assignee_id, search, is_completed, limit, offset } = params;
928
+ const qs = new URLSearchParams();
929
+ if (entity_id)
930
+ qs.set("entity_id", entity_id);
931
+ if (assignee_id)
932
+ qs.set("assignee_id", assignee_id);
933
+ if (search)
934
+ qs.set("search", search);
935
+ if (is_completed !== undefined)
936
+ qs.set("is_completed", String(is_completed));
937
+ if (limit !== undefined)
938
+ qs.set("limit", String(limit));
939
+ if (offset !== undefined)
940
+ qs.set("offset", String(offset));
941
+ const q = qs.toString();
942
+ const result = await client.get(`/v1/tasks${q ? `?${q}` : ""}`);
943
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
944
+ },
945
+ });
946
+ const GetTaskParams = Type.Object({
947
+ task_id: Type.String({ description: "Task ID" }),
948
+ });
949
+ api.registerTool({
950
+ name: "nex_get_task",
951
+ label: "Get Task",
952
+ description: "Get a single task by ID.",
953
+ parameters: GetTaskParams,
954
+ async execute(_toolCallId, params) {
955
+ const { task_id } = params;
956
+ const result = await client.get(`/v1/tasks/${encodeURIComponent(task_id)}`);
957
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
958
+ },
959
+ });
960
+ const UpdateTaskParams = Type.Object({
961
+ task_id: Type.String({ description: "Task ID to update" }),
962
+ title: Type.Optional(Type.String({ description: "New title" })),
963
+ description: Type.Optional(Type.String({ description: "New description" })),
964
+ priority: Type.Optional(Type.String({ description: "New priority" })),
965
+ due_date: Type.Optional(Type.String({ description: "New due date in RFC3339 format" })),
966
+ is_completed: Type.Optional(Type.Boolean({ description: "Mark complete/incomplete" })),
967
+ entity_ids: Type.Optional(Type.Array(Type.String({ description: "Record ID" }))),
968
+ assignee_ids: Type.Optional(Type.Array(Type.String({ description: "User ID" }))),
969
+ });
970
+ api.registerTool({
971
+ name: "nex_update_task",
972
+ label: "Update Task",
973
+ description: "Update a task's fields. All fields are optional — only provided fields are changed.",
974
+ parameters: UpdateTaskParams,
975
+ async execute(_toolCallId, params) {
976
+ const { task_id, title, description, priority, due_date, is_completed, entity_ids, assignee_ids } = params;
977
+ const body = {};
978
+ if (title !== undefined)
979
+ body.title = title;
980
+ if (description !== undefined)
981
+ body.description = description;
982
+ if (priority !== undefined)
983
+ body.priority = priority;
984
+ if (due_date !== undefined)
985
+ body.due_date = due_date;
986
+ if (is_completed !== undefined)
987
+ body.is_completed = is_completed;
988
+ if (entity_ids !== undefined)
989
+ body.entity_ids = entity_ids;
990
+ if (assignee_ids !== undefined)
991
+ body.assignee_ids = assignee_ids;
992
+ const result = await client.patch(`/v1/tasks/${encodeURIComponent(task_id)}`, body);
993
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
994
+ },
995
+ });
996
+ const DeleteTaskParams = Type.Object({
997
+ task_id: Type.String({ description: "Task ID to delete" }),
998
+ });
999
+ api.registerTool({
1000
+ name: "nex_delete_task",
1001
+ label: "Delete Task",
1002
+ description: "Archive a task (soft delete). Cannot be undone via API.",
1003
+ parameters: DeleteTaskParams,
1004
+ async execute(_toolCallId, params) {
1005
+ const { task_id } = params;
1006
+ const result = await client.delete(`/v1/tasks/${encodeURIComponent(task_id)}`);
1007
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1008
+ },
1009
+ });
1010
+ // --- Note tools ---
1011
+ const CreateNoteParams = Type.Object({
1012
+ title: Type.String({ description: "Note title" }),
1013
+ content: Type.Optional(Type.String({ description: "Note body text" })),
1014
+ entity_id: Type.Optional(Type.String({ description: "Associated record ID" })),
1015
+ });
1016
+ api.registerTool({
1017
+ name: "nex_create_note",
1018
+ label: "Create Note",
1019
+ description: "Create a new note, optionally linked to a record.",
1020
+ parameters: CreateNoteParams,
1021
+ async execute(_toolCallId, params) {
1022
+ const { title, content, entity_id } = params;
1023
+ const body = { title };
1024
+ if (content !== undefined)
1025
+ body.content = content;
1026
+ if (entity_id !== undefined)
1027
+ body.entity_id = entity_id;
1028
+ const result = await client.post("/v1/notes", body);
1029
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1030
+ },
1031
+ });
1032
+ const ListNotesParams = Type.Object({
1033
+ entity_id: Type.Optional(Type.String({ description: "Filter notes by associated record ID" })),
1034
+ });
1035
+ api.registerTool({
1036
+ name: "nex_list_notes",
1037
+ label: "List Notes",
1038
+ description: "List notes, optionally filtered by associated record.",
1039
+ parameters: ListNotesParams,
1040
+ async execute(_toolCallId, params) {
1041
+ const { entity_id } = params;
1042
+ const qs = new URLSearchParams();
1043
+ if (entity_id)
1044
+ qs.set("entity_id", entity_id);
1045
+ const q = qs.toString();
1046
+ const result = await client.get(`/v1/notes${q ? `?${q}` : ""}`);
1047
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1048
+ },
1049
+ });
1050
+ const GetNoteParams = Type.Object({
1051
+ note_id: Type.String({ description: "Note ID" }),
1052
+ });
1053
+ api.registerTool({
1054
+ name: "nex_get_note",
1055
+ label: "Get Note",
1056
+ description: "Get a single note by ID.",
1057
+ parameters: GetNoteParams,
1058
+ async execute(_toolCallId, params) {
1059
+ const { note_id } = params;
1060
+ const result = await client.get(`/v1/notes/${encodeURIComponent(note_id)}`);
1061
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1062
+ },
1063
+ });
1064
+ const UpdateNoteParams = Type.Object({
1065
+ note_id: Type.String({ description: "Note ID to update" }),
1066
+ title: Type.Optional(Type.String({ description: "New title" })),
1067
+ content: Type.Optional(Type.String({ description: "New content" })),
1068
+ entity_id: Type.Optional(Type.String({ description: "Change associated record" })),
1069
+ });
1070
+ api.registerTool({
1071
+ name: "nex_update_note",
1072
+ label: "Update Note",
1073
+ description: "Update a note's fields. All fields are optional — only provided fields are changed.",
1074
+ parameters: UpdateNoteParams,
1075
+ async execute(_toolCallId, params) {
1076
+ const { note_id, title, content, entity_id } = params;
1077
+ const body = {};
1078
+ if (title !== undefined)
1079
+ body.title = title;
1080
+ if (content !== undefined)
1081
+ body.content = content;
1082
+ if (entity_id !== undefined)
1083
+ body.entity_id = entity_id;
1084
+ const result = await client.patch(`/v1/notes/${encodeURIComponent(note_id)}`, body);
1085
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1086
+ },
1087
+ });
1088
+ const DeleteNoteParams = Type.Object({
1089
+ note_id: Type.String({ description: "Note ID to delete" }),
1090
+ });
1091
+ api.registerTool({
1092
+ name: "nex_delete_note",
1093
+ label: "Delete Note",
1094
+ description: "Archive a note (soft delete). Cannot be undone via API.",
1095
+ parameters: DeleteNoteParams,
1096
+ async execute(_toolCallId, params) {
1097
+ const { note_id } = params;
1098
+ const result = await client.delete(`/v1/notes/${encodeURIComponent(note_id)}`);
1099
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1100
+ },
1101
+ });
1102
+ // --- Context tools ---
1103
+ const GetArtifactStatusParams = Type.Object({
1104
+ artifact_id: Type.String({ description: "The artifact ID returned by nex_remember or add_context" }),
1105
+ });
1106
+ api.registerTool({
1107
+ name: "nex_get_artifact_status",
1108
+ label: "Get Artifact Status",
1109
+ description: "Check the processing status and results of a previously submitted text artifact. Poll until status is 'completed' or 'failed'.",
1110
+ parameters: GetArtifactStatusParams,
1111
+ async execute(_toolCallId, params) {
1112
+ const { artifact_id } = params;
1113
+ const result = await client.get(`/v1/context/artifacts/${encodeURIComponent(artifact_id)}`);
1114
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1115
+ },
1116
+ });
1117
+ const GetInsightsParams = Type.Object({
1118
+ last: Type.Optional(Type.String({ description: "Duration window, e.g. '30m', '2h', '1h30m'" })),
1119
+ from: Type.Optional(Type.String({ description: "Start of time range in RFC3339 format" })),
1120
+ to: Type.Optional(Type.String({ description: "End of time range in RFC3339 format" })),
1121
+ limit: Type.Optional(Type.Number({ description: "Max results (default: 20, max: 100)" })),
1122
+ });
1123
+ api.registerTool({
1124
+ name: "nex_get_insights",
1125
+ label: "Get Insights",
1126
+ description: "Query insights by time window. Returns discovered opportunities, risks, relationship changes, milestones, and other insights.",
1127
+ parameters: GetInsightsParams,
1128
+ async execute(_toolCallId, params) {
1129
+ const { last, from, to, limit } = params;
1130
+ const qs = new URLSearchParams();
1131
+ if (last)
1132
+ qs.set("last", last);
1133
+ if (from)
1134
+ qs.set("from", from);
1135
+ if (to)
1136
+ qs.set("to", to);
1137
+ if (limit !== undefined)
1138
+ qs.set("limit", String(limit));
1139
+ const q = qs.toString();
1140
+ const result = await client.get(`/v1/insights${q ? `?${q}` : ""}`);
1141
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
1142
+ },
1143
+ });
1144
+ // --- Commands ---
1145
+ api.registerCommand({
1146
+ name: "nex:recall",
1147
+ description: "Search your Nex knowledge base. Usage: /nex:recall <query>",
1148
+ acceptsArgs: true,
1149
+ async handler(ctx) {
1150
+ const query = ctx.args?.trim();
1151
+ if (!query) {
1152
+ return { text: "Usage: /nex:recall <query>" };
1153
+ }
1154
+ try {
1155
+ const result = await client.ask(query);
1156
+ const parts = [result.answer];
1157
+ if (result.entity_references && result.entity_references.length > 0) {
1158
+ const typeLabel = (t) => {
1159
+ switch (t) {
1160
+ case "14": return "Person";
1161
+ case "15": return "Company";
1162
+ default: return "Entity";
1163
+ }
1164
+ };
1165
+ // Deduplicate by name+type
1166
+ const seen = new Set();
1167
+ const unique = result.entity_references.filter((ref) => {
1168
+ const key = `${ref.name}:${ref.type}`;
1169
+ if (seen.has(key))
1170
+ return false;
1171
+ seen.add(key);
1172
+ return true;
1173
+ });
1174
+ parts.push("\n\nSources:");
1175
+ for (const ref of unique) {
1176
+ parts.push(`\n• ${ref.name} · ${typeLabel(ref.type)}`);
1177
+ }
1178
+ }
1179
+ return { text: parts.join("") };
1180
+ }
1181
+ catch (err) {
1182
+ return { text: `Recall failed: ${err instanceof Error ? err.message : String(err)}` };
1183
+ }
1184
+ },
1185
+ });
1186
+ api.registerCommand({
1187
+ name: "nex:remember",
1188
+ description: "Store information in your Nex knowledge base. Usage: /nex:remember <text>",
1189
+ acceptsArgs: true,
1190
+ async handler(ctx) {
1191
+ const text = ctx.args?.trim();
1192
+ if (!text) {
1193
+ return { text: "Usage: /nex:remember <text>" };
1194
+ }
1195
+ try {
1196
+ await rateLimiter.enqueue(async () => {
1197
+ await client.ingest(text, "manual-command");
1198
+ });
1199
+ return { text: "Remembered." };
1200
+ }
1201
+ catch (err) {
1202
+ return { text: `Remember failed: ${err instanceof Error ? err.message : String(err)}` };
1203
+ }
1204
+ },
1205
+ });
1206
+ api.registerCommand({
1207
+ name: "nex:scan",
1208
+ description: "Scan a directory for files and ingest into Nex. Usage: /nex:scan [dir]",
1209
+ acceptsArgs: true,
1210
+ async handler(ctx) {
1211
+ const dir = ctx.args?.trim() || ".";
1212
+ try {
1213
+ const result = await scanFilesUtil(dir, client);
1214
+ return {
1215
+ text: `Scanned ${result.scanned} file(s), skipped ${result.skipped}, errors ${result.errors}.`,
1216
+ };
1217
+ }
1218
+ catch (err) {
1219
+ return { text: `Scan failed: ${err instanceof Error ? err.message : String(err)}` };
1220
+ }
1221
+ },
1222
+ });
1223
+ log.info(`Nex memory plugin registered (recall: ${cfg.autoRecall}, capture: ${cfg.autoCapture})`);
1224
+ },
1225
+ };
1226
+ export default plugin;
1227
+ //# sourceMappingURL=index.js.map