@prismer/runtime 2.0.8 → 2.2.55

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +3430 -0
  2. package/README.md +34 -12
  3. package/apc/skills/FIELD-DICTIONARY.md +111 -0
  4. package/apc/skills/bug-reproduce/SKILL.md +150 -0
  5. package/apc/skills/bug-reproduce/skill.json +96 -0
  6. package/apc/skills/code-review/SKILL.md +198 -0
  7. package/apc/skills/code-review/skill.json +124 -0
  8. package/apc/skills/design-review/SKILL.md +122 -0
  9. package/apc/skills/design-review/skill.json +88 -0
  10. package/apc/skills/doc-sync/SKILL.md +168 -0
  11. package/apc/skills/doc-sync/skill.json +81 -0
  12. package/apc/skills/env-doctor/SKILL.md +194 -0
  13. package/apc/skills/env-doctor/skill.json +209 -0
  14. package/apc/skills/git-ops/SKILL.md +189 -0
  15. package/apc/skills/git-ops/skill.json +94 -0
  16. package/apc/skills/impact-trace/SKILL.md +168 -0
  17. package/apc/skills/impact-trace/skill.json +104 -0
  18. package/apc/skills/observability/SKILL.md +195 -0
  19. package/apc/skills/observability/skill.json +116 -0
  20. package/apc/skills/release-db-config-sync/SKILL.md +186 -0
  21. package/apc/skills/release-db-config-sync/skill.json +109 -0
  22. package/apc/skills/release-ota-promote/SKILL.md +195 -0
  23. package/apc/skills/release-ota-promote/skill.json +176 -0
  24. package/apc/skills/release-preflight/SKILL.md +174 -0
  25. package/apc/skills/release-preflight/skill.json +175 -0
  26. package/apc/skills/release-rollback/SKILL.md +214 -0
  27. package/apc/skills/release-rollback/skill.json +230 -0
  28. package/apc/skills/release-tag/SKILL.md +194 -0
  29. package/apc/skills/release-tag/skill.json +94 -0
  30. package/apc/skills/releasing-prod/SKILL.md +49 -0
  31. package/apc/skills/releasing-test/SKILL.md +135 -0
  32. package/apc/skills/sdk-release/SKILL.md +200 -0
  33. package/apc/skills/spec-intake/SKILL.md +169 -0
  34. package/apc/skills/spec-intake/skill.json +93 -0
  35. package/apc/skills/test-result-feedback/SKILL.md +239 -0
  36. package/apc/skills/test-result-feedback/skill.json +193 -0
  37. package/apc/skills/test-runner/SKILL.md +169 -0
  38. package/apc/skills/test-runner/skill.json +103 -0
  39. package/apc/skills/ui-align/SKILL.md +209 -0
  40. package/apc/skills/ui-align/skill.json +114 -0
  41. package/apc/skills/ui-canvas/SKILL.md +148 -0
  42. package/apc/skills/ui-canvas/skill.json +127 -0
  43. package/built-in-skills/agent-coordination/SKILL.md +59 -37
  44. package/built-in-skills/agent-meta/SKILL.md +1 -0
  45. package/built-in-skills/assets/SKILL.md +8 -6
  46. package/built-in-skills/browser-use/SKILL.md +93 -0
  47. package/built-in-skills/canvas-design/SKILL.md +1 -0
  48. package/built-in-skills/claim-agent-ownership/SKILL.md +3 -2
  49. package/built-in-skills/claude-api/SKILL.md +1 -0
  50. package/built-in-skills/codebase-design/DEEPENING.md +37 -0
  51. package/built-in-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/built-in-skills/codebase-design/LICENSE +21 -0
  53. package/built-in-skills/codebase-design/SKILL.md +116 -0
  54. package/built-in-skills/conversation-compaction/SKILL.md +114 -0
  55. package/built-in-skills/council-creator/SKILL.md +426 -0
  56. package/built-in-skills/diagnosing-bugs/LICENSE +21 -0
  57. package/built-in-skills/diagnosing-bugs/SKILL.md +136 -0
  58. package/built-in-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  59. package/built-in-skills/doc-coauthoring/SKILL.md +1 -0
  60. package/built-in-skills/document-generation/SKILL.md +105 -0
  61. package/built-in-skills/domain-modeling/ADR-FORMAT.md +47 -0
  62. package/built-in-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  63. package/built-in-skills/domain-modeling/LICENSE +21 -0
  64. package/built-in-skills/domain-modeling/SKILL.md +76 -0
  65. package/built-in-skills/frontend-design/SKILL.md +1 -0
  66. package/built-in-skills/human-approval/SKILL.md +17 -2
  67. package/built-in-skills/image-generate/SKILL.md +103 -302
  68. package/built-in-skills/image-generate/scripts/generate-and-deliver.mjs +289 -0
  69. package/built-in-skills/ingest/SKILL.md +13 -45
  70. package/built-in-skills/internal-comms/SKILL.md +1 -0
  71. package/built-in-skills/liteparse/SKILL.md +130 -110
  72. package/built-in-skills/mcp-builder/SKILL.md +1 -0
  73. package/built-in-skills/memory/SKILL.md +420 -55
  74. package/built-in-skills/memory-dream/SKILL.md +339 -0
  75. package/built-in-skills/office-artifacts/SKILL.md +17 -4
  76. package/built-in-skills/okr/SKILL.md +154 -0
  77. package/built-in-skills/persona/SKILL.md +81 -0
  78. package/built-in-skills/persona-generator/SKILL.md +296 -0
  79. package/built-in-skills/pkf-svg/SKILL.md +253 -0
  80. package/built-in-skills/pkf-writing/SKILL.md +236 -0
  81. package/built-in-skills/prismer-im-collab/SKILL.md +26 -6
  82. package/built-in-skills/proactivity/SKILL.md +84 -0
  83. package/built-in-skills/remotion/SKILL.md +431 -0
  84. package/built-in-skills/role-builder/SKILL.md +203 -0
  85. package/built-in-skills/role-builder/scripts/author-role.mjs +334 -0
  86. package/built-in-skills/role-builder/scripts/ingest-role.mjs +223 -0
  87. package/built-in-skills/role-builder/scripts/instantiate-and-run.mjs +290 -0
  88. package/built-in-skills/role-builder/scripts/operation-harness.mjs +267 -0
  89. package/built-in-skills/skill-authoring/SKILL.md +110 -100
  90. package/built-in-skills/skill-authoring/skill.json +3 -3
  91. package/built-in-skills/skill-builder/SKILL.md +171 -0
  92. package/built-in-skills/skill-builder/scripts/ingest.mjs +265 -0
  93. package/built-in-skills/skill-creator/SKILL.md +165 -423
  94. package/built-in-skills/skill-creator/references/external-library-import.md +110 -0
  95. package/built-in-skills/skill-creator/scripts/import-library.mjs +475 -0
  96. package/built-in-skills/slack-gif-creator/SKILL.md +20 -0
  97. package/built-in-skills/tasks/SKILL.md +38 -23
  98. package/built-in-skills/tdd/LICENSE +21 -0
  99. package/built-in-skills/tdd/SKILL.md +110 -0
  100. package/built-in-skills/tdd/mocking.md +59 -0
  101. package/built-in-skills/tdd/refactoring.md +10 -0
  102. package/built-in-skills/tdd/tests.md +61 -0
  103. package/built-in-skills/team/SKILL.md +2 -1
  104. package/built-in-skills/web-artifacts-builder/SKILL.md +1 -0
  105. package/built-in-skills/webapp-testing/SKILL.md +1 -0
  106. package/built-in-skills/wechat-pay/SKILL.md +59 -0
  107. package/dist/cli.cjs +71872 -19960
  108. package/dist/cli.js +71803 -19846
  109. package/dist/index.cjs +72010 -19966
  110. package/dist/index.d.cts +4258 -712
  111. package/dist/index.d.ts +4258 -712
  112. package/dist/index.js +72156 -20118
  113. package/package.json +37 -6
  114. package/plugins/memory/prismer/__init__.py +1211 -0
  115. package/plugins/memory/prismer/plugin.yaml +8 -0
  116. package/plugins/memory/prismer/tool-schemas.generated.json +249 -0
  117. package/plugins/tools/prismer-recall/__init__.py +282 -0
  118. package/plugins/tools/prismer-recall/plugin.yaml +15 -0
  119. package/built-in-skills/memory-curation/SKILL.md +0 -135
