From ae0c0f83e1b79e6401a80935942197b8985dfe0d Mon Sep 17 00:00:00 2001 From: Midas Saulsbury Date: Wed, 22 Oct 2025 12:21:55 -0700 Subject: [PATCH] Refine conversion runtime and refresh AI Shortcuts UI --- cipherstick/AI-Shortcuts/conversions.js | 283 ++++++++++++++++++++-- cipherstick/AI-Shortcuts/index.html | 299 +++++++++++++++++++++++- 2 files changed, 557 insertions(+), 25 deletions(-) diff --git a/cipherstick/AI-Shortcuts/conversions.js b/cipherstick/AI-Shortcuts/conversions.js index 71e4e29..8507c91 100644 --- a/cipherstick/AI-Shortcuts/conversions.js +++ b/cipherstick/AI-Shortcuts/conversions.js @@ -1909,6 +1909,242 @@ const userConversions = (() => { return XML.str(String(value)); } + const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + function escapeRegExp(str) { + return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function indentXMLBlock(xml, indent) { + const trimmed = String(xml ?? '').trim(); + if (!trimmed) return ''; + const lines = trimmed.split(/\r?\n/); + const formatted = lines.map((line) => `${indent}${line}`).join('\n'); + return `\n${formatted}\n`; + } + + function replaceVariableBlock(template, key, xml) { + const placeholder = `{{${key}}}`; + if (!template.includes(placeholder)) { + return { template, replaced: false }; + } + + const pattern = new RegExp(`(^[\t ]*[^<]+<\/key>\s*)[\\s\\S]*?${escapeRegExp(placeholder)}[\\s\\S]*?<\/dict>`, 'm'); + const match = pattern.exec(template); + if (!match) { + return { template, replaced: false }; + } + + const [fullMatch, keySegment] = match; + const trailingWhitespaceMatch = fullMatch.match(/\s*$/); + const trailingWhitespace = trailingWhitespaceMatch ? trailingWhitespaceMatch[0] : ''; + const indentMatch = keySegment.match(/^[ \t]*/); + const indent = indentMatch ? `${indentMatch[0]} ` : ' '; + const formattedBlock = indentXMLBlock(xml, indent); + const replacement = `${keySegment}${formattedBlock}${trailingWhitespace}`; + const updated = template.slice(0, match.index) + replacement + template.slice(match.index + fullMatch.length); + return { template: updated, replaced: true }; + } + + class ConversionRuntime { + constructor() { + this.namedUUIDs = new Map(); + this.namedLabels = new Map(); + } + + cleanToken(token) { + if (token == null) return ''; + let t = String(token).trim(); + if (!t) return ''; + t = t.replace(/^\{+/, '').replace(/\}+$/, ''); + t = t.replace(/^\((.*)\)$/, '$1'); + t = t.replace(/^"(.+)"$/, '$1'); + t = t.replace(/^'(.+)'$/, '$1'); + return t.trim(); + } + + guessDisplayName(name) { + const base = this.cleanToken(name); + if (!base) return ''; + const spaced = base.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim(); + if (!spaced) return base; + return spaced.replace(/\b\w/g, (c) => c.toUpperCase()); + } + + ensureUUIDForName(name, labelHint) { + const clean = this.cleanToken(name); + if (!clean) { + const uuid = genUUID(); + return uuid; + } + if (!this.namedUUIDs.has(clean)) { + this.namedUUIDs.set(clean, genUUID()); + } + if (labelHint) { + this.namedLabels.set(clean, labelHint); + } + return this.namedUUIDs.get(clean); + } + + resolveUUIDToken(token) { + const clean = this.cleanToken(token); + if (!clean) { + const uuid = genUUID(); + return { name: clean, uuid, label: this.guessDisplayName(clean) }; + } + if (UUID_REGEX.test(clean)) { + const label = this.namedLabels.get(clean) ?? this.guessDisplayName(clean); + return { name: clean, uuid: clean, label }; + } + const label = this.namedLabels.get(clean) ?? this.guessDisplayName(clean); + const uuid = this.ensureUUIDForName(clean, label); + return { name: clean, uuid, label }; + } + + extractPlaceholderName(value) { + if (typeof value !== 'string') return null; + const match = value.match(/^\{\{\s*([^{}]+?)\s*\}\}$/); + if (!match) return null; + const inner = this.cleanToken(match[1]); + if (!inner) return null; + const upper = inner.toUpperCase(); + if (['STRING', 'NUMBER', 'BOOLEAN', 'VARIABLE', 'RAW', 'UUID'].includes(upper)) return null; + return inner; + } + + isIDNameKey(key) { + const normalized = String(key || '').replace(/[^a-z0-9]/gi, '').toLowerCase(); + return normalized === 'idname'; + } + + isUUIDKey(key) { + const normalized = String(key || '').toLowerCase(); + return normalized.includes('uuid'); + } + + makeUUIDVariableBlock(key, token) { + const resolved = this.resolveUUIDToken(token); + const xml = renderValue(variableValue({ uuid: resolved.uuid, name: resolved.label || resolved.name })); + return { key, xml, uuid: resolved.uuid, name: resolved.name }; + } + + makeNamedVariableBlock(key, token) { + const name = this.cleanToken(token); + const xml = renderValue(variableValue({ + type: 'Variable', + value: { Type: 'Variable', VariableName: name } + })); + return { key, xml, name }; + } + + transformNestedValue(value) { + if (Array.isArray(value)) { + return value.map((item) => this.transformNestedValue(item)); + } + if (isPlainObject(value)) { + const obj = {}; + for (const [k, v] of Object.entries(value)) { + obj[k] = this.transformNestedValue(v); + } + return obj; + } + return value; + } + + transformParam(key, rawValue) { + if (typeof rawValue === 'string') { + const trimmed = rawValue.trim(); + + if (this.isIDNameKey(key)) { + const extracted = this.extractPlaceholderName(trimmed) ?? this.cleanToken(trimmed); + if (extracted) { + const label = this.guessDisplayName(extracted); + const uuid = this.ensureUUIDForName(extracted, label); + return { value: rawValue, namedUUID: { name: extracted, uuid } }; + } + } + + if (trimmed.startsWith('$')) { + const block = this.makeUUIDVariableBlock(key, trimmed.slice(1)); + return { variableBlock: block }; + } + + if (trimmed.startsWith('!')) { + const block = this.makeNamedVariableBlock(key, trimmed.slice(1)); + return { variableBlock: block }; + } + + if (this.isUUIDKey(key)) { + if (trimmed.startsWith('$')) { + const resolved = this.resolveUUIDToken(trimmed.slice(1)); + return { value: resolved.uuid }; + } + const extracted = this.extractPlaceholderName(trimmed); + if (extracted) { + const uuid = this.ensureUUIDForName(extracted, this.guessDisplayName(extracted)); + return { value: uuid }; + } + const clean = this.cleanToken(trimmed); + if (UUID_REGEX.test(clean)) { + return { value: clean }; + } + if (this.namedUUIDs.has(clean)) { + return { value: this.namedUUIDs.get(clean) }; + } + } + } + + if (Array.isArray(rawValue)) { + return { value: rawValue.map((item) => this.transformNestedValue(item)) }; + } + + if (isPlainObject(rawValue)) { + const obj = {}; + for (const [k, v] of Object.entries(rawValue)) { + obj[k] = this.transformNestedValue(v); + } + return { value: obj }; + } + + return { value: rawValue }; + } + + prepareParamsForTemplate(template, params = {}) { + let workingTemplate = template; + const prepared = {}; + const idUUIDs = []; + + for (const [key, rawValue] of Object.entries(params)) { + const result = this.transformParam(key, rawValue); + if (result.variableBlock) { + const { template: updated, replaced } = replaceVariableBlock(workingTemplate, key, result.variableBlock.xml); + if (replaced) { + workingTemplate = updated; + if (workingTemplate.includes(`{{${key}}}`)) { + prepared[key] = ''; + } + continue; + } + prepared[key] = rawValue; + continue; + } + + if (result.value !== undefined) { + prepared[key] = result.value; + } + if (result.namedUUID) { + idUUIDs.push(result.namedUUID); + } + } + + if (prepared.UUID == null && idUUIDs.length) { + prepared.UUID = idUUIDs[0].uuid; + } + + return { template: workingTemplate, params: prepared }; + } + } + function mergeParams(...sources) { const out = {}; for (const src of sources) { @@ -2275,8 +2511,9 @@ const userConversions = (() => { const prog = coerceProgram(program, name); const wfName = String(prog.name || name || 'My Shortcut'); const actions = prog.actions; + const runtime = new ConversionRuntime(); - const plistActions = await buildActionsArrayFromJSON(actions); + const plistActions = await buildActionsArrayFromJSON(actions, runtime); const plist = ` @@ -2483,7 +2720,7 @@ const userConversions = (() => { ] }; - async function buildSpecialIf(item) { + async function buildSpecialIf(item, context) { const params = (item && typeof item === 'object') ? (item.params || {}) : {}; const providedGroup = @@ -2530,11 +2767,11 @@ const userConversions = (() => { const endUUIDNode = ensureStringNode(endUUID ?? genUUID(), 'UUID'); const thenActions = Array.isArray(item?.then) && item.then.length - ? await buildActionsArrayFromJSON(item.then) + ? await buildActionsArrayFromJSON(item.then, context) : [comment('If (then) has no actions')]; const elseActions = Array.isArray(item?.else) && item.else.length - ? await buildActionsArrayFromJSON(item.else) + ? await buildActionsArrayFromJSON(item.else, context) : [comment('If (else) has no actions')]; const templateValues = { @@ -2552,7 +2789,7 @@ const userConversions = (() => { return SPECIAL_ACTION_TEMPLATES.IF.map((part) => replaceTemplate(part, templateValues)); } - async function buildSpecialRepeatCount(item) { + async function buildSpecialRepeatCount(item, context) { const params = (item && typeof item === 'object') ? (item.params || {}) : {}; const providedGroup = @@ -2574,7 +2811,7 @@ const userConversions = (() => { const endUUIDNode = ensureStringNode(endUUID ?? genUUID(), 'UUID'); const bodyActions = Array.isArray(item?.do) && item.do.length - ? await buildActionsArrayFromJSON(item.do) + ? await buildActionsArrayFromJSON(item.do, context) : [comment('Repeat has no actions')]; const templateValues = { @@ -2587,7 +2824,7 @@ const userConversions = (() => { return SPECIAL_ACTION_TEMPLATES.REPEAT.map((part) => replaceTemplate(part, templateValues)); } - async function buildSpecialRepeatEach(item) { + async function buildSpecialRepeatEach(item, context) { const params = (item && typeof item === 'object') ? (item.params || {}) : {}; const providedGroup = @@ -2611,7 +2848,7 @@ const userConversions = (() => { const endUUIDNode = ensureStringNode(endUUID ?? genUUID(), 'UUID'); const bodyActions = Array.isArray(item?.do) && item.do.length - ? await buildActionsArrayFromJSON(item.do) + ? await buildActionsArrayFromJSON(item.do, context) : [comment('Repeat Each has no actions')]; const templateValues = { @@ -2643,11 +2880,12 @@ const userConversions = (() => { // - Repeat: { action:"Repeat", params:{ Count }, do:[...] } // - RepeatEach: { action:"RepeatEach", params:{ Items }, do:[...] } // - If: { action:"If", params:{ Condition }, then:[...], else:[...] } - async function buildActionsArrayFromJSON(list) { + async function buildActionsArrayFromJSON(list, context) { + const runtime = context instanceof ConversionRuntime ? context : new ConversionRuntime(); const out = []; for (const item of list) { if (typeof item === 'string') { - out.push(await buildActionFromConversions(item, {})); + out.push(await buildActionFromConversions(item, {}, runtime)); continue; } if (!item || typeof item !== 'object') { @@ -2662,18 +2900,18 @@ const userConversions = (() => { const specialBuilder = SPECIAL_ACTION_BUILDERS.get(normalizeName(kind)); if (specialBuilder) { - out.push(...await specialBuilder(item)); + out.push(...await specialBuilder(item, runtime)); continue; } // Regular action - out.push(await buildActionFromConversions(kind, item.params || {})); + out.push(await buildActionFromConversions(kind, item.params || {}, runtime)); } return out; } // ---- Build one action from Conversions/ dict template ---- - async function buildActionFromConversions(actionName, params) { + async function buildActionFromConversions(actionName, params, context) { // Find file const filename = await lookupConversionFileForAction(actionName); if (!filename) { @@ -2683,8 +2921,11 @@ const userConversions = (() => { // Load snippet const dictXML = await loadConvFile(filename); + const runtime = context instanceof ConversionRuntime ? context : new ConversionRuntime(); + const prepared = runtime.prepareParamsForTemplate(dictXML, params || {}); + // Substitutions - let substituted = substitutePlaceholders(dictXML, params || {}); + let substituted = substitutePlaceholders(prepared.template, prepared.params); substituted = postProcessAskLLM(substituted, params || {}); // Ensure it *looks* like a dict (we won't attempt to validate fully) @@ -2728,7 +2969,7 @@ const userConversions = (() => { }); } - async function buildRepeatCountBlock(item) { + async function buildRepeatCountBlock(item, context) { const params = (item && typeof item === 'object') ? (item.params || {}) : {}; const groupId = params.GroupingIdentifier ?? @@ -2757,7 +2998,7 @@ const userConversions = (() => { const startAction = makeAction(IDS.REPEAT_COUNT, startParams); const bodyActions = Array.isArray(item?.do) - ? await buildActionsArrayFromJSON(item.do) + ? await buildActionsArrayFromJSON(item.do, context) : [comment('Repeat has no "do" array')]; const endUUID = params.EndUUID ?? params.UUIDEnd ?? null; @@ -2774,7 +3015,7 @@ const userConversions = (() => { return [startAction, ...bodyActions, endAction]; } - async function buildRepeatEachBlock(item) { + async function buildRepeatEachBlock(item, context) { const params = (item && typeof item === 'object') ? (item.params || {}) : {}; const groupId = params.GroupingIdentifier ?? @@ -2803,7 +3044,7 @@ const userConversions = (() => { const startAction = makeAction(IDS.REPEAT_EACH, startParams); const bodyActions = Array.isArray(item?.do) - ? await buildActionsArrayFromJSON(item.do) + ? await buildActionsArrayFromJSON(item.do, context) : [comment('RepeatEach has no "do" array')]; const endUUID = params.EndUUID ?? params.UUIDEnd ?? null; @@ -2820,7 +3061,7 @@ const userConversions = (() => { return [startAction, ...bodyActions, endAction]; } - async function buildIfBlock(item) { + async function buildIfBlock(item, context) { const params = (item && typeof item === 'object') ? (item.params || {}) : {}; const groupId = params.GroupingIdentifier ?? @@ -2892,11 +3133,11 @@ const userConversions = (() => { const endAction = makeAction(IDS.IF, endParams); const thenActions = Array.isArray(item?.then) - ? await buildActionsArrayFromJSON(item.then) + ? await buildActionsArrayFromJSON(item.then, context) : [comment('If has no "then" array')]; const elseActions = Array.isArray(item?.else) - ? await buildActionsArrayFromJSON(item.else) + ? await buildActionsArrayFromJSON(item.else, context) : []; return [startAction, ...thenActions, elseAction, ...elseActions, endAction]; diff --git a/cipherstick/AI-Shortcuts/index.html b/cipherstick/AI-Shortcuts/index.html index 5d7211c..14481ed 100644 --- a/cipherstick/AI-Shortcuts/index.html +++ b/cipherstick/AI-Shortcuts/index.html @@ -12,13 +12,18 @@ --bg-surface: #ffffff; --text-primary: #0f172a; --text-secondary: #475569; + --text-muted: rgba(15, 23, 42, 0.48); --outline: rgba(15, 23, 42, 0.08); --shadow: 0 24px 60px rgba(15, 23, 42, 0.08); --accent: #2563eb; --accent-dark: #1d4ed8; --accent-soft: rgba(37, 99, 235, 0.12); + --accent-glow: rgba(37, 99, 235, 0.22); + --good: #10b981; + --warn: #f59e0b; --radius-lg: 18px; --radius-md: 12px; + --radius-sm: 8px; } * { box-sizing: border-box; } @@ -28,12 +33,29 @@ min-height: 100vh; background: var(--bg-page); color: var(--text-primary); + position: relative; + overflow-x: hidden; + } + + body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + background: + radial-gradient(600px circle at 20% 15%, rgba(59, 130, 246, 0.18), transparent 60%), + radial-gradient(520px circle at 80% 5%, rgba(99, 102, 241, 0.16), transparent 65%), + radial-gradient(680px circle at 50% 85%, rgba(45, 212, 191, 0.12), transparent 70%); + filter: blur(0px); + z-index: 0; } .page { min-height: 100vh; display: flex; flex-direction: column; + position: relative; + z-index: 1; } .container { @@ -43,8 +65,23 @@ } .hero { - padding: 72px 0 48px; + padding: 88px 0 56px; text-align: center; + position: relative; + overflow: hidden; + } + + .hero::after { + content: ''; + position: absolute; + width: 480px; + height: 480px; + background: radial-gradient(circle, rgba(37, 99, 235, 0.22), transparent 65%); + top: -220px; + right: -160px; + transform: rotate(12deg); + pointer-events: none; + z-index: -1; } .hero__badge { @@ -62,9 +99,10 @@ } .hero h1 { - margin: 20px 0 12px; - font-size: clamp(2.4rem, 4vw, 2.9rem); + margin: 20px 0 16px; + font-size: clamp(2.6rem, 4vw, 3.2rem); font-weight: 700; + letter-spacing: -0.02em; } .hero p { @@ -75,19 +113,87 @@ line-height: 1.6; } + .hero__subgrid { + margin-top: 44px; + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; + } + + .hero__cta-card { + display: flex; + flex-direction: column; + gap: 12px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(148, 163, 184, 0.18); + border-radius: 20px; + padding: 20px 26px; + max-width: 320px; + box-shadow: 0 30px 70px rgba(15, 23, 42, 0.1); + backdrop-filter: blur(18px); + text-align: left; + } + + .hero__cta-card strong { + font-size: 0.95rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent); + } + + .hero__cta-card ul { + margin: 0; + padding-left: 18px; + color: var(--text-secondary); + font-size: 0.92rem; + line-height: 1.5; + } + .workspace { padding-bottom: 64px; display: grid; gap: 32px; } + .workflow-steps { + display: flex; + gap: 16px; + flex-wrap: wrap; + padding: 8px 0 10px; + } + + .workflow-step { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.05); + font-size: 0.9rem; + font-weight: 600; + color: var(--text-muted); + } + + .workflow-step__index { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: rgba(37, 99, 235, 0.12); + color: var(--accent); + font-size: 0.85rem; + } + .panel { background: var(--bg-surface); border-radius: var(--radius-lg); box-shadow: var(--shadow); padding: 28px; border: 1px solid var(--outline); - } + } .panel__heading h2 { margin: 0; @@ -224,6 +330,40 @@ font-size: 0.9rem; } + .prompt-samples { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 8px; + color: var(--text-secondary); + font-size: 0.85rem; + align-items: center; + } + + .prompt-samples__label { + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-muted); + } + + .prompt-chip { + border: none; + padding: 8px 14px; + border-radius: 999px; + background: rgba(37, 99, 235, 0.12); + color: var(--accent); + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + transition: transform 0.15s ease, background 0.2s ease; + } + + .prompt-chip:hover { + background: rgba(37, 99, 235, 0.18); + transform: translateY(-1px); + } + .editor-grid { display: grid; gap: 24px; @@ -268,6 +408,76 @@ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; } + + .feature-strip { + padding: 0 0 36px; + } + + .feature-strip__grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + } + + .feature-card { + background: rgba(255, 255, 255, 0.88); + border-radius: var(--radius-lg); + border: 1px solid rgba(148, 163, 184, 0.18); + padding: 22px; + box-shadow: 0 20px 50px rgba(15, 23, 42, 0.08); + text-align: left; + backdrop-filter: blur(12px); + } + + .feature-card__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 12px; + background: rgba(37, 99, 235, 0.1); + color: var(--accent); + font-size: 1.4rem; + margin-bottom: 12px; + } + + .feature-card h3 { + margin: 0 0 10px; + font-size: 1.05rem; + } + + .feature-card p { + margin: 0; + color: var(--text-secondary); + font-size: 0.92rem; + line-height: 1.6; + } + + code { + font-family: inherit; + background: rgba(15, 23, 42, 0.08); + padding: 2px 6px; + border-radius: var(--radius-sm); + font-weight: 600; + } + + footer { + padding: 36px 0 48px; + color: var(--text-secondary); + font-size: 0.85rem; + text-align: center; + } + + footer a { + color: var(--accent); + text-decoration: none; + font-weight: 600; + } + + footer a:hover { + text-decoration: underline; + } @@ -277,9 +487,47 @@ AI Shortcuts Toolkit

AI Shortcut Maker

Describe what you want and turn it into editable JSON or a ready-to-import Shortcuts plist in seconds.

+
+
+ Three simple moves +
    +
  • Share your idea with the planner.
  • +
  • Fine-tune the generated JSON or DSL.
  • +
  • Convert, sign, and share it instantly.
  • +
+
+
+ Designed for builders +
    +
  • Understands variable references and UUID links.
  • +
  • Works offline with embedded conversion templates.
  • +
  • Outputs clean, ready-to-import Shortcuts files.
  • +
+
+
+
+
+
+ 🤖 +

Smarter planning

+

The integrated worker compares your prompt against curated templates, producing a solid first draft you can edit immediately.

+
+
+ 🧩 +

Dynamic variables

+

Use $Name or !Variable markers in your JSON and the converter rebuilds the proper Shortcuts attachments for you.

+
+
+ ⚡️ +

Fast signing

+

Once your plist looks perfect, sign it with a click and deliver a Shortcut that imports without warnings on macOS or iOS.

+
+
+
+
@@ -297,6 +545,13 @@

Describe your shortcut

+
+ Try a sample + + + +
+
idle @@ -306,6 +561,20 @@

Describe your shortcut

+
+
+ 1 + Plan & edit JSON +
+
+ 2 + Convert to plist +
+
+ 3 + Sign & ship +
+
@@ -339,6 +608,12 @@

.plist XML

+ +
+
+ Built by the CipherStick crew. Need help or have an idea for a new template? Reach out and let's improve the Shortcut ecosystem together. +
+