@constraint/cli 0.4.1 → 0.4.2
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/package.json +1 -1
- package/src/cli.js +30 -1
- package/src/files.js +3 -1
- package/src/skills.js +36 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './setup.js';
|
|
27
27
|
import {
|
|
28
28
|
configuredAgents,
|
|
29
|
+
identifyLocalSkill,
|
|
29
30
|
installSkill,
|
|
30
31
|
installedSkills,
|
|
31
32
|
manageableDomainRecords,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
uploadSkill,
|
|
36
37
|
} from './skills.js';
|
|
37
38
|
|
|
38
|
-
const VERSION = '0.4.
|
|
39
|
+
const VERSION = '0.4.2';
|
|
39
40
|
|
|
40
41
|
const HELP = `Constraint Skills CLI
|
|
41
42
|
|
|
@@ -481,7 +482,31 @@ async function reportCommand(args, config, out) {
|
|
|
481
482
|
if (!resolvedClient || !resolvedSession) {
|
|
482
483
|
throw new Error('Could not tell the client and session from this path. Pass --client and --session.');
|
|
483
484
|
}
|
|
485
|
+
|
|
486
|
+
// Version attribution: hash the local skill folder and match it against the
|
|
487
|
+
// published versions, so the run records what actually ran — not "latest".
|
|
488
|
+
let detail;
|
|
489
|
+
try {
|
|
490
|
+
detail = await apiRequest(config, `/skills/${encodeURIComponent(skill)}`);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (/not found/i.test(error.message)) {
|
|
493
|
+
const projectRoot = await findProjectRoot();
|
|
494
|
+
const hint = await identifyLocalSkill(config, { skill_id: skill, slug: skill, latest_version: 0 }, { projectRoot })
|
|
495
|
+
.then((found) => found.localPath)
|
|
496
|
+
.catch(() => null);
|
|
497
|
+
throw new Error(
|
|
498
|
+
`${skill} is not on Constraint.` +
|
|
499
|
+
(hint ? ` Found a local skill at ${hint} — upload it first:
|
|
500
|
+
constraint skills upload ${hint} --domain <domain>` : ' Upload it first with constraint skills upload.'),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
const projectRoot = await findProjectRoot();
|
|
506
|
+
const identified = await identifyLocalSkill(config, detail, { projectRoot });
|
|
507
|
+
|
|
484
508
|
const query = new URLSearchParams({ skill, client: resolvedClient, session: resolvedSession });
|
|
509
|
+
if (identified.verified) query.set('version', String(identified.version));
|
|
485
510
|
const run = await apiRequest(config, `/skill-reports?${query.toString()}`, {
|
|
486
511
|
method: 'POST',
|
|
487
512
|
headers: { 'Content-Type': mediaType(transcript.absolute), 'X-Transcript-Filename': transcript.filename },
|
|
@@ -491,6 +516,10 @@ async function reportCommand(args, config, out) {
|
|
|
491
516
|
run_id: run.run_id,
|
|
492
517
|
skill: run.skill_slug,
|
|
493
518
|
version: run.skill_version,
|
|
519
|
+
version_verified: identified.verified,
|
|
520
|
+
...(identified.localPath && !identified.verified
|
|
521
|
+
? { warning: `local copy at ${identified.localPath} does not match any published version` }
|
|
522
|
+
: {}),
|
|
494
523
|
state: run.state,
|
|
495
524
|
client: run.client,
|
|
496
525
|
session: run.client_session_id,
|
package/src/files.js
CHANGED
|
@@ -43,7 +43,9 @@ export async function readSkillDirectory(input) {
|
|
|
43
43
|
|
|
44
44
|
export function packageDigest(files) {
|
|
45
45
|
const hash = crypto.createHash('sha256');
|
|
46
|
-
|
|
46
|
+
// Sort by code points, not locale: this digest must match the server's,
|
|
47
|
+
// which sorts paths with plain string comparison.
|
|
48
|
+
for (const file of [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
|
|
47
49
|
hash.update(file.path);
|
|
48
50
|
hash.update('\0');
|
|
49
51
|
hash.update(file.content);
|
package/src/skills.js
CHANGED
|
@@ -231,6 +231,42 @@ export async function installedSkills({ scope = 'project', projectRoot, agents }
|
|
|
231
231
|
return Object.values(lock.installations || {}).filter((item) => !selected || selected.has(item.agent));
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Version attribution for reports: find the skill's local folder (managed
|
|
235
|
+
// symlink or plain folder — agents read both from the same directories), hash
|
|
236
|
+
// it with the registry's package digest, and match against published versions.
|
|
237
|
+
export async function identifyLocalSkill(config, detail, { projectRoot = null } = {}) {
|
|
238
|
+
const candidates = [];
|
|
239
|
+
if (projectRoot) {
|
|
240
|
+
for (const agent of ['claude-code', 'codex']) {
|
|
241
|
+
candidates.push(path.join(agentPaths(agent, { scope: 'project', projectRoot }).skills, detail.slug));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const agent of ['claude-code', 'codex']) {
|
|
245
|
+
candidates.push(path.join(agentPaths(agent).skills, detail.slug));
|
|
246
|
+
}
|
|
247
|
+
let local = null;
|
|
248
|
+
for (const candidate of candidates) {
|
|
249
|
+
try {
|
|
250
|
+
const digest = packageDigest(await readSkillDirectory(candidate));
|
|
251
|
+
local = { path: candidate, digest };
|
|
252
|
+
break;
|
|
253
|
+
} catch {
|
|
254
|
+
// not present here; keep looking
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!local) return { localPath: null, version: null, verified: false };
|
|
258
|
+
for (let version = detail.latest_version; version >= 1; version -= 1) {
|
|
259
|
+
const candidate = await apiRequest(
|
|
260
|
+
config,
|
|
261
|
+
`/skills/${encodeURIComponent(detail.skill_id)}/versions/${version}`,
|
|
262
|
+
);
|
|
263
|
+
if (candidate.version.content_sha256 === local.digest) {
|
|
264
|
+
return { localPath: local.path, version, verified: true };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { localPath: local.path, version: null, verified: false };
|
|
268
|
+
}
|
|
269
|
+
|
|
234
270
|
export async function readTranscript(file) {
|
|
235
271
|
const absolute = path.resolve(file);
|
|
236
272
|
return { absolute, filename: path.basename(absolute), content: await fs.readFile(absolute) };
|