@objectstack/service-ai 4.0.1 → 4.0.2
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +9 -0
- package/dist/index.cjs +1120 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -78
- package/dist/index.d.ts +316 -78
- package/dist/index.js +1105 -63
- package/dist/index.js.map +1 -1
- package/package.json +26 -4
- package/src/__tests__/ai-service.test.ts +248 -27
- package/src/__tests__/auth-and-toolcalling.test.ts +30 -28
- package/src/__tests__/chatbot-features.test.ts +229 -82
- package/src/__tests__/metadata-tools.test.ts +964 -0
- package/src/__tests__/objectql-conversation-service.test.ts +34 -16
- package/src/__tests__/vercel-stream-encoder.test.ts +263 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/memory-adapter.ts +17 -9
- package/src/adapters/vercel-adapter.ts +148 -0
- package/src/agent-runtime.ts +27 -3
- package/src/agents/index.ts +1 -0
- package/src/agents/metadata-assistant-agent.ts +87 -0
- package/src/ai-service.ts +68 -36
- package/src/conversation/in-memory-conversation-service.ts +2 -2
- package/src/conversation/objectql-conversation-service.ts +67 -18
- package/src/index.ts +21 -2
- package/src/plugin.ts +166 -9
- package/src/routes/agent-routes.ts +26 -3
- package/src/routes/ai-routes.ts +156 -13
- package/src/stream/index.ts +3 -0
- package/src/stream/vercel-stream-encoder.ts +129 -0
- package/src/tools/add-field.tool.ts +70 -0
- package/src/tools/create-object.tool.ts +66 -0
- package/src/tools/delete-field.tool.ts +38 -0
- package/src/tools/describe-metadata-object.tool.ts +32 -0
- package/src/tools/index.ts +12 -1
- package/src/tools/list-metadata-objects.tool.ts +34 -0
- package/src/tools/metadata-tools.ts +430 -0
- package/src/tools/modify-field.tool.ts +44 -0
- package/src/tools/tool-registry.ts +32 -9
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { defineTool } from '@objectstack/spec/ai';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* add_field — AI Tool Metadata
|
|
7
|
+
*
|
|
8
|
+
* Adds a new field (column) to an existing data object.
|
|
9
|
+
* Validates snake_case for objectName, field name, reference,
|
|
10
|
+
* and select option values before merging into the definition.
|
|
11
|
+
*/
|
|
12
|
+
export const addFieldTool = defineTool({
|
|
13
|
+
name: 'add_field',
|
|
14
|
+
label: 'Add Field',
|
|
15
|
+
description:
|
|
16
|
+
'Adds a new field (column) to an existing data object. ' +
|
|
17
|
+
'Use this when the user wants to add a property, column, or attribute to a table.',
|
|
18
|
+
category: 'data',
|
|
19
|
+
builtIn: true,
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
objectName: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Target object machine name (snake_case)',
|
|
26
|
+
},
|
|
27
|
+
name: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Field machine name (snake_case, e.g. due_date)',
|
|
30
|
+
},
|
|
31
|
+
label: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Human-readable field label (e.g. Due Date)',
|
|
34
|
+
},
|
|
35
|
+
type: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Field data type',
|
|
38
|
+
enum: ['text', 'textarea', 'number', 'boolean', 'date', 'datetime', 'select', 'lookup', 'formula', 'autonumber'],
|
|
39
|
+
},
|
|
40
|
+
required: {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
description: 'Whether the field is required',
|
|
43
|
+
},
|
|
44
|
+
defaultValue: {
|
|
45
|
+
description: 'Default value for the field',
|
|
46
|
+
},
|
|
47
|
+
options: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
description: 'Options for select/picklist fields',
|
|
50
|
+
items: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
label: { type: 'string' },
|
|
54
|
+
value: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Option machine identifier (lowercase snake_case, e.g. high_priority)',
|
|
57
|
+
pattern: '^[a-z_][a-z0-9_]*$',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
reference: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Referenced object name for lookup fields (snake_case, e.g. account)',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ['objectName', 'name', 'type'],
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { defineTool } from '@objectstack/spec/ai';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* create_object — AI Tool Metadata
|
|
7
|
+
*
|
|
8
|
+
* Creates a new data object (table) with schema validation.
|
|
9
|
+
* Validates snake_case naming for object and initial fields,
|
|
10
|
+
* checks for duplicates, and registers the object definition.
|
|
11
|
+
*/
|
|
12
|
+
export const createObjectTool = defineTool({
|
|
13
|
+
name: 'create_object',
|
|
14
|
+
label: 'Create Object',
|
|
15
|
+
description:
|
|
16
|
+
'Creates a new data object (table) with the specified name, label, and optional field definitions. ' +
|
|
17
|
+
'Use this when the user wants to create a new entity, table, or data model.',
|
|
18
|
+
category: 'data',
|
|
19
|
+
builtIn: true,
|
|
20
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
21
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
22
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
23
|
+
// should only be set once server-side approval gating is implemented to
|
|
24
|
+
// avoid giving users a false sense of safety.
|
|
25
|
+
parameters: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
name: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Machine name for the object (snake_case, e.g. project_task)',
|
|
31
|
+
},
|
|
32
|
+
label: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Human-readable display name (e.g. Project Task)',
|
|
35
|
+
},
|
|
36
|
+
fields: {
|
|
37
|
+
type: 'array',
|
|
38
|
+
description: 'Initial fields to create with the object',
|
|
39
|
+
items: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
name: { type: 'string', description: 'Field machine name (snake_case)' },
|
|
43
|
+
label: { type: 'string', description: 'Field display name' },
|
|
44
|
+
type: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Field data type',
|
|
47
|
+
enum: ['text', 'textarea', 'number', 'boolean', 'date', 'datetime', 'select', 'lookup', 'formula', 'autonumber'],
|
|
48
|
+
},
|
|
49
|
+
required: { type: 'boolean', description: 'Whether the field is required' },
|
|
50
|
+
},
|
|
51
|
+
required: ['name', 'type'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
enableFeatures: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
description: 'Object capability flags',
|
|
57
|
+
properties: {
|
|
58
|
+
trackHistory: { type: 'boolean' },
|
|
59
|
+
apiEnabled: { type: 'boolean' },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ['name', 'label'],
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { defineTool } from '@objectstack/spec/ai';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* delete_field — AI Tool Metadata
|
|
7
|
+
*
|
|
8
|
+
* Removes a field (column) from an existing data object.
|
|
9
|
+
* This is a destructive operation.
|
|
10
|
+
*/
|
|
11
|
+
export const deleteFieldTool = defineTool({
|
|
12
|
+
name: 'delete_field',
|
|
13
|
+
label: 'Delete Field',
|
|
14
|
+
description:
|
|
15
|
+
'Removes a field (column) from an existing data object. This is a destructive operation. ' +
|
|
16
|
+
'Use this when the user explicitly wants to remove an attribute or column from a table.',
|
|
17
|
+
category: 'data',
|
|
18
|
+
builtIn: true,
|
|
19
|
+
// NOTE: requiresConfirmation is intentionally false (default) because the
|
|
20
|
+
// server-side tool-call loop in AIService.chatWithTools/streamChatWithTools
|
|
21
|
+
// executes tool calls immediately without checking this flag. The flag
|
|
22
|
+
// should only be set once server-side approval gating is implemented.
|
|
23
|
+
parameters: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
objectName: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Target object machine name (snake_case)',
|
|
29
|
+
},
|
|
30
|
+
fieldName: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Field machine name to delete (snake_case)',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['objectName', 'fieldName'],
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { defineTool } from '@objectstack/spec/ai';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* describe_metadata_object — AI Tool Metadata
|
|
7
|
+
*
|
|
8
|
+
* Returns the full metadata schema of a data object including all
|
|
9
|
+
* fields, types, relationships, and configuration. Uses a unique name
|
|
10
|
+
* (`describe_metadata_object`) to avoid collision with the data-tools
|
|
11
|
+
* `describe_object` tool.
|
|
12
|
+
*/
|
|
13
|
+
export const describeMetadataObjectTool = defineTool({
|
|
14
|
+
name: 'describe_metadata_object',
|
|
15
|
+
label: 'Describe Metadata Object',
|
|
16
|
+
description:
|
|
17
|
+
'Returns the full metadata schema details of a data object, including all fields, types, relationships, and configuration. ' +
|
|
18
|
+
'Use this when the user wants to inspect or understand the metadata structure of a specific table or entity.',
|
|
19
|
+
category: 'data',
|
|
20
|
+
builtIn: true,
|
|
21
|
+
parameters: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
objectName: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Object machine name to describe (snake_case)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ['objectName'],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
});
|
package/src/tools/index.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
export { ToolRegistry } from './tool-registry.js';
|
|
4
|
-
export type { ToolHandler } from './tool-registry.js';
|
|
4
|
+
export type { ToolHandler, ToolExecutionResult } from './tool-registry.js';
|
|
5
5
|
|
|
6
6
|
export { registerDataTools, DATA_TOOL_DEFINITIONS } from './data-tools.js';
|
|
7
7
|
export type { DataToolContext } from './data-tools.js';
|
|
8
|
+
|
|
9
|
+
export { registerMetadataTools, METADATA_TOOL_DEFINITIONS } from './metadata-tools.js';
|
|
10
|
+
export type { MetadataToolContext } from './metadata-tools.js';
|
|
11
|
+
|
|
12
|
+
// Individual tool metadata exports
|
|
13
|
+
export { createObjectTool } from './create-object.tool.js';
|
|
14
|
+
export { addFieldTool } from './add-field.tool.js';
|
|
15
|
+
export { modifyFieldTool } from './modify-field.tool.js';
|
|
16
|
+
export { deleteFieldTool } from './delete-field.tool.js';
|
|
17
|
+
export { listMetadataObjectsTool } from './list-metadata-objects.tool.js';
|
|
18
|
+
export { describeMetadataObjectTool } from './describe-metadata-object.tool.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { defineTool } from '@objectstack/spec/ai';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* list_metadata_objects — AI Tool Metadata
|
|
7
|
+
*
|
|
8
|
+
* Lists all registered metadata objects (tables) with optional filtering.
|
|
9
|
+
* Uses a unique name (`list_metadata_objects`) to avoid collision with
|
|
10
|
+
* the data-tools `list_objects` tool.
|
|
11
|
+
*/
|
|
12
|
+
export const listMetadataObjectsTool = defineTool({
|
|
13
|
+
name: 'list_metadata_objects',
|
|
14
|
+
label: 'List Metadata Objects',
|
|
15
|
+
description:
|
|
16
|
+
'Lists all registered metadata objects (tables) in the current environment. ' +
|
|
17
|
+
'Use this when the user wants to see what tables, entities, or data models are defined in metadata.',
|
|
18
|
+
category: 'data',
|
|
19
|
+
builtIn: true,
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
filter: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Optional name or label substring to filter objects',
|
|
26
|
+
},
|
|
27
|
+
includeFields: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Whether to include field summaries for each object (default: false)',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { IMetadataService } from '@objectstack/spec/contracts';
|
|
4
|
+
import type { Tool } from '@objectstack/spec/ai';
|
|
5
|
+
import type { ToolHandler } from './tool-registry.js';
|
|
6
|
+
import type { ToolRegistry } from './tool-registry.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Tool Metadata — individual .tool.ts files (single source of truth)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export { createObjectTool } from './create-object.tool.js';
|
|
13
|
+
export { addFieldTool } from './add-field.tool.js';
|
|
14
|
+
export { modifyFieldTool } from './modify-field.tool.js';
|
|
15
|
+
export { deleteFieldTool } from './delete-field.tool.js';
|
|
16
|
+
export { listMetadataObjectsTool } from './list-metadata-objects.tool.js';
|
|
17
|
+
export { describeMetadataObjectTool } from './describe-metadata-object.tool.js';
|
|
18
|
+
|
|
19
|
+
import { createObjectTool } from './create-object.tool.js';
|
|
20
|
+
import { addFieldTool } from './add-field.tool.js';
|
|
21
|
+
import { modifyFieldTool } from './modify-field.tool.js';
|
|
22
|
+
import { deleteFieldTool } from './delete-field.tool.js';
|
|
23
|
+
import { listMetadataObjectsTool } from './list-metadata-objects.tool.js';
|
|
24
|
+
import { describeMetadataObjectTool } from './describe-metadata-object.tool.js';
|
|
25
|
+
|
|
26
|
+
/** All built-in metadata management tool definitions (Tool metadata). */
|
|
27
|
+
export const METADATA_TOOL_DEFINITIONS: Tool[] = [
|
|
28
|
+
createObjectTool,
|
|
29
|
+
addFieldTool,
|
|
30
|
+
modifyFieldTool,
|
|
31
|
+
deleteFieldTool,
|
|
32
|
+
listMetadataObjectsTool,
|
|
33
|
+
describeMetadataObjectTool,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Internal type aliases for metadata payloads (returned as `unknown` from
|
|
38
|
+
// IMetadataService — we cast to these lightweight shapes for field access).
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/** Minimal shape of an object definition as returned by IMetadataService. */
|
|
42
|
+
interface ObjectDef {
|
|
43
|
+
name: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
fields?: Record<string, FieldDef>;
|
|
46
|
+
enable?: Record<string, boolean>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Minimal shape of a field definition inside an object. */
|
|
50
|
+
interface FieldDef {
|
|
51
|
+
name?: string;
|
|
52
|
+
type?: string;
|
|
53
|
+
label?: string;
|
|
54
|
+
required?: boolean;
|
|
55
|
+
reference?: string;
|
|
56
|
+
options?: unknown;
|
|
57
|
+
defaultValue?: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Shared validation helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/** snake_case identifier pattern (e.g. `project_task`, `due_date`). */
|
|
65
|
+
const SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
|
|
66
|
+
|
|
67
|
+
/** Validate that a value matches snake_case. */
|
|
68
|
+
function isSnakeCase(value: string): boolean {
|
|
69
|
+
return SNAKE_CASE_RE.test(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Context — injected once at registration time
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Services required by the metadata management tools.
|
|
78
|
+
*
|
|
79
|
+
* Provided by the kernel at `ai:ready` time and closed over
|
|
80
|
+
* by the handler functions so they stay framework-agnostic.
|
|
81
|
+
*/
|
|
82
|
+
export interface MetadataToolContext {
|
|
83
|
+
/** Metadata service for schema CRUD operations. */
|
|
84
|
+
metadataService: IMetadataService;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Handler Factories
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
function createCreateObjectHandler(ctx: MetadataToolContext): ToolHandler {
|
|
92
|
+
return async (args) => {
|
|
93
|
+
const { name, label, fields, enableFeatures } = args as {
|
|
94
|
+
name: string;
|
|
95
|
+
label: string;
|
|
96
|
+
fields?: Array<{ name: string; label?: string; type: string; required?: boolean }>;
|
|
97
|
+
enableFeatures?: Record<string, boolean>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (!name || !label) {
|
|
101
|
+
return JSON.stringify({ error: 'Both "name" and "label" are required' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate snake_case name
|
|
105
|
+
if (!isSnakeCase(name)) {
|
|
106
|
+
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if the object already exists
|
|
110
|
+
const existing = await ctx.metadataService.getObject(name);
|
|
111
|
+
if (existing) {
|
|
112
|
+
return JSON.stringify({ error: `Object "${name}" already exists` });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build field map from array input with per-field validation
|
|
116
|
+
const fieldMap: Record<string, Record<string, unknown>> = {};
|
|
117
|
+
if (fields && Array.isArray(fields)) {
|
|
118
|
+
const seenNames = new Set<string>();
|
|
119
|
+
for (const f of fields) {
|
|
120
|
+
if (!f.name) {
|
|
121
|
+
return JSON.stringify({ error: 'Each field must have a "name" property' });
|
|
122
|
+
}
|
|
123
|
+
if (!isSnakeCase(f.name)) {
|
|
124
|
+
return JSON.stringify({ error: `Invalid field name "${f.name}". Must be snake_case.` });
|
|
125
|
+
}
|
|
126
|
+
if (seenNames.has(f.name)) {
|
|
127
|
+
return JSON.stringify({ error: `Duplicate field name "${f.name}" in initial fields` });
|
|
128
|
+
}
|
|
129
|
+
seenNames.add(f.name);
|
|
130
|
+
fieldMap[f.name] = {
|
|
131
|
+
type: f.type,
|
|
132
|
+
...(f.label ? { label: f.label } : {}),
|
|
133
|
+
...(f.required !== undefined ? { required: f.required } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const objectDef: Record<string, unknown> = {
|
|
139
|
+
name,
|
|
140
|
+
label,
|
|
141
|
+
...(Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {}),
|
|
142
|
+
...(enableFeatures ? { enable: enableFeatures } : {}),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await ctx.metadataService.register('object', name, objectDef);
|
|
146
|
+
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
name,
|
|
149
|
+
label,
|
|
150
|
+
fieldCount: Object.keys(fieldMap).length,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createAddFieldHandler(ctx: MetadataToolContext): ToolHandler {
|
|
156
|
+
return async (args) => {
|
|
157
|
+
const { objectName, name, label, type, required, defaultValue, options, reference } = args as {
|
|
158
|
+
objectName: string;
|
|
159
|
+
name: string;
|
|
160
|
+
label?: string;
|
|
161
|
+
type: string;
|
|
162
|
+
required?: boolean;
|
|
163
|
+
defaultValue?: unknown;
|
|
164
|
+
options?: Array<{ label: string; value: string }>;
|
|
165
|
+
reference?: string;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!objectName || !name || !type) {
|
|
169
|
+
return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validate snake_case names
|
|
173
|
+
if (!isSnakeCase(objectName)) {
|
|
174
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
175
|
+
}
|
|
176
|
+
if (!isSnakeCase(name)) {
|
|
177
|
+
return JSON.stringify({ error: `Invalid field name "${name}". Must be snake_case.` });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Validate reference as snake_case if provided
|
|
181
|
+
if (reference && !isSnakeCase(reference)) {
|
|
182
|
+
return JSON.stringify({ error: `Invalid reference "${reference}". Must be a snake_case object name.` });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Validate select option values as snake_case if provided
|
|
186
|
+
if (options && Array.isArray(options)) {
|
|
187
|
+
for (const opt of options) {
|
|
188
|
+
if (opt.value && !isSnakeCase(opt.value)) {
|
|
189
|
+
return JSON.stringify({ error: `Invalid option value "${opt.value}". Must be lowercase snake_case.` });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Verify the target object exists
|
|
195
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
196
|
+
if (!objectDef) {
|
|
197
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if field already exists
|
|
201
|
+
const def = objectDef as ObjectDef;
|
|
202
|
+
if (def.fields && def.fields[name]) {
|
|
203
|
+
return JSON.stringify({ error: `Field "${name}" already exists on object "${objectName}"` });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build new field definition
|
|
207
|
+
const fieldDef: Record<string, unknown> = {
|
|
208
|
+
type,
|
|
209
|
+
...(label ? { label } : {}),
|
|
210
|
+
...(required !== undefined ? { required } : {}),
|
|
211
|
+
...(defaultValue !== undefined ? { defaultValue } : {}),
|
|
212
|
+
...(options ? { options } : {}),
|
|
213
|
+
...(reference ? { reference } : {}),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Merge the new field into the existing object definition and re-register
|
|
217
|
+
const updatedFields = { ...(def.fields ?? {}), [name]: fieldDef };
|
|
218
|
+
await ctx.metadataService.register('object', objectName, {
|
|
219
|
+
...def,
|
|
220
|
+
fields: updatedFields,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
objectName,
|
|
225
|
+
fieldName: name,
|
|
226
|
+
fieldType: type,
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createModifyFieldHandler(ctx: MetadataToolContext): ToolHandler {
|
|
232
|
+
return async (args) => {
|
|
233
|
+
const { objectName, fieldName, changes } = args as {
|
|
234
|
+
objectName: string;
|
|
235
|
+
fieldName: string;
|
|
236
|
+
changes: Record<string, unknown>;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (!objectName || !fieldName || !changes) {
|
|
240
|
+
return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Validate snake_case names
|
|
244
|
+
if (!isSnakeCase(objectName)) {
|
|
245
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
246
|
+
}
|
|
247
|
+
if (!isSnakeCase(fieldName)) {
|
|
248
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Verify the target object exists
|
|
252
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
253
|
+
if (!objectDef) {
|
|
254
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const def = objectDef as ObjectDef;
|
|
258
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
259
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Apply changes to the field definition
|
|
263
|
+
const existingField = def.fields[fieldName];
|
|
264
|
+
const updatedField = { ...existingField, ...changes };
|
|
265
|
+
const updatedFields = { ...def.fields, [fieldName]: updatedField };
|
|
266
|
+
|
|
267
|
+
await ctx.metadataService.register('object', objectName, {
|
|
268
|
+
...def,
|
|
269
|
+
fields: updatedFields,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
objectName,
|
|
274
|
+
fieldName,
|
|
275
|
+
updatedProperties: Object.keys(changes),
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function createDeleteFieldHandler(ctx: MetadataToolContext): ToolHandler {
|
|
281
|
+
return async (args) => {
|
|
282
|
+
const { objectName, fieldName } = args as {
|
|
283
|
+
objectName: string;
|
|
284
|
+
fieldName: string;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (!objectName || !fieldName) {
|
|
288
|
+
return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Validate snake_case names
|
|
292
|
+
if (!isSnakeCase(objectName)) {
|
|
293
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
294
|
+
}
|
|
295
|
+
if (!isSnakeCase(fieldName)) {
|
|
296
|
+
return JSON.stringify({ error: `Invalid field name "${fieldName}". Must be snake_case.` });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Verify the target object exists
|
|
300
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
301
|
+
if (!objectDef) {
|
|
302
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const def = objectDef as ObjectDef;
|
|
306
|
+
if (!def.fields || !def.fields[fieldName]) {
|
|
307
|
+
return JSON.stringify({ error: `Field "${fieldName}" not found on object "${objectName}"` });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Remove the field and re-register
|
|
311
|
+
const { [fieldName]: _removed, ...remainingFields } = def.fields;
|
|
312
|
+
await ctx.metadataService.register('object', objectName, {
|
|
313
|
+
...def,
|
|
314
|
+
fields: remainingFields,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return JSON.stringify({
|
|
318
|
+
objectName,
|
|
319
|
+
fieldName,
|
|
320
|
+
success: true,
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function createListObjectsHandler(ctx: MetadataToolContext): ToolHandler {
|
|
326
|
+
return async (args) => {
|
|
327
|
+
const { filter, includeFields } = (args ?? {}) as {
|
|
328
|
+
filter?: string;
|
|
329
|
+
includeFields?: boolean;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const objects = await ctx.metadataService.listObjects();
|
|
333
|
+
let result = (objects as ObjectDef[]).map(o => {
|
|
334
|
+
const base: Record<string, unknown> = {
|
|
335
|
+
name: o.name,
|
|
336
|
+
label: o.label ?? o.name,
|
|
337
|
+
fieldCount: o.fields ? Object.keys(o.fields).length : 0,
|
|
338
|
+
};
|
|
339
|
+
if (includeFields && o.fields) {
|
|
340
|
+
base.fields = Object.entries(o.fields).map(([key, f]) => ({
|
|
341
|
+
name: key,
|
|
342
|
+
type: f.type,
|
|
343
|
+
label: f.label ?? key,
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
return base;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Apply optional name/label substring filter
|
|
350
|
+
if (filter) {
|
|
351
|
+
const lower = filter.toLowerCase();
|
|
352
|
+
result = result.filter(o =>
|
|
353
|
+
(o.name as string).toLowerCase().includes(lower) ||
|
|
354
|
+
(o.label as string).toLowerCase().includes(lower),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return JSON.stringify({
|
|
359
|
+
objects: result,
|
|
360
|
+
totalCount: result.length,
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function createDescribeObjectHandler(ctx: MetadataToolContext): ToolHandler {
|
|
366
|
+
return async (args) => {
|
|
367
|
+
const { objectName } = args as { objectName: string };
|
|
368
|
+
|
|
369
|
+
if (!objectName) {
|
|
370
|
+
return JSON.stringify({ error: '"objectName" is required' });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Validate snake_case name
|
|
374
|
+
if (!isSnakeCase(objectName)) {
|
|
375
|
+
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const objectDef = await ctx.metadataService.getObject(objectName);
|
|
379
|
+
if (!objectDef) {
|
|
380
|
+
return JSON.stringify({ error: `Object "${objectName}" not found` });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const def = objectDef as ObjectDef;
|
|
384
|
+
const fields = def.fields ?? {};
|
|
385
|
+
const fieldSummary = Object.entries(fields).map(([key, f]) => ({
|
|
386
|
+
name: key,
|
|
387
|
+
type: f.type,
|
|
388
|
+
label: f.label ?? key,
|
|
389
|
+
required: f.required ?? false,
|
|
390
|
+
...(f.reference ? { reference: f.reference } : {}),
|
|
391
|
+
...(f.options ? { options: f.options } : {}),
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
return JSON.stringify({
|
|
395
|
+
name: def.name,
|
|
396
|
+
label: def.label ?? def.name,
|
|
397
|
+
fields: fieldSummary,
|
|
398
|
+
enableFeatures: def.enable ?? {},
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Public Registration Helper
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Register all built-in metadata management tools on the given {@link ToolRegistry}.
|
|
409
|
+
*
|
|
410
|
+
* Typically called from the `ai:ready` hook after the metadata service is available.
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```ts
|
|
414
|
+
* ctx.hook('ai:ready', async (aiService) => {
|
|
415
|
+
* const metadataService = ctx.getService<IMetadataService>('metadata');
|
|
416
|
+
* registerMetadataTools(aiService.toolRegistry, { metadataService });
|
|
417
|
+
* });
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
export function registerMetadataTools(
|
|
421
|
+
registry: ToolRegistry,
|
|
422
|
+
context: MetadataToolContext,
|
|
423
|
+
): void {
|
|
424
|
+
registry.register(createObjectTool, createCreateObjectHandler(context));
|
|
425
|
+
registry.register(addFieldTool, createAddFieldHandler(context));
|
|
426
|
+
registry.register(modifyFieldTool, createModifyFieldHandler(context));
|
|
427
|
+
registry.register(deleteFieldTool, createDeleteFieldHandler(context));
|
|
428
|
+
registry.register(listMetadataObjectsTool, createListObjectsHandler(context));
|
|
429
|
+
registry.register(describeMetadataObjectTool, createDescribeObjectHandler(context));
|
|
430
|
+
}
|