import { generateId } from './_helpers'; import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context'; import type { ManifestComponentDeletion, ManifestScope } from './manifest-builder'; import { requireStringComponentField } from './_existing-component-fields'; export type CustomSkillStatus = 'active' | 'inactive'; export type CustomSkillSourceType = 'user' | 'company'; /** * Initialization properties for {@link CustomSkill}. */ export interface CustomSkillProps { /** * Source-side identifier for this Custom Skill. * * For a new skill, omit this field and the SDK generates a temporary handle * that Composer maps to the skill's generated database UUID during deploy. * * For an existing skill, use the `source_skill_id` loaded from component * search/extraction. * * Optional. Auto-generated when omitted. @default generateId('skill') */ sourceSkillId?: string; /** * Display name shown in Rippling AI. * * Required. */ name: string; /** * Description used to explain when the skill applies. * * Required. Use `null` to clear this field when updating an existing skill. */ description: string | null; /** * Markdown instruction content for the skill. * * Required. */ content: string; /** * Optional folder path for organizing skills. Use `null` to clear this field * when updating an existing skill. */ path?: string | null; /** * Whether the skill is active or inactive. * * Required. */ status: CustomSkillStatus; /** * Skill visibility. `user` creates a personal skill; `company` creates a * company skill. * * Optional create-time visibility. Do not set when updating an existing * skill; use `CustomSkill.loadFromExisting(...)` so the extracted value can * round-trip unchanged. */ sourceType?: CustomSkillSourceType; } /** * Defines a Rippling AI Custom Skill and registers it with the manifest. */ export class CustomSkill { static readonly componentType = 'CUSTOM_SKILL' as const; static toDeletionIdentifier(sourceSkillId: string): ManifestComponentDeletion { return { type: CustomSkill.componentType, source_skill_id: sourceSkillId, }; } private readonly _sourceSkillId: string; private _name: string; private _description: string | null | undefined; private _content: string; private _path: string | null | undefined; private _status: CustomSkillStatus; private _sourceType: CustomSkillSourceType | undefined; /** * @param scope - The manifest to register this skill with. * @param props - Initialization properties. */ constructor(scope: ManifestScope, props: CustomSkillProps) { if (props.sourceSkillId != null && props.sourceType != null) { throw new Error( 'CustomSkill sourceType is create-time only. Omit sourceType when constructing an existing skill; ' + 'use CustomSkill.loadFromExisting(...) to update extracted skills.', ); } this._sourceSkillId = props.sourceSkillId ?? generateId('skill'); this._name = props.name; this._description = props.description; this._content = props.content; this._path = props.path; this._status = props.status; this._sourceType = props.sourceType; scope._register(this); } /** * Loads this skill from the active existing-manifest JSON context. */ static loadFromExisting(sourceSkillId: string, options: ExistingManifestContextOptions = {}): CustomSkill { return getExistingManifestContext(options).loadCustomSkill(sourceSkillId); } /** @internal Hydrates a custom skill from existing manifest wire JSON. */ static _fromExistingComponent(scope: ManifestScope, component: Record): CustomSkill { const props: CustomSkillProps = { sourceSkillId: requireStringComponentField(component, 'source_skill_id', CustomSkill.componentType), name: requireStringComponentField(component, 'name', CustomSkill.componentType), description: component['description'] == null ? null : requireStringComponentField(component, 'description', CustomSkill.componentType), content: requireStringComponentField(component, 'content', CustomSkill.componentType), status: requireStringComponentField( component, 'status', CustomSkill.componentType, ) as CustomSkillStatus, }; if (typeof component['path'] === 'string') props.path = component['path']; if (typeof component['source_type'] === 'string') { props.sourceType = component['source_type'] as CustomSkillSourceType; } const { sourceType, ...loadProps } = props; const skill = new CustomSkill(scope, loadProps); skill._sourceType = sourceType; return skill; } /** * Returns this skill's source-side manifest identifier. New skills use a * generated temporary handle; extracted skills use their database UUID. */ getSourceSkillId(): string { return this._sourceSkillId; } /** * Replaces the display name for this skill. */ setName(name: string): this { this._name = name; return this; } /** * Replaces the description for this skill. Pass `null` to clear it. */ setDescription(description: string | null): this { this._description = description; return this; } /** * Replaces the plaintext markdown instruction content for this skill. */ setContent(content: string): this { this._content = content; return this; } /** * Replaces the optional folder path for this skill. Pass `null` to clear it, * or `undefined` to omit it from the serialized manifest. */ setPath(path: string | null | undefined): this { this._path = path; return this; } /** * Replaces whether this skill is active or inactive. */ setStatus(status: CustomSkillStatus): this { this._status = status; return this; } /** * Serializes this skill to the wire format consumed by the manifest install endpoint. */ toDict(): Record { const component: Record = { type: CustomSkill.componentType, source_skill_id: this._sourceSkillId, name: this._name, content: this._content, status: this._status, }; if (this._description !== undefined) component['description'] = this._description; if (this._path !== undefined) component['path'] = this._path; if (this._sourceType != null) component['source_type'] = this._sourceType; return component; } }