174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
import type { CodingPluginToolDefinition } from '../../shared/coding-plugins';
|
|
import type {
|
|
DevicePackageIndexV1,
|
|
DevicePackageToolDetailsV1,
|
|
DevicePackageToolOperation,
|
|
InstallPreviewV1,
|
|
} from '../../shared/device-packages';
|
|
import type { DevicePackageToolName } from '../../shared/device-packages';
|
|
import type { PiProductToolResult } from '../coding-runtime/pi/product-tools';
|
|
import type { DevicePackageManager } from './device-package-manager';
|
|
|
|
export { DEVICE_PACKAGE_TOOL_NAMES } from '../../shared/device-packages';
|
|
|
|
const EMPTY_OBJECT_SCHEMA = Object.freeze({
|
|
type: 'object', additionalProperties: false, properties: {},
|
|
});
|
|
|
|
function tool(
|
|
name: DevicePackageToolName,
|
|
label: string,
|
|
description: string,
|
|
operation: DevicePackageToolOperation,
|
|
mutation: 'read' | 'write' | 'destructive',
|
|
inputSchema: Readonly<Record<string, unknown>>,
|
|
): CodingPluginToolDefinition {
|
|
return Object.freeze({
|
|
name,
|
|
label,
|
|
description,
|
|
capabilityId: 'makelore.device-packages',
|
|
operation,
|
|
roles: ['parent'],
|
|
mutation,
|
|
projectWriteLease: false,
|
|
permissions: ['device-packages'],
|
|
inputSchema,
|
|
});
|
|
}
|
|
|
|
export const DEVICE_PACKAGE_TOOL_DEFINITIONS = Object.freeze([
|
|
tool(
|
|
'local_package_prepare',
|
|
'Prepare local package',
|
|
'Resolve an npm, Git, or absolute local Pi package/Skill and show an exact installation preview. This does not install it.',
|
|
'prepare',
|
|
'read',
|
|
{
|
|
type: 'object', additionalProperties: false, required: ['source'],
|
|
properties: { source: { type: 'string', minLength: 1, maxLength: 2048 } },
|
|
},
|
|
),
|
|
tool(
|
|
'local_package_commit',
|
|
'Install local package',
|
|
'Install a prepared device package only after the user confirms in a later message.',
|
|
'commit',
|
|
'write',
|
|
{
|
|
type: 'object', additionalProperties: false, required: ['planId', 'confirmed'],
|
|
properties: {
|
|
planId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
confirmed: { const: true },
|
|
},
|
|
},
|
|
),
|
|
tool('local_package_list', 'List local packages', 'List packages installed on this device.', 'list', 'read', EMPTY_OBJECT_SCHEMA),
|
|
tool(
|
|
'local_package_set_enabled',
|
|
'Enable or disable local package',
|
|
'Enable or disable an installed device package for future parent workers.',
|
|
'set_enabled',
|
|
'write',
|
|
{
|
|
type: 'object', additionalProperties: false, required: ['packageId', 'enabled'],
|
|
properties: {
|
|
packageId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
enabled: { type: 'boolean' },
|
|
},
|
|
},
|
|
),
|
|
tool(
|
|
'local_package_uninstall',
|
|
'Remove local package',
|
|
'Remove an installed package from this device after explicit confirmation.',
|
|
'uninstall',
|
|
'destructive',
|
|
{
|
|
type: 'object', additionalProperties: false, required: ['packageId', 'confirmed'],
|
|
properties: {
|
|
packageId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
confirmed: { const: true },
|
|
},
|
|
},
|
|
),
|
|
]);
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Local package tool input is invalid');
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function exact(input: Record<string, unknown>, keys: readonly string[]): void {
|
|
const actual = Object.keys(input).sort();
|
|
const expected = [...keys].sort();
|
|
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
throw new Error('Local package tool input has unexpected fields');
|
|
}
|
|
}
|
|
|
|
function boundedString(value: unknown, name: string, maximum: number): string {
|
|
if (typeof value !== 'string' || !value.trim() || value.length > maximum) throw new Error(`${name} is invalid`);
|
|
return value.trim();
|
|
}
|
|
|
|
function result(
|
|
operation: DevicePackageToolOperation,
|
|
value: { preview?: InstallPreviewV1; index?: DevicePackageIndexV1 },
|
|
): PiProductToolResult {
|
|
const details: DevicePackageToolDetailsV1 = {
|
|
schema: 'makelore-device-package.v1', operation, success: true, ...value,
|
|
};
|
|
return { content: [{ type: 'text', text: JSON.stringify(details) }], details };
|
|
}
|
|
|
|
export class DevicePackageTools {
|
|
readonly tools = DEVICE_PACKAGE_TOOL_DEFINITIONS;
|
|
|
|
constructor(private readonly manager: DevicePackageManager) {}
|
|
|
|
async invoke(toolName: string, turnId: string, value: unknown): Promise<PiProductToolResult> {
|
|
const input = record(value);
|
|
switch (toolName) {
|
|
case 'local_package_prepare': {
|
|
exact(input, ['source']);
|
|
return result('prepare', {
|
|
preview: await this.manager.prepare(boundedString(input.source, 'source', 2048), turnId),
|
|
});
|
|
}
|
|
case 'local_package_commit': {
|
|
exact(input, ['planId', 'confirmed']);
|
|
return result('commit', {
|
|
index: await this.manager.commit(
|
|
boundedString(input.planId, 'planId', 128),
|
|
input.confirmed as true,
|
|
turnId,
|
|
),
|
|
});
|
|
}
|
|
case 'local_package_list':
|
|
exact(input, []);
|
|
return result('list', { index: await this.manager.list() });
|
|
case 'local_package_set_enabled': {
|
|
exact(input, ['packageId', 'enabled']);
|
|
if (typeof input.enabled !== 'boolean') throw new Error('enabled is invalid');
|
|
return result('set_enabled', {
|
|
index: await this.manager.setEnabled(
|
|
boundedString(input.packageId, 'packageId', 128),
|
|
input.enabled,
|
|
),
|
|
});
|
|
}
|
|
case 'local_package_uninstall': {
|
|
exact(input, ['packageId', 'confirmed']);
|
|
if (input.confirmed !== true) throw new Error('Literal confirmation is required');
|
|
return result('uninstall', {
|
|
index: await this.manager.uninstall(boundedString(input.packageId, 'packageId', 128)),
|
|
});
|
|
}
|
|
default:
|
|
throw new Error('Local package tool is unavailable');
|
|
}
|
|
}
|
|
}
|