@agentic-kit/pi 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +126 -0
- package/app-workspace.d.ts +16 -0
- package/app-workspace.js +145 -0
- package/confirm-gate.d.ts +13 -0
- package/confirm-gate.js +51 -0
- package/context.d.ts +39 -0
- package/context.js +251 -0
- package/db-probe.d.ts +29 -0
- package/db-probe.js +50 -0
- package/esm/app-workspace.d.ts +16 -0
- package/esm/app-workspace.js +134 -0
- package/esm/confirm-gate.d.ts +13 -0
- package/esm/confirm-gate.js +48 -0
- package/esm/context.d.ts +39 -0
- package/esm/context.js +242 -0
- package/esm/db-probe.d.ts +29 -0
- package/esm/db-probe.js +47 -0
- package/esm/host.d.ts +55 -0
- package/esm/host.js +20 -0
- package/esm/index.d.ts +11 -0
- package/esm/index.js +48 -0
- package/esm/policy/add-policies-to-table.d.ts +26 -0
- package/esm/policy/add-policies-to-table.js +66 -0
- package/esm/policy/provision-helpers.d.ts +38 -0
- package/esm/policy/provision-helpers.js +50 -0
- package/esm/provision-database/create-database-provision.d.ts +11 -0
- package/esm/provision-database/create-database-provision.js +48 -0
- package/esm/provision-database/credential.d.ts +14 -0
- package/esm/provision-database/credential.js +19 -0
- package/esm/provision-database/env-file.d.ts +37 -0
- package/esm/provision-database/env-file.js +85 -0
- package/esm/provision-database/modules.d.ts +2 -0
- package/esm/provision-database/modules.js +40 -0
- package/esm/provision-database/pg-fixups.d.ts +16 -0
- package/esm/provision-database/pg-fixups.js +132 -0
- package/esm/records/meta.d.ts +41 -0
- package/esm/records/meta.js +170 -0
- package/esm/run-codegen/barrels.d.ts +3 -0
- package/esm/run-codegen/barrels.js +48 -0
- package/esm/run-codegen/endpoints.d.ts +8 -0
- package/esm/run-codegen/endpoints.js +46 -0
- package/esm/schema-resolve.d.ts +23 -0
- package/esm/schema-resolve.js +69 -0
- package/esm/tool-schema.d.ts +3 -0
- package/esm/tool-schema.js +10 -0
- package/esm/tools/add-policies.d.ts +8 -0
- package/esm/tools/add-policies.js +96 -0
- package/esm/tools/add-records.d.ts +14 -0
- package/esm/tools/add-records.js +124 -0
- package/esm/tools/add-relation-schema.d.ts +30 -0
- package/esm/tools/add-relation-schema.js +58 -0
- package/esm/tools/add-relation.d.ts +10 -0
- package/esm/tools/add-relation.js +91 -0
- package/esm/tools/describe-schema.d.ts +18 -0
- package/esm/tools/describe-schema.js +132 -0
- package/esm/tools/mutations.d.ts +14 -0
- package/esm/tools/mutations.js +235 -0
- package/esm/tools/provision-blueprint.d.ts +22 -0
- package/esm/tools/provision-blueprint.js +111 -0
- package/esm/tools/provision-database.d.ts +14 -0
- package/esm/tools/provision-database.js +251 -0
- package/esm/tools/run-codegen.d.ts +11 -0
- package/esm/tools/run-codegen.js +139 -0
- package/esm/tools/templates.d.ts +49 -0
- package/esm/tools/templates.js +337 -0
- package/host.d.ts +55 -0
- package/host.js +25 -0
- package/index.d.ts +11 -0
- package/index.js +60 -0
- package/package.json +51 -0
- package/policy/add-policies-to-table.d.ts +26 -0
- package/policy/add-policies-to-table.js +71 -0
- package/policy/provision-helpers.d.ts +38 -0
- package/policy/provision-helpers.js +58 -0
- package/provision-database/create-database-provision.d.ts +11 -0
- package/provision-database/create-database-provision.js +51 -0
- package/provision-database/credential.d.ts +14 -0
- package/provision-database/credential.js +22 -0
- package/provision-database/env-file.d.ts +37 -0
- package/provision-database/env-file.js +91 -0
- package/provision-database/modules.d.ts +2 -0
- package/provision-database/modules.js +43 -0
- package/provision-database/pg-fixups.d.ts +16 -0
- package/provision-database/pg-fixups.js +170 -0
- package/records/meta.d.ts +41 -0
- package/records/meta.js +175 -0
- package/run-codegen/barrels.d.ts +3 -0
- package/run-codegen/barrels.js +54 -0
- package/run-codegen/endpoints.d.ts +8 -0
- package/run-codegen/endpoints.js +51 -0
- package/schema-resolve.d.ts +23 -0
- package/schema-resolve.js +75 -0
- package/tool-schema.d.ts +3 -0
- package/tool-schema.js +13 -0
- package/tools/add-policies.d.ts +8 -0
- package/tools/add-policies.js +99 -0
- package/tools/add-records.d.ts +14 -0
- package/tools/add-records.js +127 -0
- package/tools/add-relation-schema.d.ts +30 -0
- package/tools/add-relation-schema.js +62 -0
- package/tools/add-relation.d.ts +10 -0
- package/tools/add-relation.js +94 -0
- package/tools/describe-schema.d.ts +18 -0
- package/tools/describe-schema.js +135 -0
- package/tools/mutations.d.ts +14 -0
- package/tools/mutations.js +238 -0
- package/tools/provision-blueprint.d.ts +22 -0
- package/tools/provision-blueprint.js +114 -0
- package/tools/provision-database.d.ts +14 -0
- package/tools/provision-database.js +257 -0
- package/tools/run-codegen.d.ts +11 -0
- package/tools/run-codegen.js +143 -0
- package/tools/templates.d.ts +49 -0
- package/tools/templates.js +341 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { fieldTypeToTypeName, isInternalPolicy } from '@agentic-kit/harness';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { resolveProjectContext } from '../context';
|
|
4
|
+
import { toolSchema } from '../tool-schema';
|
|
5
|
+
const ParamZod = z.object({});
|
|
6
|
+
const ParamSchema = toolSchema(ParamZod);
|
|
7
|
+
function toDefaultValueString(value) {
|
|
8
|
+
if (value == null)
|
|
9
|
+
return null;
|
|
10
|
+
if (typeof value === 'string')
|
|
11
|
+
return value;
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
function formatText(details) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
if (details.tables.length === 0) {
|
|
17
|
+
lines.push('No application tables yet.');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
lines.push(`Tables (${details.tables.length}):`);
|
|
21
|
+
for (const table of details.tables) {
|
|
22
|
+
lines.push(` ${table.name}`);
|
|
23
|
+
for (const field of table.fields) {
|
|
24
|
+
const req = field.isRequired ? ' required' : '';
|
|
25
|
+
const def = field.defaultValue != null ? ` default=${field.defaultValue}` : '';
|
|
26
|
+
lines.push(` - ${field.name}: ${field.type}${req}${def}`);
|
|
27
|
+
}
|
|
28
|
+
if (table.policies.length > 0) {
|
|
29
|
+
lines.push(` policies: ${table.policies.join(', ')}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
export const describeSchemaTool = {
|
|
36
|
+
name: 'describe_schema',
|
|
37
|
+
label: 'Describe schema',
|
|
38
|
+
description: 'Inspect the current Constructive database schema for this project: application tables with their fields (name, type, required, default). Read-only.',
|
|
39
|
+
promptSnippet: 'describe_schema: list the project database tables and their fields. Read-only — call before modifying schema.',
|
|
40
|
+
parameters: ParamSchema,
|
|
41
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
42
|
+
const resolved = await resolveProjectContext(ctx.cwd);
|
|
43
|
+
if (!resolved.context) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: 'text', text: resolved.reason }],
|
|
46
|
+
details: { tables: [] },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const { api, databaseId } = resolved.context;
|
|
50
|
+
const tablesResult = await api.table
|
|
51
|
+
.findMany({
|
|
52
|
+
select: { id: true, name: true, category: true },
|
|
53
|
+
where: {
|
|
54
|
+
databaseId: { equalTo: databaseId },
|
|
55
|
+
or: [{ category: { equalTo: 'APP' } }, { name: { equalTo: 'users' } }],
|
|
56
|
+
},
|
|
57
|
+
orderBy: ['NAME_ASC'],
|
|
58
|
+
})
|
|
59
|
+
.execute();
|
|
60
|
+
if (!tablesResult.ok) {
|
|
61
|
+
const message = tablesResult.errors.map((e) => e.message).join('; ');
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: 'text', text: `Failed to read schema: ${message}` }],
|
|
64
|
+
details: { tables: [] },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const tableNodes = tablesResult.data.tables?.nodes ?? [];
|
|
68
|
+
const tableIds = tableNodes.map((t) => t.id);
|
|
69
|
+
const fieldsByTable = new Map();
|
|
70
|
+
const policiesByTable = new Map();
|
|
71
|
+
if (tableIds.length > 0) {
|
|
72
|
+
const [fieldsResult, policiesResult] = await Promise.all([
|
|
73
|
+
api.field
|
|
74
|
+
.findMany({
|
|
75
|
+
select: {
|
|
76
|
+
name: true,
|
|
77
|
+
type: true,
|
|
78
|
+
isRequired: true,
|
|
79
|
+
defaultValue: true,
|
|
80
|
+
tableId: true,
|
|
81
|
+
},
|
|
82
|
+
where: { tableId: { in: tableIds } },
|
|
83
|
+
})
|
|
84
|
+
.execute(),
|
|
85
|
+
api.policy
|
|
86
|
+
.findMany({
|
|
87
|
+
select: { tableId: true, policyType: true },
|
|
88
|
+
where: { tableId: { in: tableIds } },
|
|
89
|
+
})
|
|
90
|
+
.execute(),
|
|
91
|
+
]);
|
|
92
|
+
if (!fieldsResult.ok) {
|
|
93
|
+
const message = fieldsResult.errors.map((e) => e.message).join('; ');
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: 'text', text: `Failed to read fields: ${message}` }],
|
|
96
|
+
details: { tables: [] },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
for (const field of fieldsResult.data.fields?.nodes ?? []) {
|
|
100
|
+
const list = fieldsByTable.get(field.tableId) ?? [];
|
|
101
|
+
list.push({
|
|
102
|
+
name: field.name,
|
|
103
|
+
type: fieldTypeToTypeName(field.type),
|
|
104
|
+
isRequired: Boolean(field.isRequired),
|
|
105
|
+
defaultValue: toDefaultValueString(field.defaultValue),
|
|
106
|
+
});
|
|
107
|
+
fieldsByTable.set(field.tableId, list);
|
|
108
|
+
}
|
|
109
|
+
// One policy spans CRUD as several rows; collapse to distinct types per table.
|
|
110
|
+
if (policiesResult.ok) {
|
|
111
|
+
for (const policy of policiesResult.data.policies?.nodes ?? []) {
|
|
112
|
+
if (isInternalPolicy(policy.policyType))
|
|
113
|
+
continue;
|
|
114
|
+
const list = policiesByTable.get(policy.tableId) ?? [];
|
|
115
|
+
if (!list.includes(policy.policyType))
|
|
116
|
+
list.push(policy.policyType);
|
|
117
|
+
policiesByTable.set(policy.tableId, list);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const tables = tableNodes.map((table) => ({
|
|
122
|
+
name: table.name,
|
|
123
|
+
fields: fieldsByTable.get(table.id) ?? [],
|
|
124
|
+
policies: policiesByTable.get(table.id) ?? [],
|
|
125
|
+
}));
|
|
126
|
+
const details = { tables };
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: 'text', text: formatText(details) }],
|
|
129
|
+
details,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
export type MutationDetails = {
|
|
3
|
+
success: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
declare const DeleteTableSchema: import("typebox/type").TSchema;
|
|
7
|
+
export declare const deleteTableTool: ToolDefinition<typeof DeleteTableSchema, MutationDetails>;
|
|
8
|
+
declare const CreateFieldSchema: import("typebox/type").TSchema;
|
|
9
|
+
export declare const createFieldTool: ToolDefinition<typeof CreateFieldSchema, MutationDetails>;
|
|
10
|
+
declare const UpdateFieldSchema: import("typebox/type").TSchema;
|
|
11
|
+
export declare const updateFieldTool: ToolDefinition<typeof UpdateFieldSchema, MutationDetails>;
|
|
12
|
+
declare const DeleteFieldSchema: import("typebox/type").TSchema;
|
|
13
|
+
export declare const deleteFieldTool: ToolDefinition<typeof DeleteFieldSchema, MutationDetails>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { toFieldDefault, toFieldType } from '@agentic-kit/harness';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { resolveProjectContext } from '../context';
|
|
4
|
+
import { findUniqueConstraintId, resolveField, resolveSchema, resolveTable } from '../schema-resolve';
|
|
5
|
+
import { toolSchema } from '../tool-schema';
|
|
6
|
+
function ok(message) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: 'text', text: message }],
|
|
9
|
+
details: { success: true, message },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function fail(message) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: 'text', text: message }],
|
|
15
|
+
details: { success: false, message },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// ── delete_table ──────────────────────────────────────────────────────────
|
|
19
|
+
const DeleteTableZod = z.object({
|
|
20
|
+
table_name: z.string().describe('Name of the table to delete (snake_case)'),
|
|
21
|
+
});
|
|
22
|
+
const DeleteTableSchema = toolSchema(DeleteTableZod);
|
|
23
|
+
export const deleteTableTool = {
|
|
24
|
+
name: 'delete_table',
|
|
25
|
+
label: 'Delete table',
|
|
26
|
+
description: 'Permanently delete a table and its data from the project database.',
|
|
27
|
+
promptSnippet: 'delete_table: drop an existing table by name. Destructive — gated.',
|
|
28
|
+
parameters: DeleteTableSchema,
|
|
29
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
30
|
+
const resolved = await resolveProjectContext(ctx.cwd);
|
|
31
|
+
if (!resolved.context)
|
|
32
|
+
return fail(resolved.reason);
|
|
33
|
+
try {
|
|
34
|
+
const schema = await resolveSchema(resolved.context);
|
|
35
|
+
const table = resolveTable(schema, params.table_name);
|
|
36
|
+
await resolved.context.api.table
|
|
37
|
+
.delete({ where: { id: table.id }, select: { id: true } })
|
|
38
|
+
.unwrap();
|
|
39
|
+
return ok(`Deleted table ${params.table_name}`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return fail(err instanceof Error ? err.message : 'Failed to delete table');
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
// ── create_field ──────────────────────────────────────────────────────────
|
|
47
|
+
const CreateFieldZod = z.object({
|
|
48
|
+
table_name: z.string().describe('Name of the table to add the field to'),
|
|
49
|
+
field_name: z.string().describe('Field name in snake_case'),
|
|
50
|
+
type: z
|
|
51
|
+
.string()
|
|
52
|
+
.describe('PostgreSQL type: text, integer, smallint, numeric, boolean, date, timestamp, timestamptz, time, json, jsonb, inet, email, url, image, upload'),
|
|
53
|
+
is_required: z.boolean().describe('Whether the field is NOT NULL. Defaults to false.').optional(),
|
|
54
|
+
default_value: z
|
|
55
|
+
.union([z.string(), z.null()])
|
|
56
|
+
.describe('SQL default expression, e.g. "now()", "\'draft\'"')
|
|
57
|
+
.optional(),
|
|
58
|
+
is_unique: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.describe('Whether the field has a unique constraint. Defaults to false.')
|
|
61
|
+
.optional(),
|
|
62
|
+
});
|
|
63
|
+
const CreateFieldSchema = toolSchema(CreateFieldZod);
|
|
64
|
+
export const createFieldTool = {
|
|
65
|
+
name: 'create_field',
|
|
66
|
+
label: 'Create field',
|
|
67
|
+
description: 'Add a new field (column) to an existing table in the project database.',
|
|
68
|
+
promptSnippet: 'create_field: add a column to an existing table. Gated.',
|
|
69
|
+
parameters: CreateFieldSchema,
|
|
70
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
71
|
+
const resolved = await resolveProjectContext(ctx.cwd);
|
|
72
|
+
if (!resolved.context)
|
|
73
|
+
return fail(resolved.reason);
|
|
74
|
+
const { api, databaseId } = resolved.context;
|
|
75
|
+
try {
|
|
76
|
+
const schema = await resolveSchema(resolved.context);
|
|
77
|
+
const table = resolveTable(schema, params.table_name);
|
|
78
|
+
const result = await api.field
|
|
79
|
+
.create({
|
|
80
|
+
data: {
|
|
81
|
+
name: params.field_name,
|
|
82
|
+
type: toFieldType(params.type),
|
|
83
|
+
tableId: table.id,
|
|
84
|
+
databaseId,
|
|
85
|
+
isRequired: params.is_required ?? false,
|
|
86
|
+
defaultValue: toFieldDefault(params.default_value),
|
|
87
|
+
},
|
|
88
|
+
select: { id: true, name: true, type: true },
|
|
89
|
+
})
|
|
90
|
+
.unwrap();
|
|
91
|
+
const createdId = result.createField?.field?.id;
|
|
92
|
+
if (params.is_unique && createdId) {
|
|
93
|
+
try {
|
|
94
|
+
await api.uniqueConstraint
|
|
95
|
+
.create({
|
|
96
|
+
data: {
|
|
97
|
+
tableId: table.id,
|
|
98
|
+
databaseId,
|
|
99
|
+
fieldIds: [createdId],
|
|
100
|
+
name: `${table.name}_${params.field_name}_key`,
|
|
101
|
+
type: 'u',
|
|
102
|
+
},
|
|
103
|
+
select: { id: true },
|
|
104
|
+
})
|
|
105
|
+
.unwrap();
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn('Failed to create unique constraint:', err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return ok(`Added field ${params.table_name}.${params.field_name}`);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return fail(err instanceof Error ? err.message : 'Failed to create field');
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
// ── update_field ──────────────────────────────────────────────────────────
|
|
119
|
+
const UpdateFieldZod = z.object({
|
|
120
|
+
table_name: z.string().describe('Name of the table containing the field'),
|
|
121
|
+
field_name: z.string().describe('Current field name'),
|
|
122
|
+
new_name: z.string().describe('New field name in snake_case (if renaming)').optional(),
|
|
123
|
+
new_type: z.string().describe('New PostgreSQL type (if changing type)').optional(),
|
|
124
|
+
is_required: z.boolean().describe('Set NOT NULL constraint').optional(),
|
|
125
|
+
default_value: z
|
|
126
|
+
.union([z.string(), z.null()])
|
|
127
|
+
.describe('New SQL default expression, or null to remove')
|
|
128
|
+
.optional(),
|
|
129
|
+
is_unique: z.boolean().describe('Set or remove unique constraint').optional(),
|
|
130
|
+
});
|
|
131
|
+
const UpdateFieldSchema = toolSchema(UpdateFieldZod);
|
|
132
|
+
export const updateFieldTool = {
|
|
133
|
+
name: 'update_field',
|
|
134
|
+
label: 'Update field',
|
|
135
|
+
description: 'Modify an existing field: rename, change type, toggle required/unique, or set default.',
|
|
136
|
+
promptSnippet: 'update_field: rename/retype/toggle an existing column. Gated.',
|
|
137
|
+
parameters: UpdateFieldSchema,
|
|
138
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
139
|
+
const resolved = await resolveProjectContext(ctx.cwd);
|
|
140
|
+
if (!resolved.context)
|
|
141
|
+
return fail(resolved.reason);
|
|
142
|
+
const { api, databaseId } = resolved.context;
|
|
143
|
+
try {
|
|
144
|
+
const schema = await resolveSchema(resolved.context);
|
|
145
|
+
const table = resolveTable(schema, params.table_name);
|
|
146
|
+
const field = resolveField(table, params.field_name);
|
|
147
|
+
const patch = {};
|
|
148
|
+
if (params.new_name)
|
|
149
|
+
patch.name = params.new_name;
|
|
150
|
+
if (params.new_type)
|
|
151
|
+
patch.type = toFieldType(params.new_type);
|
|
152
|
+
if (params.is_required !== undefined)
|
|
153
|
+
patch.isRequired = params.is_required;
|
|
154
|
+
if (params.default_value !== undefined) {
|
|
155
|
+
patch.defaultValue =
|
|
156
|
+
params.default_value === null ? null : (toFieldDefault(params.default_value) ?? null);
|
|
157
|
+
}
|
|
158
|
+
if (Object.keys(patch).length > 0) {
|
|
159
|
+
await api.field
|
|
160
|
+
.update({
|
|
161
|
+
where: { id: field.id },
|
|
162
|
+
data: patch,
|
|
163
|
+
select: { id: true, name: true, type: true },
|
|
164
|
+
})
|
|
165
|
+
.unwrap();
|
|
166
|
+
}
|
|
167
|
+
if (params.is_unique !== undefined) {
|
|
168
|
+
const existingUniqueId = findUniqueConstraintId(table, field.id);
|
|
169
|
+
if (params.is_unique && !existingUniqueId) {
|
|
170
|
+
try {
|
|
171
|
+
await api.uniqueConstraint
|
|
172
|
+
.create({
|
|
173
|
+
data: {
|
|
174
|
+
tableId: table.id,
|
|
175
|
+
databaseId,
|
|
176
|
+
fieldIds: [field.id],
|
|
177
|
+
name: `${table.name}_${params.new_name ?? field.name}_key`,
|
|
178
|
+
type: 'u',
|
|
179
|
+
},
|
|
180
|
+
select: { id: true },
|
|
181
|
+
})
|
|
182
|
+
.unwrap();
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
console.warn('Failed to create unique constraint:', err);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (!params.is_unique && existingUniqueId) {
|
|
189
|
+
try {
|
|
190
|
+
await api.uniqueConstraint
|
|
191
|
+
.delete({ where: { id: existingUniqueId }, select: { id: true } })
|
|
192
|
+
.unwrap();
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.warn('Failed to delete unique constraint:', err);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return ok(`Updated field ${params.table_name}.${params.field_name}`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
return fail(err instanceof Error ? err.message : 'Failed to update field');
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
// ── delete_field ──────────────────────────────────────────────────────────
|
|
207
|
+
const DeleteFieldZod = z.object({
|
|
208
|
+
table_name: z.string().describe('Name of the table containing the field'),
|
|
209
|
+
field_name: z.string().describe('Name of the field to delete'),
|
|
210
|
+
});
|
|
211
|
+
const DeleteFieldSchema = toolSchema(DeleteFieldZod);
|
|
212
|
+
export const deleteFieldTool = {
|
|
213
|
+
name: 'delete_field',
|
|
214
|
+
label: 'Delete field',
|
|
215
|
+
description: 'Permanently delete a field (column) from an existing table.',
|
|
216
|
+
promptSnippet: 'delete_field: drop a column from a table by name. Destructive — gated.',
|
|
217
|
+
parameters: DeleteFieldSchema,
|
|
218
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
219
|
+
const resolved = await resolveProjectContext(ctx.cwd);
|
|
220
|
+
if (!resolved.context)
|
|
221
|
+
return fail(resolved.reason);
|
|
222
|
+
try {
|
|
223
|
+
const schema = await resolveSchema(resolved.context);
|
|
224
|
+
const table = resolveTable(schema, params.table_name);
|
|
225
|
+
const field = resolveField(table, params.field_name);
|
|
226
|
+
await resolved.context.api.field
|
|
227
|
+
.delete({ where: { id: field.id }, select: { id: true } })
|
|
228
|
+
.unwrap();
|
|
229
|
+
return ok(`Deleted field ${params.table_name}.${params.field_name}`);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
return fail(err instanceof Error ? err.message : 'Failed to delete field');
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
declare const BlueprintSchema: import("typebox/type").TSchema;
|
|
3
|
+
type ProvisionedField = {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
isRequired: boolean;
|
|
7
|
+
defaultValue: string | null;
|
|
8
|
+
};
|
|
9
|
+
type ProvisionedTable = {
|
|
10
|
+
name: string;
|
|
11
|
+
fields: ProvisionedField[];
|
|
12
|
+
policies: string[];
|
|
13
|
+
relationCount: number;
|
|
14
|
+
};
|
|
15
|
+
export type ProvisionBlueprintDetails = {
|
|
16
|
+
created: number;
|
|
17
|
+
total: number;
|
|
18
|
+
tables: ProvisionedTable[];
|
|
19
|
+
error: string | null;
|
|
20
|
+
};
|
|
21
|
+
export declare const provisionBlueprintTool: ToolDefinition<typeof BlueprintSchema, ProvisionBlueprintDetails>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { BlueprintZod, expandBlueprintDefaults, filterInternalPolicies, } from '@agentic-kit/harness';
|
|
2
|
+
import { resolveProjectContext } from '../context';
|
|
3
|
+
import { toolSchema } from '../tool-schema';
|
|
4
|
+
const BlueprintSchema = toolSchema(BlueprintZod);
|
|
5
|
+
function formatText(name, details) {
|
|
6
|
+
if (details.error)
|
|
7
|
+
return `Failed to provision "${name}": ${details.error}`;
|
|
8
|
+
const lines = [`Created ${details.created} table${details.created === 1 ? '' : 's'} from "${name}":`];
|
|
9
|
+
for (const table of details.tables) {
|
|
10
|
+
const parts = [`${table.fields.length} field${table.fields.length === 1 ? '' : 's'}`];
|
|
11
|
+
if (table.relationCount > 0) {
|
|
12
|
+
parts.push(`${table.relationCount} relation${table.relationCount === 1 ? '' : 's'}`);
|
|
13
|
+
}
|
|
14
|
+
if (table.policies.length > 0) {
|
|
15
|
+
parts.push(`policies: ${table.policies.map((policy) => policy.replace(/^Authz/, '')).join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
lines.push(` - ${table.name} (${parts.join(', ')})`);
|
|
18
|
+
}
|
|
19
|
+
return lines.join('\n');
|
|
20
|
+
}
|
|
21
|
+
export const provisionBlueprintTool = {
|
|
22
|
+
name: 'provision_blueprint',
|
|
23
|
+
label: 'Provision blueprint',
|
|
24
|
+
description: 'Create one or more related tables (with fields, RLS policies, and relations) in the project database from a blueprint definition. Always put related tables in a SINGLE call so relations are created together.',
|
|
25
|
+
promptSnippet: 'provision_blueprint: create new tables from scratch in a single call (fields + policies + relations). Call describe_schema first.',
|
|
26
|
+
parameters: BlueprintSchema,
|
|
27
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
28
|
+
const empty = { created: 0, total: 0, tables: [], error: null };
|
|
29
|
+
const resolved = await resolveProjectContext(ctx.cwd);
|
|
30
|
+
if (!resolved.context) {
|
|
31
|
+
return { content: [{ type: 'text', text: resolved.reason }], details: empty };
|
|
32
|
+
}
|
|
33
|
+
const { modules, databaseId, schemaId, ownerId } = resolved.context;
|
|
34
|
+
if (!ownerId) {
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: 'Not authenticated (missing OWNER_ID in .env).' }],
|
|
37
|
+
details: empty,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const definition = expandBlueprintDefaults(params.definition);
|
|
41
|
+
const uniqueName = `${params.name}_${Date.now()}`;
|
|
42
|
+
try {
|
|
43
|
+
const createResult = await modules.blueprint
|
|
44
|
+
.create({
|
|
45
|
+
data: {
|
|
46
|
+
ownerId,
|
|
47
|
+
databaseId,
|
|
48
|
+
name: uniqueName,
|
|
49
|
+
displayName: params.name,
|
|
50
|
+
description: params.description,
|
|
51
|
+
definition,
|
|
52
|
+
},
|
|
53
|
+
select: { id: true, name: true },
|
|
54
|
+
})
|
|
55
|
+
.unwrap();
|
|
56
|
+
const blueprintId = createResult.createBlueprint?.blueprint?.id;
|
|
57
|
+
if (!blueprintId)
|
|
58
|
+
throw new Error('Failed to create blueprint');
|
|
59
|
+
const constructResult = await modules.mutation
|
|
60
|
+
.constructBlueprint({ input: { blueprintId, schemaId } }, { select: { result: true } })
|
|
61
|
+
.unwrap();
|
|
62
|
+
const result = constructResult.constructBlueprint?.result;
|
|
63
|
+
if (!result) {
|
|
64
|
+
const constructions = await modules.blueprintConstruction
|
|
65
|
+
.findMany({
|
|
66
|
+
where: { blueprintId: { equalTo: blueprintId } },
|
|
67
|
+
orderBy: ['ID_DESC'],
|
|
68
|
+
first: 1,
|
|
69
|
+
select: { status: true, errorDetails: true },
|
|
70
|
+
})
|
|
71
|
+
.unwrap();
|
|
72
|
+
const error = constructions?.blueprintConstructions?.nodes?.[0]?.errorDetails ?? 'Unknown error';
|
|
73
|
+
const details = {
|
|
74
|
+
created: 0,
|
|
75
|
+
total: params.definition.tables.length,
|
|
76
|
+
tables: [],
|
|
77
|
+
error: typeof error === 'string' ? error : JSON.stringify(error),
|
|
78
|
+
};
|
|
79
|
+
return { content: [{ type: 'text', text: formatText(params.name, details) }], details };
|
|
80
|
+
}
|
|
81
|
+
const relations = params.definition.relations ?? [];
|
|
82
|
+
const details = {
|
|
83
|
+
created: params.definition.tables.length,
|
|
84
|
+
total: params.definition.tables.length,
|
|
85
|
+
tables: params.definition.tables.map((t) => ({
|
|
86
|
+
name: t.table_name,
|
|
87
|
+
fields: (t.fields ?? []).map((f) => ({
|
|
88
|
+
name: f.name,
|
|
89
|
+
type: f.type,
|
|
90
|
+
isRequired: f.is_required ?? false,
|
|
91
|
+
defaultValue: f.default ?? null,
|
|
92
|
+
})),
|
|
93
|
+
policies: filterInternalPolicies(t.policies.map((p) => p.$type)),
|
|
94
|
+
relationCount: relations.filter((r) => r.source_table === t.table_name).length,
|
|
95
|
+
})),
|
|
96
|
+
error: null,
|
|
97
|
+
};
|
|
98
|
+
return { content: [{ type: 'text', text: formatText(params.name, details) }], details };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : 'Failed to provision blueprint';
|
|
102
|
+
const details = {
|
|
103
|
+
created: 0,
|
|
104
|
+
total: params.definition.tables.length,
|
|
105
|
+
tables: [],
|
|
106
|
+
error: message,
|
|
107
|
+
};
|
|
108
|
+
return { content: [{ type: 'text', text: formatText(params.name, details) }], details };
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
declare const ProvisionDatabaseSchema: import("typebox/type").TSchema;
|
|
3
|
+
export type ProvisionDatabaseDetails = {
|
|
4
|
+
success: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
databaseId?: string;
|
|
7
|
+
databaseName?: string;
|
|
8
|
+
ownerId?: string;
|
|
9
|
+
fixupNote?: string;
|
|
10
|
+
/** True when an existing live binding was kept — nothing changed. */
|
|
11
|
+
skipped?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare const provisionDatabaseTool: ToolDefinition<typeof ProvisionDatabaseSchema, ProvisionDatabaseDetails>;
|
|
14
|
+
export {};
|