Files
makelore/electron/coding-packages/device-package-tools.ts

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');
}
}
}