@@ -0,0 +1,110 @@
1
+ # External skill-library import
2
+
3
+ Use this workflow when the source is a repository or directory containing more
4
+ than one skill, especially when the result will be wired into a role.
5
+
6
+ ## 1. Establish the authoritative source
7
+
8
+ - Read the repository README, license, and AGENTS.md before changing anything.
9
+ - Identify mirrors/translations and import only the source tree the repository
10
+ declares authoritative. Record the excluded mirrors in the final report.
11
+ - Inventory directories containing a root `SKILL.md`; do not infer the count
12
+ from repository file totals.
13
+ - Work from a temporary copy when normalization is genuinely required. Do not
14
+ edit the cloned authority merely to satisfy the platform parser.
15
+
16
+ The Prismer bundle parser accepts ordinary YAML block scalars (`description: |`
17
+ and `description: >`). The description quality floor is language-aware, so a
18
+ concise CJK description need not be padded with filler. If validation rejects
19
+ either shape, treat it as a CLI/runtime version-skew problem before rewriting
20
+ the source.
21
+
22
+ ## 2. Run the deterministic import harness
23
+
24
+ After choosing the authoritative tree, put configuration in the runtime
25
+ environment and run one command:
26
+
27
+ ```bash
28
+ node scripts/import-library.mjs --json
29
+ ```
30
+
31
+ Required environment: `PRISMER_SKILL_LIBRARY_ROOT`. Optional:
32
+ `PRISMER_SKILL_LIBRARY_EXCLUDE` (comma/newline-separated relative trees),
33
+ `PRISMER_IMPORT_LEDGER`, `PRISMER_CLOUD_BIN`, `PRISMER_CLOUD_BASE`, and
34
+ `PRISMER_ALLOW_REMOTE_WRITE=1` for an explicitly confirmed non-local target.
35
+ Authentication remains in the Cloud CLI's injected environment/config; never
36
+ put a token in arguments or source files.
37
+
38
+ Use `--preflight-only` when only inventory/validation is requested. The harness:
39
+
40
+ 1. discovers root `SKILL.md` bundles under the selected tree;
41
+ 2. validates the entire set locally before the first create;
42
+ 3. rejects duplicate requested slugs, which usually means both an authority and
43
+ its mirror were selected;
44
+ 4. creates only unverified ledger rows and persists every server-returned slug;
45
+ 5. verifies private content through owner-scoped `cloud skill show --content`;
46
+ 6. emits `requiredSkills[]` from verified canonical slugs for `role-builder`.
47
+
48
+ Do not publish the first item as an exploratory probe. A probe creates durable
49
+ catalog state and is not a substitute for a read-only validator.
50
+
51
+ If `cloud` exists but a documented command such as `skill create` is absent,
52
+ report the installed CLI version and use the repository's current Cloud CLI
53
+ build. Do not silently jump to a raw HTTP request or a bundled ingest script;
54
+ that bypasses the exact auth/scope/readback path being verified. The portable
55
+ script is only a fallback when the CLI binary itself is unavailable.
56
+
57
+ ## 3. Recovery and lower-level diagnosis
58
+
59
+ On a transient failure, rerun the same command with the same
60
+ `PRISMER_IMPORT_LEDGER`; verified rows are not recreated. If the selected files
61
+ changed, the harness rejects the stale ledger before Cloud calls—use a new
62
+ task-bound ledger after reviewing that change.
63
+
64
+ Only diagnose a failed row with the lower-level primitives:
65
+
66
+ ```bash
67
+ cloud skill validate <dir> --json
68
+ cloud skill create <dir> --json
69
+ cloud skill mine --json
70
+ cloud skill show <returned-slug-or-id> --content --json
71
+ ```
72
+
73
+ Community slugs may be prefixed by the server; never lowercase, prefix, or
74
+ otherwise reconstruct them. A conflict alone is not proof of a complete or
75
+ correct prior import.
76
+
77
+ ## 4. Verify the private domain correctly
78
+
79
+ `cloud skill find` searches the public Marketplace and is expected not to show
80
+ workspace-private skills. Verify authored skills through:
81
+
82
+ ```bash
83
+ cloud skill mine --json
84
+ cloud skill show <canonical-slug-or-id> --content --json
85
+ ```
86
+
87
+ Every successful ledger row must round-trip. Do not create temporary catalog
88
+ objects to discover response shape; use `--json` on the real create/readback.
89
+
90
+ ## 5. Hand canonical slugs to role-builder
91
+
92
+ Generate `requiredSkills[]` only from successful ledger rows. Then use the
93
+ `role-builder` workflow:
94
+
95
+ ```bash
96
+ cloud role validate <role-dir> --json
97
+ cloud role test <role-dir> --json
98
+ cloud role create <role-dir> --json
99
+ cloud role show <returned-slug-or-id> --json
100
+ ```
101
+
102
+ `role test` is the read-only dependency proof. Do not apply the role merely to
103
+ test resolution. Applying changes a live agent profile and requires the user's
104
+ explicit target and intent; never use the currently executing agent as a probe.
105
+
106
+ ## Completion evidence
107
+
108
+ Report selected/excluded source trees, validated/created/failed counts, the
109
+ canonical slug ledger, role readback, and whether any apply was explicitly
110
+ requested. A POST count or a list of requested names is not completion evidence.
@@ -0,0 +1,475 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { dirname, relative, resolve, sep } from 'node:path';
7
+
8
+ export class LibraryImportError extends Error {
9
+ constructor(stage, code, message, exitCode = 1, details = undefined) {
10
+ super(message);
11
+ this.name = 'LibraryImportError';
12
+ this.stage = stage;
13
+ this.code = code;
14
+ this.exitCode = exitCode;
15
+ this.details = details;
16
+ }
17
+ }
18
+
19
+ const SECRET_OPTIONS = new Set(['--api-key', '--token', '--jwt', '--secret', '--password']);
20
+ const BOOLEAN_OPTIONS = new Set(['--json', '--preflight-only', '--help']);
21
+ const IGNORED_DIRECTORIES = new Set(['.git', '.prismer', 'node_modules']);
22
+ const BUNDLE_EXCLUDED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'LICENSE', 'LICENSE.txt', 'LICENSE.md']);
23
+
24
+ function optionName(arg) {
25
+ const equalsAt = typeof arg === 'string' ? arg.indexOf('=') : -1;
26
+ return equalsAt === -1 ? arg : arg.slice(0, equalsAt);
27
+ }
28
+
29
+ function isLocalTarget(hostname) {
30
+ return ['localhost', '127.0.0.1', '::1', 'host.docker.internal'].includes(hostname);
31
+ }
32
+
33
+ function parseBoolean(value) {
34
+ return ['1', 'true', 'yes'].includes(String(value ?? '').toLowerCase());
35
+ }
36
+
37
+ function normalizeRelative(root, absolute) {
38
+ return relative(root, absolute).split(sep).join('/') || '.';
39
+ }
40
+
41
+ function sha256(value) {
42
+ return createHash('sha256').update(value).digest('hex');
43
+ }
44
+
45
+ function defaultLedgerPath(root) {
46
+ return resolve(`.prismer/operations/skill-library-${sha256(root).slice(0, 16)}.json`);
47
+ }
48
+
49
+ export function loadLibraryImportConfig(argv, env = process.env) {
50
+ const flags = new Set();
51
+ for (let index = 0; index < argv.length; index += 1) {
52
+ const arg = argv[index];
53
+ const name = optionName(arg);
54
+ if (SECRET_OPTIONS.has(name)) {
55
+ throw new LibraryImportError(
56
+ 'local_validation',
57
+ 'SECRET_ARGUMENT_FORBIDDEN',
58
+ `${name} is forbidden; credentials must come from the injected environment`,
59
+ 2,
60
+ );
61
+ }
62
+ if (!BOOLEAN_OPTIONS.has(arg)) {
63
+ throw new LibraryImportError(
64
+ 'local_validation',
65
+ 'UNKNOWN_ARGUMENT',
66
+ `unknown argument at position ${index + 1}`,
67
+ 2,
68
+ );
69
+ }
70
+ flags.add(arg);
71
+ }
72
+ if (flags.has('--help')) return { help: true };
73
+
74
+ const sourceRootRaw = env.PRISMER_SKILL_LIBRARY_ROOT?.trim();
75
+ if (!sourceRootRaw) {
76
+ throw new LibraryImportError('local_validation', 'CONFIG_MISSING', 'PRISMER_SKILL_LIBRARY_ROOT is required', 2);
77
+ }
78
+ const sourceRoot = resolve(sourceRootRaw);
79
+ if (!existsSync(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
80
+ throw new LibraryImportError(
81
+ 'local_validation',
82
+ 'SOURCE_ROOT_INVALID',
83
+ 'PRISMER_SKILL_LIBRARY_ROOT must identify an existing directory',
84
+ 2,
85
+ );
86
+ }
87
+
88
+ let baseUrl;
89
+ try {
90
+ baseUrl = new URL((env.PRISMER_CLOUD_BASE || 'http://127.0.0.1:3000').replace(/\/$/, ''));
91
+ } catch {
92
+ throw new LibraryImportError('local_validation', 'INVALID_BASE_URL', 'PRISMER_CLOUD_BASE must be a valid URL', 2);
93
+ }
94
+ if (
95
+ !['http:', 'https:'].includes(baseUrl.protocol) ||
96
+ baseUrl.username ||
97
+ baseUrl.password ||
98
+ baseUrl.search ||
99
+ baseUrl.hash
100
+ ) {
101
+ throw new LibraryImportError(
102
+ 'local_validation',
103
+ 'UNSAFE_BASE_URL',
104
+ 'PRISMER_CLOUD_BASE must be a plain http(s) origin without credentials, query, or hash',
105
+ 2,
106
+ );
107
+ }
108
+
109
+ const preflightOnly = flags.has('--preflight-only');
110
+ if (!preflightOnly && !isLocalTarget(baseUrl.hostname) && !parseBoolean(env.PRISMER_ALLOW_REMOTE_WRITE)) {
111
+ throw new LibraryImportError(
112
+ 'local_validation',
113
+ 'REMOTE_WRITE_NOT_CONFIRMED',
114
+ 'remote mutation is blocked; set PRISMER_ALLOW_REMOTE_WRITE=1 after confirming the target environment',
115
+ 2,
116
+ );
117
+ }
118
+
119
+ const excludes = String(env.PRISMER_SKILL_LIBRARY_EXCLUDE || '')
120
+ .split(/[\n,]/)
121
+ .map((entry) => entry.trim().replace(/^\.\//, '').replace(/\/$/, ''))
122
+ .filter(Boolean);
123
+
124
+ return {
125
+ sourceRoot,
126
+ excludes,
127
+ cloudBin: env.PRISMER_CLOUD_BIN?.trim() || 'cloud',
128
+ baseUrl: baseUrl.toString().replace(/\/$/, ''),
129
+ ledgerPath: resolve(env.PRISMER_IMPORT_LEDGER?.trim() || defaultLedgerPath(sourceRoot)),
130
+ preflightOnly,
131
+ json: flags.has('--json'),
132
+ };
133
+ }
134
+
135
+ function excluded(config, absolute) {
136
+ const path = normalizeRelative(config.sourceRoot, absolute);
137
+ return config.excludes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
138
+ }
139
+
140
+ export function discoverSkillDirectories(config) {
141
+ const found = [];
142
+ const visit = (directory) => {
143
+ if (excluded(config, directory)) return;
144
+ const entries = readdirSync(directory, { withFileTypes: true });
145
+ if (entries.some((entry) => entry.isFile() && entry.name === 'SKILL.md')) {
146
+ found.push(directory);
147
+ return;
148
+ }
149
+ for (const entry of entries) {
150
+ if (!entry.isDirectory() || IGNORED_DIRECTORIES.has(entry.name) || entry.name.startsWith('.')) continue;
151
+ visit(resolve(directory, entry.name));
152
+ }
153
+ };
154
+ visit(config.sourceRoot);
155
+ return found.sort((left, right) => left.localeCompare(right));
156
+ }
157
+
158
+ function sourceRevision(config, directories) {
159
+ const hash = createHash('sha256');
160
+ for (const directory of directories) {
161
+ const files = [];
162
+ const visit = (current) => {
163
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
164
+ if (entry.name.startsWith('.')) continue;
165
+ const absolute = resolve(current, entry.name);
166
+ if (entry.isDirectory()) visit(absolute);
167
+ else files.push(absolute);
168
+ }
169
+ };
170
+ visit(directory);
171
+ for (const file of files.sort((left, right) => left.localeCompare(right))) {
172
+ hash.update(normalizeRelative(config.sourceRoot, file));
173
+ hash.update('\0');
174
+ hash.update(readFileSync(file));
175
+ hash.update('\0');
176
+ }
177
+ }
178
+ return hash.digest('hex');
179
+ }
180
+
181
+ function bundleManifest(directory) {
182
+ const files = [];
183
+ const visit = (current) => {
184
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
185
+ if (entry.name.startsWith('.')) continue;
186
+ const absolute = resolve(current, entry.name);
187
+ if (entry.isDirectory()) visit(absolute);
188
+ else if (!BUNDLE_EXCLUDED_FILES.has(entry.name)) files.push(absolute);
189
+ }
190
+ };
191
+ visit(directory);
192
+ return files
193
+ .sort((left, right) => left.localeCompare(right))
194
+ .map((absolute) => {
195
+ const bytes = readFileSync(absolute);
196
+ return {
197
+ path: normalizeRelative(directory, absolute),
198
+ size: bytes.byteLength,
199
+ sha256: sha256(bytes),
200
+ ...(normalizeRelative(directory, absolute) === 'SKILL.md' ? { content: bytes.toString('utf8') } : {}),
201
+ };
202
+ });
203
+ }
204
+
205
+ function readbackMatchesBundle(readback, directory) {
206
+ const expected = bundleManifest(directory);
207
+ const actual = Array.isArray(readback?.files) ? readback.files : [];
208
+ if (actual.length !== expected.length) return false;
209
+ const actualByPath = new Map(actual.map((file) => [file?.path, file]));
210
+ for (const file of expected) {
211
+ const remote = actualByPath.get(file.path);
212
+ if (!remote || remote.sha256 !== file.sha256 || Number(remote.size) !== file.size) return false;
213
+ if (file.path === 'SKILL.md' && readback.content !== file.content) return false;
214
+ }
215
+ return true;
216
+ }
217
+
218
+ async function readbackOwnedBundle(config, row, directory, runCloud, canonicalSlug) {
219
+ const shown = await runCloud(config, ['skill', 'show', canonicalSlug, '--content', '--json']);
220
+ if (!shown.ok) return { ok: false, error: safeCliFailure(shown) };
221
+ const readback = parseJsonOutput(shown, 'readback');
222
+ if (!readbackMatchesBundle(readback, directory)) {
223
+ return { ok: false, error: 'catalog readback does not match the selected local bundle' };
224
+ }
225
+ row.canonicalSlug = canonicalSlug;
226
+ row.stage = 'verified';
227
+ row.verifiedAt = new Date().toISOString();
228
+ return { ok: true, readback };
229
+ }
230
+
231
+ function parseJsonOutput(result, stage) {
232
+ const raw = String(result.stdout || '').trim();
233
+ try {
234
+ return JSON.parse(raw);
235
+ } catch {
236
+ const start = raw.indexOf('{');
237
+ const end = raw.lastIndexOf('}');
238
+ if (start >= 0 && end > start) {
239
+ try {
240
+ return JSON.parse(raw.slice(start, end + 1));
241
+ } catch {
242
+ // Fall through to the typed diagnostic below.
243
+ }
244
+ }
245
+ throw new LibraryImportError(stage, 'INVALID_CLI_JSON', 'cloud CLI did not return valid JSON', 5);
246
+ }
247
+ }
248
+
249
+ function defaultRunCloud(config, args) {
250
+ const result = spawnSync(config.cloudBin, args, {
251
+ encoding: 'utf8',
252
+ env: { ...process.env, PRISMER_CLOUD_BASE: config.baseUrl },
253
+ shell: false,
254
+ });
255
+ if (result.error) {
256
+ throw new LibraryImportError(
257
+ 'local_validation',
258
+ 'CLOUD_CLI_UNAVAILABLE',
259
+ `could not execute the configured Cloud CLI: ${result.error.message}`,
260
+ 2,
261
+ );
262
+ }
263
+ return {
264
+ ok: result.status === 0,
265
+ status: result.status,
266
+ stdout: result.stdout || '',
267
+ stderr: result.stderr || '',
268
+ };
269
+ }
270
+
271
+ function writeLedger(path, ledger) {
272
+ mkdirSync(dirname(path), { recursive: true });
273
+ const temporary = `${path}.tmp`;
274
+ writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
275
+ renameSync(temporary, path);
276
+ }
277
+
278
+ function readLedger(path) {
279
+ if (!existsSync(path)) return null;
280
+ try {
281
+ const value = JSON.parse(readFileSync(path, 'utf8'));
282
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
283
+ } catch {
284
+ throw new LibraryImportError(
285
+ 'local_validation',
286
+ 'INVALID_IMPORT_LEDGER',
287
+ 'the import ledger exists but is not valid JSON',
288
+ 2,
289
+ );
290
+ }
291
+ }
292
+
293
+ function safeCliFailure(result) {
294
+ const message = String(result.stderr || '').trim();
295
+ return message ? message.slice(0, 2_000) : `cloud CLI exited ${String(result.status)}`;
296
+ }
297
+
298
+ export async function runLibraryImport(config, runCloud = defaultRunCloud) {
299
+ const directories = discoverSkillDirectories(config);
300
+ if (directories.length === 0) {
301
+ throw new LibraryImportError('local_validation', 'NO_SKILLS_FOUND', 'no root SKILL.md directories found', 2);
302
+ }
303
+ const revision = sourceRevision(config, directories);
304
+ const previous = readLedger(config.ledgerPath);
305
+ if (previous && (previous.sourceRoot !== config.sourceRoot || previous.sourceRevision !== revision)) {
306
+ throw new LibraryImportError(
307
+ 'local_validation',
308
+ 'IMPORT_LEDGER_CONFLICT',
309
+ 'the import ledger belongs to a different source selection or source revision',
310
+ 2,
311
+ );
312
+ }
313
+
314
+ const rowsByPath = new Map((previous?.rows || []).map((row) => [row.sourcePath, row]));
315
+ const rows = [];
316
+ for (const directory of directories) {
317
+ const sourcePath = normalizeRelative(config.sourceRoot, directory);
318
+ const prior = rowsByPath.get(sourcePath);
319
+ if (prior?.stage === 'verified') {
320
+ rows.push(prior);
321
+ continue;
322
+ }
323
+ const validated = await runCloud(config, ['skill', 'validate', directory, '--json']);
324
+ let validation = null;
325
+ try {
326
+ validation = parseJsonOutput(validated, 'validation');
327
+ } catch (error) {
328
+ if (validated.ok) throw error;
329
+ }
330
+ rows.push({
331
+ sourcePath,
332
+ requestedSlug: validation?.slug ?? prior?.requestedSlug ?? null,
333
+ stage: validated.ok && validation?.ok !== false ? 'validated' : 'validation_failed',
334
+ ...(validated.ok && validation?.ok !== false
335
+ ? {}
336
+ : { error: safeCliFailure(validated), validation: validation ?? undefined }),
337
+ });
338
+ }
339
+
340
+ const slugs = rows.map((row) => row.requestedSlug).filter(Boolean);
341
+ const duplicates = [...new Set(slugs.filter((slug, index) => slugs.indexOf(slug) !== index))];
342
+ if (duplicates.length > 0) {
343
+ throw new LibraryImportError(
344
+ 'validation',
345
+ 'DUPLICATE_SOURCE_SLUGS',
346
+ 'the selected source contains duplicate Skill slugs; choose the authoritative tree or exclude mirrors',
347
+ 2,
348
+ { slugs: duplicates },
349
+ );
350
+ }
351
+
352
+ const ledger = {
353
+ schemaVersion: 1,
354
+ sourceRoot: config.sourceRoot,
355
+ sourceRevision: revision,
356
+ stage: rows.some((row) => row.stage === 'validation_failed') ? 'validation_failed' : 'validated',
357
+ rows,
358
+ updatedAt: new Date().toISOString(),
359
+ };
360
+ writeLedger(config.ledgerPath, ledger);
361
+ if (ledger.stage === 'validation_failed') {
362
+ throw new LibraryImportError(
363
+ 'validation',
364
+ 'LIBRARY_VALIDATION_FAILED',
365
+ 'one or more Skill bundles failed local validation; no catalog writes were attempted',
366
+ 2,
367
+ { failed: rows.filter((row) => row.stage === 'validation_failed').map((row) => row.sourcePath) },
368
+ );
369
+ }
370
+ if (config.preflightOnly) return ledger;
371
+
372
+ for (const row of rows) {
373
+ if (row.stage === 'verified') continue;
374
+ const directory = resolve(config.sourceRoot, row.sourcePath);
375
+ const created = await runCloud(config, ['skill', 'create', directory, '--json']);
376
+ if (!created.ok) {
377
+ // A process may die after the server commits but before the ledger records
378
+ // the create response. Community slugs are server-canonical and
379
+ // deterministic; only accept the existing owner-visible row when its
380
+ // complete manifest matches the selected bundle byte-for-byte.
381
+ const recoverySlug = `community-${row.requestedSlug}`;
382
+ const recovered = await readbackOwnedBundle(config, row, directory, runCloud, recoverySlug);
383
+ if (recovered.ok) {
384
+ row.recoveredAfterCreateFailure = true;
385
+ delete row.error;
386
+ } else {
387
+ row.stage = 'create_failed';
388
+ row.error = safeCliFailure(created);
389
+ row.recoveryError = recovered.error;
390
+ }
391
+ ledger.updatedAt = new Date().toISOString();
392
+ writeLedger(config.ledgerPath, ledger);
393
+ continue;
394
+ }
395
+ const creation = parseJsonOutput(created, 'create');
396
+ row.id = creation.id ?? null;
397
+ row.canonicalSlug = creation.slug ?? null;
398
+ row.publishScope = creation.publishScope ?? null;
399
+ row.status = creation.status ?? null;
400
+ row.stage = 'created';
401
+ delete row.error;
402
+ ledger.updatedAt = new Date().toISOString();
403
+ writeLedger(config.ledgerPath, ledger);
404
+
405
+ if (!row.canonicalSlug) {
406
+ row.stage = 'readback_failed';
407
+ row.error = 'create response did not contain a canonical slug';
408
+ writeLedger(config.ledgerPath, ledger);
409
+ continue;
410
+ }
411
+ const verified = await readbackOwnedBundle(config, row, directory, runCloud, row.canonicalSlug);
412
+ if (!verified.ok) {
413
+ row.stage = 'readback_failed';
414
+ row.error = verified.error;
415
+ writeLedger(config.ledgerPath, ledger);
416
+ continue;
417
+ }
418
+ writeLedger(config.ledgerPath, ledger);
419
+ }
420
+
421
+ const failed = rows.filter((row) => row.stage !== 'verified');
422
+ ledger.stage = failed.length === 0 ? 'completed' : 'partial_failure';
423
+ ledger.completedAt = failed.length === 0 ? new Date().toISOString() : undefined;
424
+ ledger.updatedAt = new Date().toISOString();
425
+ ledger.requiredSkills = rows
426
+ .filter((row) => row.stage === 'verified')
427
+ .map((row) => ({ skillSlug: row.canonicalSlug, required: true }));
428
+ writeLedger(config.ledgerPath, ledger);
429
+ if (failed.length > 0) {
430
+ throw new LibraryImportError(
431
+ 'import',
432
+ 'LIBRARY_IMPORT_PARTIAL_FAILURE',
433
+ 'some Skill bundles were not verified; retry with the same ledger after fixing transient failures',
434
+ 5,
435
+ { failed: failed.map((row) => ({ sourcePath: row.sourcePath, stage: row.stage })) },
436
+ );
437
+ }
438
+ return ledger;
439
+ }
440
+
441
+ const HELP = `Usage:
442
+ # PRISMER_SKILL_LIBRARY_ROOT and cloud authentication are injected in env
443
+ node scripts/import-library.mjs [--preflight-only] [--json]
444
+
445
+ Optional env: PRISMER_SKILL_LIBRARY_EXCLUDE, PRISMER_IMPORT_LEDGER,
446
+ PRISMER_CLOUD_BIN, PRISMER_CLOUD_BASE, PRISMER_ALLOW_REMOTE_WRITE=1.`;
447
+
448
+ async function main() {
449
+ let config;
450
+ try {
451
+ config = loadLibraryImportConfig(process.argv.slice(2));
452
+ if (config.help) {
453
+ process.stdout.write(`${HELP}\n`);
454
+ return;
455
+ }
456
+ const result = await runLibraryImport(config);
457
+ if (config.json) process.stdout.write(`${JSON.stringify(result)}\n`);
458
+ else {
459
+ process.stdout.write(
460
+ `[skill-creator] ${result.stage}: verified=${result.rows.filter((row) => row.stage === 'verified').length}/${result.rows.length} ledger=${config.ledgerPath}\n`,
461
+ );
462
+ }
463
+ } catch (error) {
464
+ const failure =
465
+ error instanceof LibraryImportError
466
+ ? error
467
+ : new LibraryImportError('internal', 'UNEXPECTED_ERROR', error?.message || String(error), 1);
468
+ process.stderr.write(
469
+ `${JSON.stringify({ ok: false, stage: failure.stage, error: { code: failure.code, message: failure.message, details: failure.details }, ...(config?.ledgerPath ? { ledger: config.ledgerPath } : {}) })}\n`,
470
+ );
471
+ process.exitCode = failure.exitCode;
472
+ }
473
+ }
474
+
475
+ if (import.meta.url === `file://${process.argv[1]}`) await main();
@@ -1,9 +1,29 @@
1
1
  ---
2
2
  name: slack-gif-creator
3
+ scope: persistence
3
4
  description: Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."
4
5
  license: Complete terms in LICENSE.txt
6
+ config:
7
+ - key: SLACK_WEBHOOK_URL
8
+ type: secret
9
+ required: false
10
+ default: null
11
+ bindable: [role, agent]
12
+ description: Optional Slack Incoming Webhook to post the finished GIF to a channel directly. Webhook URLs embed a token — treated as a secret.
13
+ prompt:
14
+ zh: "要把 GIF 直接发到 Slack 频道吗?请提供 Incoming Webhook URL。"
15
+ en: "Should the GIF be posted to a Slack channel directly? Provide the Incoming Webhook URL."
5
16
  ---
6
17
 
18
+ ## Optional: post directly to Slack
19
+
20
+ If `SLACK_WEBHOOK_URL` is present in your environment, you may additionally
21
+ post the finished GIF to that Slack channel (upload the file where the
22
+ webhook workflow expects it, or send a message linking the delivered asset).
23
+ Never print the webhook URL itself into chat, files, or logs. When the
24
+ variable is absent, skip this step silently — delivery below is the only
25
+ required output.
26
+
7
27
  ## Delivering the GIF
8
28
 
9
29
  When this skill finishes producing an animated GIF (`output.gif`), deliver