From 6e1ea0e038a4408ee4002b034f0bc5d5b2754aea Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Mon, 24 Feb 2025 13:54:48 -0500 Subject: [PATCH 1/2] Don't treat non-URL CodeRef modules as URLs - adds commands field to SkillCard to exercise this --- packages/base/code-ref.gts | 22 +++-- packages/base/skill-card.gts | 57 ++++++++++++- .../realm-indexing-and-querying-test.gts | 81 +++++++++++++++++++ 3 files changed, 152 insertions(+), 8 deletions(-) diff --git a/packages/base/code-ref.gts b/packages/base/code-ref.gts index dd4bc9afee..181c8e91bf 100644 --- a/packages/base/code-ref.gts +++ b/packages/base/code-ref.gts @@ -37,9 +37,11 @@ export default class CodeRefField extends FieldDef { _visited?: Set, opts?: SerializeOpts, ) { + const moduleIsUrlLike = + codeRef.module.startsWith('http') || codeRef.module.startsWith('.'); return { ...codeRef, - ...(opts?.maybeRelativeURL && !opts?.useAbsoluteURL + ...(opts?.maybeRelativeURL && !opts?.useAbsoluteURL && moduleIsUrlLike ? { module: opts.maybeRelativeURL(codeRef.module) } : {}), }; @@ -70,12 +72,18 @@ function maybeSerializeCodeRef( stack: CardDef[] = [], ) { if (codeRef) { - // if a stack is passed in, use the containing card to resolve relative references - let moduleHref = - stack.length > 0 - ? new URL(codeRef.module, stack[0][relativeTo]).href - : codeRef.module; - return `${moduleHref}/${codeRef.name}`; + const moduleIsUrlLike = + codeRef.module.startsWith('http') || codeRef.module.startsWith('.'); + if (moduleIsUrlLike) { + // if a stack is passed in, use the containing card to resolve relative references + let moduleHref = + stack.length > 0 + ? new URL(codeRef.module, stack[0][relativeTo]).href + : codeRef.module; + return `${moduleHref}/${codeRef.name}`; + } else { + return `${codeRef.module}/${codeRef.name}`; + } } return undefined; } diff --git a/packages/base/skill-card.gts b/packages/base/skill-card.gts index 02fced1a4f..a8283bb345 100644 --- a/packages/base/skill-card.gts +++ b/packages/base/skill-card.gts @@ -1,11 +1,66 @@ +import { + CardDef, + Component, + FieldDef, + field, + contains, + containsMany, +} from './card-api'; +import BooleanField from './boolean'; +import CodeRefField from './code-ref'; import MarkdownField from './markdown'; -import { CardDef, Component, field, contains } from './card-api'; +import StringField from './string'; import RobotIcon from '@cardstack/boxel-icons/robot'; +function djb2_xor(str: string) { + let len = str.length; + let h = 5381; + + for (let i = 0; i < len; i++) { + h = (h * 33) ^ str.charCodeAt(i); + } + return (h >>> 0).toString(16); +} + +function friendlyModuleName(fullModuleUrl: string) { + return fullModuleUrl + .split('/') + .slice(-1)[0] + .replace(/\.gts$/, ' '); +} + +export class CommandField extends FieldDef { + static displayName = 'CommandField'; + @field codeRef = contains(CodeRefField, { + description: 'An absolute code reference to the command to be executed', + }); + @field requiresApproval = contains(BooleanField, { + description: + 'If true, this command will require human approval before it is executed in the host.', + }); + + @field functionName = contains(StringField, { + description: 'The name of the function to be executed', + computeVia: function (this: CommandField) { + if (!this.codeRef?.module || !this.codeRef?.name) { + return ''; + } + + const hashed = djb2_xor(`${this.codeRef.module}#${this.codeRef.name}`); + let name = + this.codeRef.name === 'default' + ? friendlyModuleName(this.codeRef.module) + : this.codeRef.name; + return `${name}_${hashed.slice(0, 4)}`; + }, + }); +} + export class SkillCard extends CardDef { static displayName = 'Skill'; static icon = RobotIcon; @field instructions = contains(MarkdownField); + @field commands = containsMany(CommandField); static embedded = class Embedded extends Component {