@yadsh/dsh-qa-surface 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -9
- package/compatibility.json +10 -1
- package/cordis.patch.yml +9 -0
- package/lib/account-remotes.js +10 -0
- package/lib/account-remotes.js.map +1 -1
- package/lib/accounts/errors.js.map +1 -1
- package/lib/accounts/file.js.map +1 -1
- package/lib/accounts/store.js +22 -0
- package/lib/accounts/store.js.map +1 -1
- package/lib/client/QaAccountsController.js +29 -0
- package/lib/client/QaAccountsController.js.map +1 -1
- package/lib/client/QaSessionController.js +168 -48
- package/lib/client/QaSessionController.js.map +1 -1
- package/lib/client/QaSurface.js +125 -56
- package/lib/client/QaSurface.js.map +1 -1
- package/lib/client/QaSurfaceGuard.js +96 -0
- package/lib/client/QaSurfaceGuard.js.map +1 -0
- package/lib/client/QaTranscriptAdapter.js +11 -5
- package/lib/client/QaTranscriptAdapter.js.map +1 -1
- package/lib/client/components/QaChangelog.js +31 -0
- package/lib/client/components/QaChangelog.js.map +1 -1
- package/lib/client/components/QaComposer.js +1 -1
- package/lib/client/components/QaComposer.js.map +1 -1
- package/lib/client/components/QaHeader.js +2 -8
- package/lib/client/components/QaHeader.js.map +1 -1
- package/lib/client/components/QaMessage.js +20 -7
- package/lib/client/components/QaMessage.js.map +1 -1
- package/lib/client/components/QaModal.js +24 -6
- package/lib/client/components/QaModal.js.map +1 -1
- package/lib/client/components/QaSidebar.js +2 -6
- package/lib/client/components/QaSidebar.js.map +1 -1
- package/lib/client/components/QaSourcesPanel.js +6 -5
- package/lib/client/components/QaSourcesPanel.js.map +1 -1
- package/lib/client/components/QaWidthHandle.js +21 -1
- package/lib/client/components/QaWidthHandle.js.map +1 -1
- package/lib/client/components/surface-utils.js +2 -0
- package/lib/client/components/surface-utils.js.map +1 -1
- package/lib/client/index.js +72 -24
- package/lib/client/index.js.map +1 -1
- package/lib/client/panels/PanelHost.js +209 -0
- package/lib/client/panels/PanelHost.js.map +1 -0
- package/lib/client/panels/PanelLauncher.js +39 -0
- package/lib/client/panels/PanelLauncher.js.map +1 -0
- package/lib/client/panels/contract.js +2 -0
- package/lib/client/panels/contract.js.map +1 -0
- package/lib/client/panels/index.js +2 -0
- package/lib/client/panels/index.js.map +1 -0
- package/lib/client/panels/registry.js +171 -0
- package/lib/client/panels/registry.js.map +1 -0
- package/lib/client/project-session-state.js +3 -0
- package/lib/client/project-session-state.js.map +1 -1
- package/lib/client/settings/card.js +2 -2
- package/lib/client/settings/card.js.map +1 -1
- package/lib/client/settings/sections/accounts.js +3 -1
- package/lib/client/settings/sections/accounts.js.map +1 -1
- package/lib/client/settings/sections/documents.js +88 -0
- package/lib/client/settings/sections/documents.js.map +1 -0
- package/lib/client/settings/sections/interface.js +2 -0
- package/lib/client/settings/sections/interface.js.map +1 -1
- package/lib/client/settings/sections.js +1 -0
- package/lib/client/settings/sections.js.map +1 -1
- package/lib/client/settings-extensions/contract.js +2 -0
- package/lib/client/settings-extensions/contract.js.map +1 -0
- package/lib/client/settings-extensions/index.js +2 -0
- package/lib/client/settings-extensions/index.js.map +1 -0
- package/lib/client/settings-extensions/registry.js +63 -0
- package/lib/client/settings-extensions/registry.js.map +1 -0
- package/lib/client/settlement.js +51 -5
- package/lib/client/settlement.js.map +1 -1
- package/lib/client/source-preview.js +24 -0
- package/lib/client/source-preview.js.map +1 -0
- package/lib/client/styles.js +157 -32
- package/lib/client/styles.js.map +1 -1
- package/lib/client/subagent-codenames.js +109 -0
- package/lib/client/subagent-codenames.js.map +1 -0
- package/lib/client/types.js +1 -0
- package/lib/client/types.js.map +1 -1
- package/lib/client/user-settings/GeneralSettingsPage.js +13 -0
- package/lib/client/user-settings/GeneralSettingsPage.js.map +1 -0
- package/lib/client/user-settings/ProfileSettingsPage.js +59 -0
- package/lib/client/user-settings/ProfileSettingsPage.js.map +1 -0
- package/lib/client/user-settings/SkillCatalog.js +23 -0
- package/lib/client/user-settings/SkillCatalog.js.map +1 -0
- package/lib/client/user-settings/SkillEditor.js +89 -0
- package/lib/client/user-settings/SkillEditor.js.map +1 -0
- package/lib/client/user-settings/SkillToolPicker.js +58 -0
- package/lib/client/user-settings/SkillToolPicker.js.map +1 -0
- package/lib/client/user-settings/SkillsSettingsPage.js +171 -0
- package/lib/client/user-settings/SkillsSettingsPage.js.map +1 -0
- package/lib/client/user-settings/StartersSettingsPage.js +78 -0
- package/lib/client/user-settings/StartersSettingsPage.js.map +1 -0
- package/lib/client/user-settings/UserSettingsDialog.js +50 -0
- package/lib/client/user-settings/UserSettingsDialog.js.map +1 -0
- package/lib/client/user-settings/copy.js +84 -0
- package/lib/client/user-settings/copy.js.map +1 -0
- package/lib/client/user-settings/draft.js +127 -0
- package/lib/client/user-settings/draft.js.map +1 -0
- package/lib/client/user-settings/fields.js +24 -0
- package/lib/client/user-settings/fields.js.map +1 -0
- package/lib/client.js +5467 -1379
- package/lib/client.js.map +1 -1
- package/lib/config-resolvers/accounts.js +30 -0
- package/lib/config-resolvers/accounts.js.map +1 -1
- package/lib/config-resolvers/defaults.js +23 -0
- package/lib/config-resolvers/defaults.js.map +1 -1
- package/lib/config-resolvers/tools.js +37 -0
- package/lib/config-resolvers/tools.js.map +1 -0
- package/lib/config-resolvers/ui.js +2 -0
- package/lib/config-resolvers/ui.js.map +1 -1
- package/lib/config.js +288 -0
- package/lib/config.js.map +1 -1
- package/lib/documents/artifacts/ids.js +43 -0
- package/lib/documents/artifacts/ids.js.map +1 -0
- package/lib/documents/artifacts/manifest.js +66 -0
- package/lib/documents/artifacts/manifest.js.map +1 -0
- package/lib/documents/artifacts/store.js +169 -0
- package/lib/documents/artifacts/store.js.map +1 -0
- package/lib/documents/capabilities.js +93 -0
- package/lib/documents/capabilities.js.map +1 -0
- package/lib/documents/config.js +302 -0
- package/lib/documents/config.js.map +1 -0
- package/lib/documents/defaults.js +78 -0
- package/lib/documents/defaults.js.map +1 -0
- package/lib/documents/errors.js +101 -0
- package/lib/documents/errors.js.map +1 -0
- package/lib/documents/index.js +80 -0
- package/lib/documents/index.js.map +1 -0
- package/lib/documents/inspect/facts.js +161 -0
- package/lib/documents/inspect/facts.js.map +1 -0
- package/lib/documents/inspect/zip.js +108 -0
- package/lib/documents/inspect/zip.js.map +1 -0
- package/lib/documents/markdown/directives.js +135 -0
- package/lib/documents/markdown/directives.js.map +1 -0
- package/lib/documents/markdown/frontmatter.js +35 -0
- package/lib/documents/markdown/frontmatter.js.map +1 -0
- package/lib/documents/markdown/normalize.js +113 -0
- package/lib/documents/markdown/normalize.js.map +1 -0
- package/lib/documents/orchestrator/assets.js +243 -0
- package/lib/documents/orchestrator/assets.js.map +1 -0
- package/lib/documents/orchestrator/convert-document.js +186 -0
- package/lib/documents/orchestrator/convert-document.js.map +1 -0
- package/lib/documents/orchestrator/create-document.js +292 -0
- package/lib/documents/orchestrator/create-document.js.map +1 -0
- package/lib/documents/orchestrator/extract-document.js +162 -0
- package/lib/documents/orchestrator/extract-document.js.map +1 -0
- package/lib/documents/orchestrator/inspect-document.js +89 -0
- package/lib/documents/orchestrator/inspect-document.js.map +1 -0
- package/lib/documents/orchestrator/runtime-deps.js +59 -0
- package/lib/documents/orchestrator/runtime-deps.js.map +1 -0
- package/lib/documents/orchestrator/scope.js +132 -0
- package/lib/documents/orchestrator/scope.js.map +1 -0
- package/lib/documents/providers/docling.js +247 -0
- package/lib/documents/providers/docling.js.map +1 -0
- package/lib/documents/providers/libreoffice.js +130 -0
- package/lib/documents/providers/libreoffice.js.map +1 -0
- package/lib/documents/providers/markitdown.js +89 -0
- package/lib/documents/providers/markitdown.js.map +1 -0
- package/lib/documents/providers/pandoc.js +161 -0
- package/lib/documents/providers/pandoc.js.map +1 -0
- package/lib/documents/providers/process.js +164 -0
- package/lib/documents/providers/process.js.map +1 -0
- package/lib/documents/providers/registry.js +152 -0
- package/lib/documents/providers/registry.js.map +1 -0
- package/lib/documents/providers/shared.js +75 -0
- package/lib/documents/providers/shared.js.map +1 -0
- package/lib/documents/runtime.js +114 -0
- package/lib/documents/runtime.js.map +1 -0
- package/lib/documents/security/file-types.js +188 -0
- package/lib/documents/security/file-types.js.map +1 -0
- package/lib/documents/security/limits.js +78 -0
- package/lib/documents/security/limits.js.map +1 -0
- package/lib/documents/security/mime.js +43 -0
- package/lib/documents/security/mime.js.map +1 -0
- package/lib/documents/security/paths.js +133 -0
- package/lib/documents/security/paths.js.map +1 -0
- package/lib/documents/templates/registry.js +147 -0
- package/lib/documents/templates/registry.js.map +1 -0
- package/lib/documents/templates/resolver.js +59 -0
- package/lib/documents/templates/resolver.js.map +1 -0
- package/lib/documents/tools/convert.js +125 -0
- package/lib/documents/tools/convert.js.map +1 -0
- package/lib/documents/tools/create.js +187 -0
- package/lib/documents/tools/create.js.map +1 -0
- package/lib/documents/tools/index.js +41 -0
- package/lib/documents/tools/index.js.map +1 -0
- package/lib/documents/tools/inspect.js +135 -0
- package/lib/documents/tools/inspect.js.map +1 -0
- package/lib/documents/tools/shared.js +88 -0
- package/lib/documents/tools/shared.js.map +1 -0
- package/lib/documents/tools/to-markdown.js +125 -0
- package/lib/documents/tools/to-markdown.js.map +1 -0
- package/lib/documents/types.js +12 -0
- package/lib/documents/types.js.map +1 -0
- package/lib/index.js +225 -20
- package/lib/index.js.map +1 -1
- package/lib/integration-principals.js +22 -0
- package/lib/integration-principals.js.map +1 -0
- package/lib/lockdown-policy.js +15 -3
- package/lib/lockdown-policy.js.map +1 -1
- package/lib/personal-skills/errors.js +9 -0
- package/lib/personal-skills/errors.js.map +1 -0
- package/lib/personal-skills/host.js +80 -0
- package/lib/personal-skills/host.js.map +1 -0
- package/lib/personal-skills/index.js +10 -0
- package/lib/personal-skills/index.js.map +1 -0
- package/lib/personal-skills/paths.js +127 -0
- package/lib/personal-skills/paths.js.map +1 -0
- package/lib/personal-skills/provider.js +109 -0
- package/lib/personal-skills/provider.js.map +1 -0
- package/lib/personal-skills/remotes.js +40 -0
- package/lib/personal-skills/remotes.js.map +1 -0
- package/lib/personal-skills/service.js +686 -0
- package/lib/personal-skills/service.js.map +1 -0
- package/lib/personal-skills/skill-file.js +278 -0
- package/lib/personal-skills/skill-file.js.map +1 -0
- package/lib/personal-skills/skill-format.js +202 -0
- package/lib/personal-skills/skill-format.js.map +1 -0
- package/lib/personal-skills/watcher.js +96 -0
- package/lib/personal-skills/watcher.js.map +1 -0
- package/lib/profile.js +6 -4
- package/lib/profile.js.map +1 -1
- package/lib/prompt-notes.js +19 -0
- package/lib/prompt-notes.js.map +1 -1
- package/lib/provenance/file-preview.js +68 -21
- package/lib/provenance/file-preview.js.map +1 -1
- package/lib/provenance/host-store.js +44 -11
- package/lib/provenance/host-store.js.map +1 -1
- package/lib/provenance/index.js +1 -0
- package/lib/provenance/index.js.map +1 -1
- package/lib/provenance/normalize.js +3 -1
- package/lib/provenance/normalize.js.map +1 -1
- package/lib/provenance/snapshot-store.js +154 -0
- package/lib/provenance/snapshot-store.js.map +1 -0
- package/lib/qa-tools/activation-detector.js +53 -0
- package/lib/qa-tools/activation-detector.js.map +1 -0
- package/lib/qa-tools/activation-manager.js +111 -0
- package/lib/qa-tools/activation-manager.js.map +1 -0
- package/lib/qa-tools/catalog.js +108 -0
- package/lib/qa-tools/catalog.js.map +1 -0
- package/lib/qa-tools/durable-marker.js +115 -0
- package/lib/qa-tools/durable-marker.js.map +1 -0
- package/lib/qa-tools/index.js +83 -0
- package/lib/qa-tools/index.js.map +1 -0
- package/lib/qa-tools/lifecycle.js +64 -0
- package/lib/qa-tools/lifecycle.js.map +1 -0
- package/lib/qa-tools/types.js +2 -0
- package/lib/qa-tools/types.js.map +1 -0
- package/lib/resolve-config.js +5 -0
- package/lib/resolve-config.js.map +1 -1
- package/lib/secure-session.js +35 -3
- package/lib/secure-session.js.map +1 -1
- package/lib/starters.js +117 -0
- package/lib/starters.js.map +1 -0
- package/lib/typert.host.js +767 -22
- package/lib/typert.remote-client.d.ts +17 -1
- package/lib/typert.remote-client.js +609 -17
- package/lib/types/account-remotes.d.ts +3 -1
- package/lib/types/accounts/errors.d.ts +1 -1
- package/lib/types/accounts/file.d.ts +10 -0
- package/lib/types/accounts/store.d.ts +7 -1
- package/lib/types/client/QaAccountsController.d.ts +7 -1
- package/lib/types/client/QaSessionController.d.ts +12 -0
- package/lib/types/client/QaSurface.d.ts +14 -4
- package/lib/types/client/QaSurfaceGuard.d.ts +27 -0
- package/lib/types/client/QaTranscriptAdapter.d.ts +7 -0
- package/lib/types/client/components/QaComposer.d.ts +3 -1
- package/lib/types/client/components/QaHeader.d.ts +3 -1
- package/lib/types/client/components/QaModal.d.ts +5 -4
- package/lib/types/client/components/QaSidebar.d.ts +8 -11
- package/lib/types/client/components/QaWidthHandle.d.ts +13 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/panels/PanelHost.d.ts +18 -0
- package/lib/types/client/panels/PanelLauncher.d.ts +5 -0
- package/lib/types/client/panels/contract.d.ts +83 -0
- package/lib/types/client/panels/index.d.ts +3 -0
- package/lib/types/client/panels/registry.d.ts +40 -0
- package/lib/types/client/project-session-state.d.ts +5 -0
- package/lib/types/client/settings/sections/documents.d.ts +13 -0
- package/lib/types/client/settings/sections.d.ts +1 -0
- package/lib/types/client/settings-extensions/contract.d.ts +32 -0
- package/lib/types/client/settings-extensions/index.d.ts +3 -0
- package/lib/types/client/settings-extensions/registry.d.ts +15 -0
- package/lib/types/client/settlement.d.ts +41 -5
- package/lib/types/client/source-preview.d.ts +8 -0
- package/lib/types/client/styles.d.ts +2 -0
- package/lib/types/client/subagent-codenames.d.ts +18 -0
- package/lib/types/client/types.d.ts +35 -3
- package/lib/types/client/user-settings/GeneralSettingsPage.d.ts +14 -0
- package/lib/types/client/user-settings/ProfileSettingsPage.d.ts +18 -0
- package/lib/types/client/user-settings/SkillCatalog.d.ts +16 -0
- package/lib/types/client/user-settings/SkillEditor.d.ts +42 -0
- package/lib/types/client/user-settings/SkillToolPicker.d.ts +17 -0
- package/lib/types/client/user-settings/SkillsSettingsPage.d.ts +15 -0
- package/lib/types/client/user-settings/StartersSettingsPage.d.ts +14 -0
- package/lib/types/client/user-settings/UserSettingsDialog.d.ts +43 -0
- package/lib/types/client/user-settings/copy.d.ts +25 -0
- package/lib/types/client/user-settings/draft.d.ts +51 -0
- package/lib/types/client/user-settings/fields.d.ts +43 -0
- package/lib/types/config-resolvers/tools.d.ts +6 -0
- package/lib/types/documents/artifacts/ids.d.ts +14 -0
- package/lib/types/documents/artifacts/manifest.d.ts +32 -0
- package/lib/types/documents/artifacts/store.d.ts +67 -0
- package/lib/types/documents/capabilities.d.ts +32 -0
- package/lib/types/documents/config.d.ts +27 -0
- package/lib/types/documents/defaults.d.ts +161 -0
- package/lib/types/documents/errors.d.ts +42 -0
- package/lib/types/documents/index.d.ts +63 -0
- package/lib/types/documents/inspect/facts.d.ts +43 -0
- package/lib/types/documents/inspect/zip.d.ts +28 -0
- package/lib/types/documents/markdown/directives.d.ts +39 -0
- package/lib/types/documents/markdown/frontmatter.d.ts +15 -0
- package/lib/types/documents/markdown/normalize.d.ts +30 -0
- package/lib/types/documents/orchestrator/assets.d.ts +55 -0
- package/lib/types/documents/orchestrator/convert-document.d.ts +25 -0
- package/lib/types/documents/orchestrator/create-document.d.ts +18 -0
- package/lib/types/documents/orchestrator/extract-document.d.ts +24 -0
- package/lib/types/documents/orchestrator/inspect-document.d.ts +12 -0
- package/lib/types/documents/orchestrator/runtime-deps.d.ts +43 -0
- package/lib/types/documents/orchestrator/scope.d.ts +52 -0
- package/lib/types/documents/providers/docling.d.ts +54 -0
- package/lib/types/documents/providers/libreoffice.d.ts +40 -0
- package/lib/types/documents/providers/markitdown.d.ts +28 -0
- package/lib/types/documents/providers/pandoc.d.ts +42 -0
- package/lib/types/documents/providers/process.d.ts +54 -0
- package/lib/types/documents/providers/registry.d.ts +68 -0
- package/lib/types/documents/providers/shared.d.ts +27 -0
- package/lib/types/documents/runtime.d.ts +59 -0
- package/lib/types/documents/security/file-types.d.ts +39 -0
- package/lib/types/documents/security/limits.d.ts +24 -0
- package/lib/types/documents/security/mime.d.ts +13 -0
- package/lib/types/documents/security/paths.d.ts +38 -0
- package/lib/types/documents/templates/registry.d.ts +50 -0
- package/lib/types/documents/templates/resolver.d.ts +30 -0
- package/lib/types/documents/tools/convert.d.ts +9 -0
- package/lib/types/documents/tools/create.d.ts +14 -0
- package/lib/types/documents/tools/index.d.ts +33 -0
- package/lib/types/documents/tools/inspect.d.ts +9 -0
- package/lib/types/documents/tools/shared.d.ts +77 -0
- package/lib/types/documents/tools/to-markdown.d.ts +12 -0
- package/lib/types/documents/types.d.ts +294 -0
- package/lib/types/index.d.ts +75 -3
- package/lib/types/integration-principals.d.ts +9 -0
- package/lib/types/lockdown-policy.d.ts +12 -2
- package/lib/types/personal-skills/errors.d.ts +11 -0
- package/lib/types/personal-skills/host.d.ts +42 -0
- package/lib/types/personal-skills/index.d.ts +10 -0
- package/lib/types/personal-skills/paths.d.ts +39 -0
- package/lib/types/personal-skills/provider.d.ts +33 -0
- package/lib/types/personal-skills/remotes.d.ts +35 -0
- package/lib/types/personal-skills/service.d.ts +131 -0
- package/lib/types/personal-skills/skill-file.d.ts +64 -0
- package/lib/types/personal-skills/skill-format.d.ts +100 -0
- package/lib/types/personal-skills/watcher.d.ts +39 -0
- package/lib/types/profile.d.ts +13 -0
- package/lib/types/prompt-notes.d.ts +9 -0
- package/lib/types/provenance/file-preview.d.ts +47 -4
- package/lib/types/provenance/host-store.d.ts +7 -1
- package/lib/types/provenance/index.d.ts +1 -0
- package/lib/types/provenance/snapshot-store.d.ts +23 -0
- package/lib/types/qa-tools/activation-detector.d.ts +29 -0
- package/lib/types/qa-tools/activation-manager.d.ts +47 -0
- package/lib/types/qa-tools/catalog.d.ts +40 -0
- package/lib/types/qa-tools/durable-marker.d.ts +16 -0
- package/lib/types/qa-tools/index.d.ts +40 -0
- package/lib/types/qa-tools/lifecycle.d.ts +44 -0
- package/lib/types/qa-tools/types.d.ts +45 -0
- package/lib/types/secure-session.d.ts +24 -2
- package/lib/types/starters.d.ts +45 -0
- package/lib/types/types.d.ts +202 -3
- package/package.json +24 -11
- package/scripts/repair-session-events.mjs +260 -0
- package/lib/client/components/QaProfile.js +0 -64
- package/lib/client/components/QaProfile.js.map +0 -1
- package/lib/types/client/components/QaProfile.d.ts +0 -25
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { existingQaUserWorkspace, prepareQaUserWorkspace, QA_USER_WORKSPACES_DIRECTORY, } from "../user-workspace.js";
|
|
5
|
+
import { QaPersonalSkillError } from "./errors.js";
|
|
6
|
+
import { directoryExists, ensureSkillRoots, listSkillDirectories, resolveSkillRoots, skillDirectory, skillFilePath, } from "./paths.js";
|
|
7
|
+
import { normalizeAllowedTools, parseSkillFile, serializeSkillFile, skillFileBody, skillFileBytes, skillNameProblem, validateSkillDraft, QA_SKILL_DESCRIPTION_MAX, QA_SKILL_FILE_MAX_BYTES, QA_SKILL_MAX_TOOLS, QA_SKILL_WHEN_TO_USE_MAX, } from "./skill-file.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resource entries a skill may carry. v1 never writes them; anything else
|
|
10
|
+
* found beside `SKILL.md` earns a warning so the user knows the editor is not
|
|
11
|
+
* managing it.
|
|
12
|
+
*/
|
|
13
|
+
const RECOGNIZED_RESOURCES = new Set(["references", "assets", "scripts"]);
|
|
14
|
+
/**
|
|
15
|
+
* Personal Skills over one account's own directory.
|
|
16
|
+
*
|
|
17
|
+
* The service is the only writer: it derives every path from the account id,
|
|
18
|
+
* validates a draft before touching the disk, writes `SKILL.md` through a
|
|
19
|
+
* temporary file and a rename, and keeps a removed skill recoverable in the
|
|
20
|
+
* trash beside its skills root. Reading is deliberately permissive — a skill
|
|
21
|
+
* whose file the parser rejects still returns a document carrying the
|
|
22
|
+
* diagnostic that says why, so the editor can repair it.
|
|
23
|
+
*/
|
|
24
|
+
export class QaPersonalSkills {
|
|
25
|
+
ctx;
|
|
26
|
+
options;
|
|
27
|
+
constructor(ctx, options) {
|
|
28
|
+
this.ctx = ctx;
|
|
29
|
+
this.options = options;
|
|
30
|
+
}
|
|
31
|
+
/** Whether the feature is available on this deployment at all. */
|
|
32
|
+
get enabled() {
|
|
33
|
+
return this.options.getConfig().accounts.skills.enabled;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve one account's storage roots, or refuse the operation. */
|
|
36
|
+
rootsFor(context) {
|
|
37
|
+
const config = this.options.getConfig();
|
|
38
|
+
if (!config.accounts.skills.enabled) {
|
|
39
|
+
throw new QaPersonalSkillError("skills-disabled", "personal skills are not enabled on this deployment");
|
|
40
|
+
}
|
|
41
|
+
const workspacePath = this.options.workspacePath();
|
|
42
|
+
if (workspacePath === undefined) {
|
|
43
|
+
throw new QaPersonalSkillError("workspace-unavailable", "the configured QA workspace is not registered");
|
|
44
|
+
}
|
|
45
|
+
let personalRoot;
|
|
46
|
+
try {
|
|
47
|
+
// `prepare` tolerates a missing account directory: reading one's own
|
|
48
|
+
// skills must not require an unrelated session to exist first.
|
|
49
|
+
personalRoot = prepareQaUserWorkspace(workspacePath, context.userId);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new QaPersonalSkillError("workspace-unavailable", error instanceof Error ? error.message : String(error));
|
|
53
|
+
}
|
|
54
|
+
return resolveSkillRoots(personalRoot, config.accounts.skills.relativeRoot);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the storage roots a session cwd names. This is the discovery
|
|
58
|
+
* provider's entry point and takes no account token: the cwd itself is the
|
|
59
|
+
* boundary, validated as a provisioned `<workspace>/.qa-users/<uuid>`
|
|
60
|
+
* directory, so no arbitrary directory can name another account's storage.
|
|
61
|
+
*/
|
|
62
|
+
rootsForCwd(cwd, relativeRoot) {
|
|
63
|
+
const personalRoot = qaUserWorkspaceFromCwd(cwd);
|
|
64
|
+
if (personalRoot === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
try {
|
|
67
|
+
return resolveSkillRoots(personalRoot, relativeRoot);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Every skill of one account, sorted by directory name. */
|
|
74
|
+
list(context) {
|
|
75
|
+
const roots = this.rootsFor(context);
|
|
76
|
+
const available = this.availableTools();
|
|
77
|
+
return listSkillDirectories(roots).map((directoryName) => this.summarize(this.load(roots, directoryName), available));
|
|
78
|
+
}
|
|
79
|
+
/** One skill with everything the editor needs. */
|
|
80
|
+
get(context, name) {
|
|
81
|
+
const roots = this.rootsFor(context);
|
|
82
|
+
const stored = this.load(roots, name);
|
|
83
|
+
if (!stored.exists) {
|
|
84
|
+
throw new QaPersonalSkillError("skill-not-found", `no skill directory named ${name}`);
|
|
85
|
+
}
|
|
86
|
+
return this.document(stored, this.availableTools());
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check one unsaved draft without touching storage: the serializer is the
|
|
90
|
+
* one a Save uses and the preserved frontmatter is the stored file's, so the
|
|
91
|
+
* preview the editor shows is byte-for-byte the file that save would write,
|
|
92
|
+
* and the diagnostics carry the operator's own limit (which the browser
|
|
93
|
+
* cannot know). `name` is the stored skill being edited, or null to create.
|
|
94
|
+
*/
|
|
95
|
+
validate(context, name, input) {
|
|
96
|
+
const config = this.options.getConfig();
|
|
97
|
+
const prepared = this.prepareWrite(input, config, false);
|
|
98
|
+
const extraFrontmatter = name === null ? {} : this.storedFrontmatter(this.rootsFor(context), name);
|
|
99
|
+
const text = serializeSkillFile({ ...prepared.draft, extraFrontmatter });
|
|
100
|
+
return {
|
|
101
|
+
preview: text,
|
|
102
|
+
diagnostics: validateSkillDraft({
|
|
103
|
+
...prepared.draft,
|
|
104
|
+
sizeBytes: skillFileBytes(text),
|
|
105
|
+
maxBytes: config.accounts.skills.maxSkillBytes,
|
|
106
|
+
availableTools: [...this.availableTools()],
|
|
107
|
+
rejectedTools: prepared.rejectedTools,
|
|
108
|
+
extraFieldNames: Object.keys(extraFrontmatter),
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Whether one account stores a skill under this name. */
|
|
113
|
+
has(context, name) {
|
|
114
|
+
try {
|
|
115
|
+
const roots = this.rootsFor(context);
|
|
116
|
+
return directoryExists(skillDirectory(roots, name));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Create one skill directory and its `SKILL.md`. */
|
|
123
|
+
create(context, input) {
|
|
124
|
+
const roots = this.rootsFor(context);
|
|
125
|
+
const prepared = this.prepareWrite(input, this.options.getConfig(), true);
|
|
126
|
+
ensureSkillRoots(roots);
|
|
127
|
+
const directory = skillDirectory(roots, prepared.name);
|
|
128
|
+
if (directoryExists(directory)) {
|
|
129
|
+
throw new QaPersonalSkillError("skill-exists", `a skill named ${prepared.name} already exists`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
133
|
+
this.writeSkillFile(skillFilePath(roots, prepared.name), prepared.text);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
rmSync(directory, { recursive: true, force: true });
|
|
137
|
+
throw this.storageError(error);
|
|
138
|
+
}
|
|
139
|
+
this.refreshCatalog("skill.create", context, prepared.name);
|
|
140
|
+
return this.get(context, prepared.name);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Replace one skill's file, preserving the frontmatter fields this editor
|
|
144
|
+
* does not own and moving the whole directory when the name changed.
|
|
145
|
+
*/
|
|
146
|
+
update(context, name, input) {
|
|
147
|
+
const roots = this.rootsFor(context);
|
|
148
|
+
const prepared = this.prepareWrite(input, this.options.getConfig(), true);
|
|
149
|
+
const current = this.load(roots, name);
|
|
150
|
+
if (!current.exists) {
|
|
151
|
+
throw new QaPersonalSkillError("skill-not-found", `no skill directory named ${name}`);
|
|
152
|
+
}
|
|
153
|
+
this.assertRevision(name, input.expectedRevision, current.revision);
|
|
154
|
+
const text = serializeSkillFile({
|
|
155
|
+
...prepared.draft,
|
|
156
|
+
// Foreign frontmatter is read from the file, not from the browser: the
|
|
157
|
+
// stored copy is the authority on what a Save must not lose.
|
|
158
|
+
extraFrontmatter: current.contents?.extraFrontmatter ?? {},
|
|
159
|
+
});
|
|
160
|
+
const target = skillDirectory(roots, prepared.name);
|
|
161
|
+
if (current.directory === target) {
|
|
162
|
+
this.writeSkillFile(current.filePath, text);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.renameSkillDirectory(current.directory, target, text, prepared.name);
|
|
166
|
+
}
|
|
167
|
+
this.refreshCatalog("skill.update", context, prepared.name);
|
|
168
|
+
return this.get(context, prepared.name);
|
|
169
|
+
}
|
|
170
|
+
/** Move one skill directory into the trash beside its skills root. */
|
|
171
|
+
remove(context, name, expectedRevision) {
|
|
172
|
+
const roots = this.rootsFor(context);
|
|
173
|
+
const current = this.load(roots, name);
|
|
174
|
+
if (!current.exists) {
|
|
175
|
+
throw new QaPersonalSkillError("skill-not-found", `no skill directory named ${name}`);
|
|
176
|
+
}
|
|
177
|
+
this.assertRevision(name, expectedRevision, current.revision);
|
|
178
|
+
try {
|
|
179
|
+
mkdirSync(roots.trash, { recursive: true, mode: 0o700 });
|
|
180
|
+
renameSync(current.directory, this.trashTarget(roots.trash, name));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw this.storageError(error);
|
|
184
|
+
}
|
|
185
|
+
this.refreshCatalog("skill.delete", context, name);
|
|
186
|
+
return { name, trashed: true };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The tool catalog the picker offers. Availability is the QA session's own
|
|
190
|
+
* scope — the deployment's reviewed allow-list — never a suggestion of what
|
|
191
|
+
* a skill may grant: a declared tool the scope excludes stays listed and
|
|
192
|
+
* stays unavailable.
|
|
193
|
+
*/
|
|
194
|
+
tools(context) {
|
|
195
|
+
const available = this.availableTools();
|
|
196
|
+
const described = new Map();
|
|
197
|
+
for (const schema of this.ctx.tools.schemas()) {
|
|
198
|
+
described.set(schema.name, schema.description);
|
|
199
|
+
}
|
|
200
|
+
const names = new Set(described.keys());
|
|
201
|
+
for (const name of available)
|
|
202
|
+
names.add(name);
|
|
203
|
+
// Declared names come last: an imported tool the registry never had has to
|
|
204
|
+
// stay removable from the skill that declares it.
|
|
205
|
+
for (const name of this.declaredTools(context))
|
|
206
|
+
names.add(name);
|
|
207
|
+
const descriptors = [...names].map((name) => ({
|
|
208
|
+
name,
|
|
209
|
+
description: described.get(name) ?? "",
|
|
210
|
+
available: available.has(name),
|
|
211
|
+
}));
|
|
212
|
+
descriptors.sort((left, right) => {
|
|
213
|
+
if (left.available !== right.available)
|
|
214
|
+
return left.available ? -1 : 1;
|
|
215
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
216
|
+
});
|
|
217
|
+
return descriptors;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The skills one session cwd exposes to DSH. A file the parser rejects, or
|
|
221
|
+
* one whose name disagrees with its directory, is reported in the editor and
|
|
222
|
+
* never handed to the model as a half-valid skill.
|
|
223
|
+
*/
|
|
224
|
+
discover(cwd) {
|
|
225
|
+
const config = this.options.getConfig();
|
|
226
|
+
if (!config.accounts.skills.enabled)
|
|
227
|
+
return [];
|
|
228
|
+
const roots = this.rootsForCwd(cwd, config.accounts.skills.relativeRoot);
|
|
229
|
+
if (roots === undefined)
|
|
230
|
+
return [];
|
|
231
|
+
this.observe(roots.skills);
|
|
232
|
+
return listSkillDirectories(roots)
|
|
233
|
+
.map((directoryName) => this.load(roots, directoryName))
|
|
234
|
+
.filter((skill) => this.isDiscoverable(skill));
|
|
235
|
+
}
|
|
236
|
+
/** Re-read one skill of a session cwd, re-checking the same boundary. */
|
|
237
|
+
read(cwd, directoryName) {
|
|
238
|
+
const config = this.options.getConfig();
|
|
239
|
+
if (!config.accounts.skills.enabled)
|
|
240
|
+
return undefined;
|
|
241
|
+
const roots = this.rootsForCwd(cwd, config.accounts.skills.relativeRoot);
|
|
242
|
+
if (roots === undefined)
|
|
243
|
+
return undefined;
|
|
244
|
+
let directory;
|
|
245
|
+
try {
|
|
246
|
+
directory = skillDirectory(roots, directoryName);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
if (!directoryExists(directory))
|
|
252
|
+
return undefined;
|
|
253
|
+
const stored = this.load(roots, directoryName);
|
|
254
|
+
return this.isDiscoverable(stored) ? stored : undefined;
|
|
255
|
+
}
|
|
256
|
+
observe(root) {
|
|
257
|
+
this.options.onRootDiscovered?.(root);
|
|
258
|
+
}
|
|
259
|
+
isDiscoverable(skill) {
|
|
260
|
+
const contents = skill.contents;
|
|
261
|
+
if (!skill.exists || contents === undefined)
|
|
262
|
+
return false;
|
|
263
|
+
return contents.name === skill.directoryName;
|
|
264
|
+
}
|
|
265
|
+
availableTools() {
|
|
266
|
+
return new Set(this.options.getConfig().lockdown.toolPolicy.allow);
|
|
267
|
+
}
|
|
268
|
+
declaredTools(context) {
|
|
269
|
+
const names = new Set();
|
|
270
|
+
let roots;
|
|
271
|
+
try {
|
|
272
|
+
roots = this.rootsFor(context);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
for (const directoryName of listSkillDirectories(roots)) {
|
|
278
|
+
const stored = this.load(roots, directoryName);
|
|
279
|
+
for (const tool of stored.contents?.allowedTools ?? [])
|
|
280
|
+
names.add(tool);
|
|
281
|
+
}
|
|
282
|
+
return [...names];
|
|
283
|
+
}
|
|
284
|
+
/** The foreign frontmatter a save must keep, read from the stored file. */
|
|
285
|
+
storedFrontmatter(roots, name) {
|
|
286
|
+
return this.load(roots, name).contents?.extraFrontmatter ?? {};
|
|
287
|
+
}
|
|
288
|
+
/** Read one skill directory, tolerating every failure the editor must see. */
|
|
289
|
+
load(roots, directoryName) {
|
|
290
|
+
const absent = {
|
|
291
|
+
exists: false,
|
|
292
|
+
directoryName,
|
|
293
|
+
directory: path.join(roots.skills, directoryName),
|
|
294
|
+
filePath: path.join(roots.skills, directoryName, "SKILL.md"),
|
|
295
|
+
raw: "",
|
|
296
|
+
revision: "",
|
|
297
|
+
updatedAt: null,
|
|
298
|
+
contents: undefined,
|
|
299
|
+
diagnostics: [],
|
|
300
|
+
resourceCount: 0,
|
|
301
|
+
};
|
|
302
|
+
let directory;
|
|
303
|
+
try {
|
|
304
|
+
directory = skillDirectory(roots, directoryName);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return absent;
|
|
308
|
+
}
|
|
309
|
+
if (!directoryExists(directory))
|
|
310
|
+
return absent;
|
|
311
|
+
const filePath = path.join(directory, "SKILL.md");
|
|
312
|
+
const maxBytes = this.options.getConfig().accounts.skills.maxSkillBytes;
|
|
313
|
+
const read = readSkillText(filePath, Math.max(maxBytes, QA_SKILL_FILE_MAX_BYTES));
|
|
314
|
+
const resources = this.resources(directory);
|
|
315
|
+
const base = {
|
|
316
|
+
exists: true,
|
|
317
|
+
directoryName,
|
|
318
|
+
directory,
|
|
319
|
+
filePath,
|
|
320
|
+
resourceCount: resources.count,
|
|
321
|
+
};
|
|
322
|
+
if (read === undefined) {
|
|
323
|
+
return {
|
|
324
|
+
...base,
|
|
325
|
+
raw: "",
|
|
326
|
+
revision: "",
|
|
327
|
+
updatedAt: null,
|
|
328
|
+
contents: undefined,
|
|
329
|
+
diagnostics: [
|
|
330
|
+
...resources.diagnostics,
|
|
331
|
+
{
|
|
332
|
+
code: "skill-file-missing",
|
|
333
|
+
severity: "error",
|
|
334
|
+
field: null,
|
|
335
|
+
detail: "SKILL.md",
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const parsed = parseSkillFile(read.text);
|
|
341
|
+
const diagnostics = [
|
|
342
|
+
...resources.diagnostics,
|
|
343
|
+
...(parsed.ok ? parsed.value.warnings : []),
|
|
344
|
+
];
|
|
345
|
+
if (!parsed.ok) {
|
|
346
|
+
diagnostics.push({
|
|
347
|
+
code: parsed.code,
|
|
348
|
+
severity: "error",
|
|
349
|
+
field: null,
|
|
350
|
+
detail: parsed.detail,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
diagnostics.push(...validateSkillDraft({
|
|
355
|
+
name: parsed.value.name,
|
|
356
|
+
description: parsed.value.description,
|
|
357
|
+
whenToUse: parsed.value.whenToUse,
|
|
358
|
+
modelInvocable: parsed.value.modelInvocable,
|
|
359
|
+
userInvocable: parsed.value.userInvocable,
|
|
360
|
+
allowedTools: parsed.value.allowedTools,
|
|
361
|
+
sizeBytes: skillFileBytes(read.text),
|
|
362
|
+
maxBytes,
|
|
363
|
+
availableTools: [...this.availableTools()],
|
|
364
|
+
}));
|
|
365
|
+
if (parsed.value.name !== directoryName) {
|
|
366
|
+
diagnostics.push({
|
|
367
|
+
code: "name-mismatch",
|
|
368
|
+
severity: "warning",
|
|
369
|
+
field: "name",
|
|
370
|
+
detail: parsed.value.name,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
...base,
|
|
376
|
+
raw: read.text,
|
|
377
|
+
revision: hashText(read.text),
|
|
378
|
+
updatedAt: read.updatedAt,
|
|
379
|
+
contents: parsed.ok ? parsed.value : undefined,
|
|
380
|
+
diagnostics,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
resources(directory) {
|
|
384
|
+
let entries;
|
|
385
|
+
try {
|
|
386
|
+
entries = readdirSync(directory).filter((entry) => entry !== "SKILL.md");
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return { count: 0, diagnostics: [] };
|
|
390
|
+
}
|
|
391
|
+
const diagnostics = [];
|
|
392
|
+
for (const entry of entries) {
|
|
393
|
+
if (RECOGNIZED_RESOURCES.has(entry))
|
|
394
|
+
continue;
|
|
395
|
+
diagnostics.push({
|
|
396
|
+
code: "resource-unsupported",
|
|
397
|
+
severity: "warning",
|
|
398
|
+
field: null,
|
|
399
|
+
detail: entry,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return { count: entries.length, diagnostics };
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Normalize one editor draft into the file to write. `strict` is the
|
|
406
|
+
* difference between a save and a check: a save refuses a draft the Host
|
|
407
|
+
* will not store, while a check hands the same problems back as diagnostics,
|
|
408
|
+
* because that is what the editor asked for.
|
|
409
|
+
*/
|
|
410
|
+
prepareWrite(input, config, strict) {
|
|
411
|
+
const name = typeof input.name === "string" ? input.name.trim() : "";
|
|
412
|
+
const description = typeof input.description === "string" ? input.description.trim() : "";
|
|
413
|
+
const normalized = normalizeAllowedTools(input.allowedTools) ?? {
|
|
414
|
+
tools: [],
|
|
415
|
+
rejected: [],
|
|
416
|
+
};
|
|
417
|
+
const whenToUse = typeof input.whenToUse === "string" && input.whenToUse.trim() !== ""
|
|
418
|
+
? input.whenToUse.trim()
|
|
419
|
+
: null;
|
|
420
|
+
const draft = {
|
|
421
|
+
name,
|
|
422
|
+
description,
|
|
423
|
+
whenToUse,
|
|
424
|
+
modelInvocable: input.modelInvocable !== false,
|
|
425
|
+
userInvocable: input.userInvocable !== false,
|
|
426
|
+
allowedTools: normalized.tools,
|
|
427
|
+
extraFrontmatter: {},
|
|
428
|
+
body: typeof input.body === "string" ? input.body : "",
|
|
429
|
+
};
|
|
430
|
+
const text = serializeSkillFile(draft);
|
|
431
|
+
if (!strict) {
|
|
432
|
+
return { name, text, draft, rejectedTools: normalized.rejected };
|
|
433
|
+
}
|
|
434
|
+
if (skillNameProblem(name) !== null) {
|
|
435
|
+
throw new QaPersonalSkillError("skill-name-invalid", "the skill name must be lowercase kebab-case");
|
|
436
|
+
}
|
|
437
|
+
if (description === "") {
|
|
438
|
+
throw new QaPersonalSkillError("skill-invalid", "the skill description must not be empty");
|
|
439
|
+
}
|
|
440
|
+
if (normalized.rejected.length > 0) {
|
|
441
|
+
throw new QaPersonalSkillError("skill-invalid", `unusable tool name(s): ${normalized.rejected.join(", ")}`);
|
|
442
|
+
}
|
|
443
|
+
if (normalized.tools.length > QA_SKILL_MAX_TOOLS) {
|
|
444
|
+
throw new QaPersonalSkillError("skill-invalid", `at most ${QA_SKILL_MAX_TOOLS} tools may be declared`);
|
|
445
|
+
}
|
|
446
|
+
const blocking = validateSkillDraft({
|
|
447
|
+
...draft,
|
|
448
|
+
sizeBytes: skillFileBytes(text),
|
|
449
|
+
maxBytes: config.accounts.skills.maxSkillBytes,
|
|
450
|
+
}).find((entry) => entry.severity === "error");
|
|
451
|
+
if (blocking !== undefined) {
|
|
452
|
+
this.options.logger.warn("skill.validation-failed", {
|
|
453
|
+
code: blocking.code,
|
|
454
|
+
detail: blocking.detail,
|
|
455
|
+
name,
|
|
456
|
+
});
|
|
457
|
+
throw new QaPersonalSkillError("skill-invalid", `the skill is not savable: ${blocking.code}`);
|
|
458
|
+
}
|
|
459
|
+
if (description.length > QA_SKILL_DESCRIPTION_MAX) {
|
|
460
|
+
this.options.logger.warn("skill.validation-warning", {
|
|
461
|
+
code: "description-too-long",
|
|
462
|
+
name,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (whenToUse !== null && whenToUse.length > QA_SKILL_WHEN_TO_USE_MAX) {
|
|
466
|
+
this.options.logger.warn("skill.validation-warning", {
|
|
467
|
+
code: "when-to-use-too-long",
|
|
468
|
+
name,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return { name, text, draft, rejectedTools: [] };
|
|
472
|
+
}
|
|
473
|
+
/** Move a renamed skill's whole directory, resources included. */
|
|
474
|
+
renameSkillDirectory(from, to, text, name) {
|
|
475
|
+
if (directoryExists(to)) {
|
|
476
|
+
throw new QaPersonalSkillError("skill-exists", `a skill named ${name} already exists`);
|
|
477
|
+
}
|
|
478
|
+
const moved = [];
|
|
479
|
+
try {
|
|
480
|
+
mkdirSync(to, { recursive: true, mode: 0o700 });
|
|
481
|
+
for (const entry of readdirSync(from)) {
|
|
482
|
+
renameSync(path.join(from, entry), path.join(to, entry));
|
|
483
|
+
moved.push(entry);
|
|
484
|
+
}
|
|
485
|
+
this.writeSkillFile(path.join(to, "SKILL.md"), text);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
// Put every entry back before reporting: a half-moved skill directory is
|
|
489
|
+
// worse than a refused save.
|
|
490
|
+
for (const entry of moved) {
|
|
491
|
+
try {
|
|
492
|
+
renameSync(path.join(to, entry), path.join(from, entry));
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// The original move already failed; the skill stays where it is and
|
|
496
|
+
// the operator gets the error below.
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
rmSync(to, { recursive: true, force: true });
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// A leftover empty directory is harmless.
|
|
504
|
+
}
|
|
505
|
+
throw this.storageError(error);
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
rmSync(from, { recursive: true, force: true });
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// The contents already moved; an empty directory left behind is not
|
|
512
|
+
// worth failing a save over.
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/** Write one file through a temporary sibling and a rename. */
|
|
516
|
+
writeSkillFile(filePath, text) {
|
|
517
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
518
|
+
try {
|
|
519
|
+
writeFileSync(temporary, text, "utf8");
|
|
520
|
+
renameSync(temporary, filePath);
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
try {
|
|
524
|
+
rmSync(temporary, { force: true });
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
// A leftover temporary file never shadows the real one.
|
|
528
|
+
}
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
trashTarget(trash, name) {
|
|
533
|
+
const stamp = new Date()
|
|
534
|
+
.toISOString()
|
|
535
|
+
.replace(/[-:]/gu, "")
|
|
536
|
+
.replace(/\.\d+Zu?$/u, "Z");
|
|
537
|
+
let candidate = path.join(trash, `${name}-${stamp}`);
|
|
538
|
+
for (let attempt = 2; attempt < 100; attempt += 1) {
|
|
539
|
+
if (!pathExists(candidate))
|
|
540
|
+
break;
|
|
541
|
+
candidate = path.join(trash, `${name}-${stamp}-${attempt}`);
|
|
542
|
+
}
|
|
543
|
+
return candidate;
|
|
544
|
+
}
|
|
545
|
+
assertRevision(name, expected, current) {
|
|
546
|
+
if (expected !== null && expected !== current) {
|
|
547
|
+
throw new QaPersonalSkillError("skill-conflict", `skill ${name} changed after it was read`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
summarize(stored, available) {
|
|
551
|
+
const contents = stored.contents;
|
|
552
|
+
return {
|
|
553
|
+
name: stored.directoryName,
|
|
554
|
+
description: contents?.description ?? "",
|
|
555
|
+
whenToUse: contents?.whenToUse ?? null,
|
|
556
|
+
modelInvocable: contents?.modelInvocable ?? true,
|
|
557
|
+
userInvocable: contents?.userInvocable ?? true,
|
|
558
|
+
allowedTools: contents?.allowedTools ?? [],
|
|
559
|
+
unavailableTools: (contents?.allowedTools ?? []).filter((tool) => !available.has(tool)),
|
|
560
|
+
resourceCount: stored.resourceCount,
|
|
561
|
+
valid: !stored.diagnostics.some((entry) => entry.severity === "error"),
|
|
562
|
+
diagnostics: stored.diagnostics,
|
|
563
|
+
updatedAt: stored.updatedAt,
|
|
564
|
+
revision: stored.revision,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
document(stored, available) {
|
|
568
|
+
const contents = stored.contents;
|
|
569
|
+
return {
|
|
570
|
+
...this.summarize(stored, available),
|
|
571
|
+
body: contents?.body ?? skillFileBody(stored.raw),
|
|
572
|
+
extraFrontmatter: contents?.extraFrontmatter ?? {},
|
|
573
|
+
sourcePath: stored.filePath,
|
|
574
|
+
preview: contents === undefined ? stored.raw : serializeSkillFile(contents),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
storageError(error) {
|
|
578
|
+
if (error instanceof QaPersonalSkillError)
|
|
579
|
+
return error;
|
|
580
|
+
return new QaPersonalSkillError("storage-unavailable", error instanceof Error ? error.message : String(error));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Publish one write: audit it, then refresh the DSH catalog. The two are
|
|
584
|
+
* separate stages on purpose — a stored skill whose catalog refresh failed
|
|
585
|
+
* is still stored, and the deployment needs to see which half broke.
|
|
586
|
+
*/
|
|
587
|
+
refreshCatalog(action, context, name) {
|
|
588
|
+
// The account id is hashed: an audit line names no personal data.
|
|
589
|
+
this.options.logger.info(action, {
|
|
590
|
+
user: createHash("sha256")
|
|
591
|
+
.update(context.userId)
|
|
592
|
+
.digest("hex")
|
|
593
|
+
.slice(0, 12),
|
|
594
|
+
skill: name,
|
|
595
|
+
});
|
|
596
|
+
try {
|
|
597
|
+
this.options.invalidate();
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
this.options.logger.error("skill.provider.invalidate-failed", {
|
|
601
|
+
skill: name,
|
|
602
|
+
message: error instanceof Error ? error.message : String(error),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The one place the feature defines what an account-scoped cwd looks like:
|
|
609
|
+
* `<registered workspace>/.qa-users/<uuid>`. Anything else names no personal
|
|
610
|
+
* root, which is what keeps the discovery provider from reading arbitrary
|
|
611
|
+
* directories a session happens to be pinned to.
|
|
612
|
+
*/
|
|
613
|
+
function qaUserWorkspaceFromCwd(cwd) {
|
|
614
|
+
const segments = path.resolve(cwd).split(/[/\\]+/u);
|
|
615
|
+
const userId = segments.at(-1);
|
|
616
|
+
const usersDirectory = segments.at(-2);
|
|
617
|
+
if (userId === undefined || usersDirectory === undefined)
|
|
618
|
+
return undefined;
|
|
619
|
+
const matches = process.platform === "win32"
|
|
620
|
+
? usersDirectory.toLowerCase() === QA_USER_WORKSPACES_DIRECTORY
|
|
621
|
+
: usersDirectory === QA_USER_WORKSPACES_DIRECTORY;
|
|
622
|
+
if (!matches)
|
|
623
|
+
return undefined;
|
|
624
|
+
try {
|
|
625
|
+
return existingQaUserWorkspace(path.resolve(cwd, "..", ".."), userId);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function hashText(text) {
|
|
632
|
+
return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
633
|
+
}
|
|
634
|
+
function pathExists(input) {
|
|
635
|
+
try {
|
|
636
|
+
statSync(input);
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Read one skill file, refusing to hold an unbounded file in memory: an
|
|
645
|
+
* oversized `SKILL.md` is read only up to the ceiling and reported by the
|
|
646
|
+
* validator, which keeps the editor usable instead of the Host unresponsive.
|
|
647
|
+
* The file is lstat-ed, so a symlinked `SKILL.md` is treated as absent rather
|
|
648
|
+
* than followed out of the account's directory.
|
|
649
|
+
*/
|
|
650
|
+
function readSkillText(filePath, maxBytes) {
|
|
651
|
+
let size;
|
|
652
|
+
let updatedAt;
|
|
653
|
+
try {
|
|
654
|
+
const stat = lstatSync(filePath);
|
|
655
|
+
if (!stat.isFile())
|
|
656
|
+
return undefined;
|
|
657
|
+
size = stat.size;
|
|
658
|
+
updatedAt = stat.mtime.toISOString();
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
if (size <= maxBytes) {
|
|
664
|
+
try {
|
|
665
|
+
return { text: readFileSync(filePath, "utf8"), updatedAt };
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const handle = openSync(filePath, "r");
|
|
673
|
+
try {
|
|
674
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
675
|
+
const read = readSync(handle, buffer, 0, maxBytes, 0);
|
|
676
|
+
return { text: buffer.subarray(0, read).toString("utf8"), updatedAt };
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
closeSync(handle);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
//# sourceMappingURL=service.js.map
|