@constraint/cli 0.4.1 → 0.4.3
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 +2 -3
- package/src/cli.js +85 -145
- package/src/config.js +0 -3
- package/src/files.js +3 -1
- package/src/prompts.js +3 -13
- package/src/skills.js +36 -0
- package/skills/constraint-skills/SKILL.md +0 -71
- package/skills/constraint-skills/agents/openai.yaml +0 -4
- package/skills/constraint-skills/references/commands.md +0 -76
- package/src/setup.js +0 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constraint/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Install, publish, and report Agent Skills with Constraint",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
-
"src"
|
|
12
|
-
"skills"
|
|
11
|
+
"src"
|
|
13
12
|
],
|
|
14
13
|
"scripts": {
|
|
15
14
|
"check": "node --check bin/constraint.js && node --check src/cli.js",
|
package/src/cli.js
CHANGED
|
@@ -8,24 +8,19 @@ import { rejectUnknown, takeFlag, takeOption } from './arguments.js';
|
|
|
8
8
|
import { login, logout } from './auth.js';
|
|
9
9
|
import { loadConfig } from './config.js';
|
|
10
10
|
import { mediaType } from './files.js';
|
|
11
|
-
import { AGENTS, detectAgents, findProjectRoot, normalizeAgent } from './paths.js';
|
|
11
|
+
import { AGENTS, agentPaths, detectAgents, findProjectRoot, normalizeAgent } from './paths.js';
|
|
12
12
|
import {
|
|
13
13
|
begin,
|
|
14
14
|
chooseAgents,
|
|
15
|
-
chooseMode,
|
|
16
15
|
chooseScope,
|
|
17
16
|
confirmPlan,
|
|
18
17
|
finish,
|
|
19
18
|
note,
|
|
20
19
|
taskSpinner,
|
|
21
20
|
} from './prompts.js';
|
|
22
|
-
import {
|
|
23
|
-
installCompanionSkill,
|
|
24
|
-
setupPlan,
|
|
25
|
-
setupStatus,
|
|
26
|
-
} from './setup.js';
|
|
27
21
|
import {
|
|
28
22
|
configuredAgents,
|
|
23
|
+
identifyLocalSkill,
|
|
29
24
|
installSkill,
|
|
30
25
|
installedSkills,
|
|
31
26
|
manageableDomainRecords,
|
|
@@ -35,16 +30,14 @@ import {
|
|
|
35
30
|
uploadSkill,
|
|
36
31
|
} from './skills.js';
|
|
37
32
|
|
|
38
|
-
const VERSION = '0.4.
|
|
33
|
+
const VERSION = '0.4.3';
|
|
39
34
|
|
|
40
35
|
const HELP = `Constraint Skills CLI
|
|
41
36
|
|
|
42
37
|
Usage:
|
|
43
38
|
constraint login | logout | whoami
|
|
44
|
-
constraint setup [--agent codex|claude-code]... [--dry-run] [--yes]
|
|
45
39
|
constraint doctor [--agent codex|claude-code]...
|
|
46
40
|
constraint skills list [--search <text>]
|
|
47
|
-
constraint skills list --installed [--project|--global] [--agent <agent>]...
|
|
48
41
|
constraint skills domains
|
|
49
42
|
constraint skills inspect <skill> [--version <version>] [--files]
|
|
50
43
|
constraint skills install <skill> [--version <version>] [--agent <agent>]...
|
|
@@ -52,10 +45,10 @@ Usage:
|
|
|
52
45
|
constraint skills update <skill> [--agent <agent>]... [--project|--global]
|
|
53
46
|
[--copy] [--yes]
|
|
54
47
|
constraint skills update --all [--agent <agent>]... [--project|--global] [--yes]
|
|
55
|
-
constraint skills upload <path> --domain <domain>
|
|
48
|
+
constraint skills upload <path> [--domain <domain>]
|
|
56
49
|
constraint report <transcript-path> --skill <skill> [--client <client>] [--session <id>]
|
|
57
|
-
constraint skills run list [--team <team>]... [--
|
|
58
|
-
constraint skills run get <run-id
|
|
50
|
+
constraint skills run list [--team <team>]... [--skill <skill>]...
|
|
51
|
+
constraint skills run get <run-id> [--raw] [--output <path>] (writes a file, prints summary + path)
|
|
59
52
|
|
|
60
53
|
Global options: --json --quiet --no-color --help --version`;
|
|
61
54
|
|
|
@@ -88,14 +81,8 @@ function humanize(value) {
|
|
|
88
81
|
return String(value || '').replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
89
82
|
}
|
|
90
83
|
|
|
91
|
-
export function skillListPresentation(items, {
|
|
84
|
+
export function skillListPresentation(items, { search = null } = {}) {
|
|
92
85
|
if (items.length === 0) {
|
|
93
|
-
if (installed) {
|
|
94
|
-
return {
|
|
95
|
-
title: 'No installed skills',
|
|
96
|
-
message: `Nothing is installed in ${scope} scope yet.\n\nBrowse available skills:\nconstraint skills list`,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
86
|
if (search) {
|
|
100
87
|
return {
|
|
101
88
|
title: 'No matching skills',
|
|
@@ -108,20 +95,10 @@ export function skillListPresentation(items, { installed = false, search = null,
|
|
|
108
95
|
};
|
|
109
96
|
}
|
|
110
97
|
|
|
111
|
-
if (installed) {
|
|
112
|
-
return {
|
|
113
|
-
title: `${items.length} installed skill${items.length === 1 ? '' : 's'}`,
|
|
114
|
-
message: items.map((item) => [
|
|
115
|
-
`${item.slug} · v${item.version} · ${item.agent}`,
|
|
116
|
-
`${humanize(item.scope)} · ${humanize(item.mode)} · ${item.path}`,
|
|
117
|
-
].join('\n')).join('\n\n'),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
98
|
return {
|
|
122
99
|
title: `${items.length} available skill${items.length === 1 ? '' : 's'}`,
|
|
123
100
|
message: items.map((item) => [
|
|
124
|
-
`${item.name}
|
|
101
|
+
`${item.name} · v${item.latest_version} · ${humanize(item.domain)}`,
|
|
125
102
|
item.description,
|
|
126
103
|
].join('\n')).join('\n\n'),
|
|
127
104
|
};
|
|
@@ -193,7 +170,7 @@ async function resolveAgents(provided, out, { prompt = true } = {}) {
|
|
|
193
170
|
async function chooseDomain(domains) {
|
|
194
171
|
if (domains.length === 0) throw new Error('You do not have permission to upload skills.');
|
|
195
172
|
if (domains.length === 1) return domains[0];
|
|
196
|
-
if (!process.stdin.isTTY) throw new Error(
|
|
173
|
+
if (!process.stdin.isTTY) throw new Error(`Choose a team with --domain. You can upload to: ${domains.join(', ')}`);
|
|
197
174
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
198
175
|
domains.forEach((domain, index) => process.stdout.write(`${index + 1}. ${domain}\n`));
|
|
199
176
|
const answer = await rl.question('Choose the skill domain: ');
|
|
@@ -222,74 +199,14 @@ async function authCommand(command, args, config, out) {
|
|
|
222
199
|
await logout(config);
|
|
223
200
|
out.line('Logged out.');
|
|
224
201
|
} else {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
async function setupCommand(args, out) {
|
|
230
|
-
let agents = takeAgents(args);
|
|
231
|
-
const dryRun = takeFlag(args, '--dry-run');
|
|
232
|
-
const yes = takeFlag(args, '--yes');
|
|
233
|
-
rejectUnknown(args);
|
|
234
|
-
if (args.length) throw new Error('setup does not accept positional arguments');
|
|
235
|
-
const useUi = interactive(out) && !dryRun;
|
|
236
|
-
if (useUi) begin('Constraint Skills setup');
|
|
237
|
-
if (!agents.length) {
|
|
238
|
-
const configured = await configuredAgents();
|
|
239
|
-
const detected = configured.length ? configured : await detectAgents();
|
|
240
|
-
agents = useUi && !yes ? await chooseAgents(detected) : detected;
|
|
241
|
-
}
|
|
242
|
-
if (!agents.length) throw new Error('Choose at least one agent with --agent.');
|
|
243
|
-
const plans = await Promise.all(agents.map(setupPlan));
|
|
244
|
-
const conflicts = plans.filter((plan) => plan.skill.action === 'conflict');
|
|
245
|
-
if (conflicts.length) {
|
|
246
|
-
throw new Error(`Local Constraint skill has uncommitted changes and was not replaced: ${conflicts.map((plan) => plan.skill.path).join(', ')}`);
|
|
247
|
-
}
|
|
248
|
-
if (dryRun) {
|
|
249
|
-
out.data(plans.map((plan) => ({
|
|
250
|
-
agent: plan.agent,
|
|
251
|
-
skill: { path: plan.skill.path, scope: 'global', action: plan.skill.action },
|
|
252
|
-
})));
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
const hasSkillChanges = plans.some((plan) => plan.skill.action !== 'current');
|
|
256
|
-
if (!useUi && !yes && hasSkillChanges) {
|
|
257
|
-
throw new Error('Non-interactive setup has pending changes. Pass --yes or use --dry-run.');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
let installSkill = true;
|
|
261
|
-
if (useUi) {
|
|
262
|
-
note(
|
|
263
|
-
[
|
|
264
|
-
'Skill: constraint-skills',
|
|
265
|
-
'Scope: Global',
|
|
266
|
-
'',
|
|
267
|
-
...plans.flatMap((plan) => [
|
|
268
|
-
`${humanize(plan.agent)} destination:`,
|
|
269
|
-
`${plan.skill.path} · ${plan.skill.action === 'current' ? 'already current' : plan.skill.action}`,
|
|
270
|
-
'',
|
|
271
|
-
]),
|
|
272
|
-
].join('\n').trim(),
|
|
273
|
-
'Install Constraint Skills',
|
|
274
|
-
);
|
|
275
|
-
installSkill = !hasSkillChanges || yes || await confirmPlan(`Install globally for ${agents.map(humanize).join(' and ')}?`);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const result = { skill: [] };
|
|
279
|
-
if (installSkill) {
|
|
280
|
-
const spinner = taskSpinner(useUi && hasSkillChanges);
|
|
281
|
-
for (const plan of plans) {
|
|
282
|
-
spinner.message(`Installing Constraint Skills for ${humanize(plan.agent)}`);
|
|
283
|
-
result.skill.push(await installCompanionSkill(plan.agent));
|
|
202
|
+
const me = await apiRequest(config, '/me');
|
|
203
|
+
if (out.json) {
|
|
204
|
+
out.data(me);
|
|
205
|
+
return;
|
|
284
206
|
}
|
|
285
|
-
|
|
207
|
+
const teams = (me.domains || []).map((domain) => domain.name || domain.slug);
|
|
208
|
+
out.line(`Signed in as ${me.workos_user_id} · ${me.roles?.join(', ') || 'member'} · ${teams.length} team${teams.length === 1 ? '' : 's'}${teams.length ? ` (${teams.join(', ')})` : ''}`);
|
|
286
209
|
}
|
|
287
|
-
if (useUi) {
|
|
288
|
-
finish([
|
|
289
|
-
'Setup complete.',
|
|
290
|
-
`Skill: ${installSkill ? `installed for ${agents.map(humanize).join(' and ')}` : 'skipped'}`,
|
|
291
|
-
].join('\n'));
|
|
292
|
-
} else out.data(result);
|
|
293
210
|
}
|
|
294
211
|
|
|
295
212
|
async function doctorCommand(args, config, out) {
|
|
@@ -304,30 +221,35 @@ async function doctorCommand(args, config, out) {
|
|
|
304
221
|
} catch (error) {
|
|
305
222
|
authError = error.message;
|
|
306
223
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
224
|
+
const statuses = await Promise.all(agents.map(async (agent) => {
|
|
225
|
+
const selected = normalizeAgent(agent);
|
|
226
|
+
try {
|
|
227
|
+
await fs.stat(path.join(agentPaths(selected).skills, 'constraint-skills', 'SKILL.md'));
|
|
228
|
+
return { agent: selected, skill: true };
|
|
229
|
+
} catch {
|
|
230
|
+
return { agent: selected, skill: false };
|
|
231
|
+
}
|
|
232
|
+
}));
|
|
233
|
+
if (out.json) {
|
|
234
|
+
out.data({
|
|
235
|
+
api_url: config.apiUrl,
|
|
236
|
+
authenticated: Boolean(identity),
|
|
237
|
+
identity,
|
|
238
|
+
auth_error: authError,
|
|
239
|
+
agents: statuses,
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
out.line(identity ? `Signed in against ${config.apiUrl}.` : `Not signed in (${authError || 'run constraint login'}).`);
|
|
244
|
+
for (const status of statuses) {
|
|
245
|
+
out.line(`${humanize(status.agent)}: companion skill ${status.skill ? 'installed' : 'missing — run constraint skills install constraint-skills --global'}.`);
|
|
246
|
+
}
|
|
314
247
|
}
|
|
315
248
|
|
|
316
249
|
async function listSkills(args, config, out) {
|
|
317
250
|
const search = takeOption(args, '--search');
|
|
318
|
-
const installed = takeFlag(args, '--installed');
|
|
319
|
-
const scopeFlag = takeScope(args);
|
|
320
|
-
const agents = takeAgents(args);
|
|
321
251
|
rejectUnknown(args);
|
|
322
252
|
if (args.length) throw new Error('skills list does not accept positional arguments');
|
|
323
|
-
if (installed) {
|
|
324
|
-
if (search) throw new Error('--search cannot be combined with --installed.');
|
|
325
|
-
const scope = scopeFlag || 'project';
|
|
326
|
-
const skills = await installedSkills({ scope, agents });
|
|
327
|
-
printSkillList(skills, { installed: true, scope }, out);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (scopeFlag || agents.length) throw new Error('--project, --global, and --agent require --installed.');
|
|
331
253
|
const query = search ? `?search=${encodeURIComponent(search)}` : '';
|
|
332
254
|
const skills = await apiRequest(config, `/skills${query}`);
|
|
333
255
|
printSkillList(skills, { search }, out);
|
|
@@ -364,13 +286,11 @@ async function installCommand(args, config, out, update = false) {
|
|
|
364
286
|
.map((item) => item.agent))];
|
|
365
287
|
}
|
|
366
288
|
agents = await resolveAgents(agents, out, { prompt: !yes && !update });
|
|
367
|
-
if (!copyRequested && useUi && !yes) copy = (await chooseMode()) === 'copy';
|
|
368
289
|
if (useUi) {
|
|
369
290
|
note([
|
|
370
|
-
`${update ? 'Update' : 'Install'}
|
|
371
|
-
`
|
|
372
|
-
`
|
|
373
|
-
`Mode: ${copy === undefined ? 'preserve installed mode' : copy ? 'copy' : 'shared package'}`,
|
|
291
|
+
`${update ? 'Update' : 'Install'} ${args[0]}${version ? ` v${version}` : ''}`,
|
|
292
|
+
`For: ${agents.map(humanize).join(' and ')}`,
|
|
293
|
+
scope === 'project' ? `Available only in this project${root ? ` (${root})` : ''}` : 'Available everywhere on this computer',
|
|
374
294
|
].join('\n'));
|
|
375
295
|
if (!yes && !(await confirmPlan(`${update ? 'Update' : 'Install'} this skill?`))) {
|
|
376
296
|
finish('No changes made.');
|
|
@@ -481,7 +401,31 @@ async function reportCommand(args, config, out) {
|
|
|
481
401
|
if (!resolvedClient || !resolvedSession) {
|
|
482
402
|
throw new Error('Could not tell the client and session from this path. Pass --client and --session.');
|
|
483
403
|
}
|
|
404
|
+
|
|
405
|
+
// Version attribution: hash the local skill folder and match it against the
|
|
406
|
+
// published versions, so the run records what actually ran — not "latest".
|
|
407
|
+
let detail;
|
|
408
|
+
try {
|
|
409
|
+
detail = await apiRequest(config, `/skills/${encodeURIComponent(skill)}`);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (/not found/i.test(error.message)) {
|
|
412
|
+
const projectRoot = await findProjectRoot();
|
|
413
|
+
const hint = await identifyLocalSkill(config, { skill_id: skill, slug: skill, latest_version: 0 }, { projectRoot })
|
|
414
|
+
.then((found) => found.localPath)
|
|
415
|
+
.catch(() => null);
|
|
416
|
+
throw new Error(
|
|
417
|
+
`${skill} is not on Constraint.` +
|
|
418
|
+
(hint ? ` Found a local skill at ${hint} — upload it first:
|
|
419
|
+
constraint skills upload ${hint} --domain <domain>` : ' Upload it first with constraint skills upload.'),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
const projectRoot = await findProjectRoot();
|
|
425
|
+
const identified = await identifyLocalSkill(config, detail, { projectRoot });
|
|
426
|
+
|
|
484
427
|
const query = new URLSearchParams({ skill, client: resolvedClient, session: resolvedSession });
|
|
428
|
+
if (identified.verified) query.set('version', String(identified.version));
|
|
485
429
|
const run = await apiRequest(config, `/skill-reports?${query.toString()}`, {
|
|
486
430
|
method: 'POST',
|
|
487
431
|
headers: { 'Content-Type': mediaType(transcript.absolute), 'X-Transcript-Filename': transcript.filename },
|
|
@@ -491,6 +435,10 @@ async function reportCommand(args, config, out) {
|
|
|
491
435
|
run_id: run.run_id,
|
|
492
436
|
skill: run.skill_slug,
|
|
493
437
|
version: run.skill_version,
|
|
438
|
+
version_verified: identified.verified,
|
|
439
|
+
...(identified.localPath && !identified.verified
|
|
440
|
+
? { warning: `local copy at ${identified.localPath} does not match any published version` }
|
|
441
|
+
: {}),
|
|
494
442
|
state: run.state,
|
|
495
443
|
client: run.client,
|
|
496
444
|
session: run.client_session_id,
|
|
@@ -501,7 +449,6 @@ async function reportCommand(args, config, out) {
|
|
|
501
449
|
async function runList(args, config, out) {
|
|
502
450
|
const query = new URLSearchParams();
|
|
503
451
|
for (const value of takeOption(args, '--team', { multiple: true })) query.append('team', value);
|
|
504
|
-
for (const value of takeOption(args, '--user', { multiple: true })) query.append('user', value);
|
|
505
452
|
for (const value of takeOption(args, '--skill', { multiple: true })) query.append('skill', value);
|
|
506
453
|
for (const name of ['--after', '--before', '--limit', '--cursor']) {
|
|
507
454
|
const value = takeOption(args, name);
|
|
@@ -526,9 +473,7 @@ async function runGet(args, config, out) {
|
|
|
526
473
|
const raw = takeFlag(args, '--raw');
|
|
527
474
|
const output = takeOption(args, '--output');
|
|
528
475
|
rejectUnknown(args);
|
|
529
|
-
if (
|
|
530
|
-
if (output && args.length !== 1) throw new Error('--output accepts exactly one run ID');
|
|
531
|
-
if (raw && args.length !== 1) throw new Error('--raw accepts exactly one run ID');
|
|
476
|
+
if (args.length !== 1) throw new Error('Usage: constraint skills run get <run-id> [--raw] [--output <path>]');
|
|
532
477
|
if (raw) {
|
|
533
478
|
const response = await apiRequest(config, `/skill-runs/${encodeURIComponent(args[0])}/transcript`, { rawResponse: true });
|
|
534
479
|
const content = Buffer.from(await response.arrayBuffer());
|
|
@@ -537,25 +482,21 @@ async function runGet(args, config, out) {
|
|
|
537
482
|
out.data({ run_id: args[0], bytes: content.length, path: destination });
|
|
538
483
|
return;
|
|
539
484
|
}
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
path: destination,
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
out.data(summaries.length === 1 ? summaries[0] : summaries);
|
|
485
|
+
const run = await apiRequest(config, `/skill-runs/${encodeURIComponent(args[0])}`);
|
|
486
|
+
const destination = await runGetDestination(output, args[0], '.json');
|
|
487
|
+
await fs.writeFile(destination, `${JSON.stringify(run, null, 2)}\n`);
|
|
488
|
+
const events = Array.isArray(run.normalized_events) ? run.normalized_events : [];
|
|
489
|
+
out.data({
|
|
490
|
+
run_id: run.run_id,
|
|
491
|
+
skill: run.skill_slug,
|
|
492
|
+
version: run.skill_version,
|
|
493
|
+
state: run.state,
|
|
494
|
+
client: run.client,
|
|
495
|
+
completed_at: run.completed_at,
|
|
496
|
+
events: events.length,
|
|
497
|
+
events_with_text: events.filter((event) => event.content).length,
|
|
498
|
+
path: destination,
|
|
499
|
+
});
|
|
559
500
|
}
|
|
560
501
|
|
|
561
502
|
async function runCommand(args, config, out) {
|
|
@@ -593,7 +534,6 @@ export async function main(argv) {
|
|
|
593
534
|
const command = args.shift();
|
|
594
535
|
const config = await loadConfig();
|
|
595
536
|
if (['login', 'logout', 'whoami'].includes(command)) return authCommand(command, args, config, out);
|
|
596
|
-
if (command === 'setup') return setupCommand(args, out);
|
|
597
537
|
if (command === 'doctor') return doctorCommand(args, config, out);
|
|
598
538
|
if (command === 'report') return reportCommand(args, config, out);
|
|
599
539
|
if (command === 'skills') return skillsCommand(args, config, out);
|
package/src/config.js
CHANGED
|
@@ -28,9 +28,6 @@ export async function loadConfig() {
|
|
|
28
28
|
return {
|
|
29
29
|
apiUrl: (process.env.CONSTRAINT_API_URL || saved.apiUrl || DEFAULT_API_URL).replace(/\/$/, ''),
|
|
30
30
|
agents: Array.isArray(saved.agents) ? saved.agents : Array.isArray(saved.clients) ? saved.clients : [],
|
|
31
|
-
companion_hashes: saved.companion_hashes && typeof saved.companion_hashes === 'object'
|
|
32
|
-
? saved.companion_hashes
|
|
33
|
-
: {},
|
|
34
31
|
};
|
|
35
32
|
}
|
|
36
33
|
|
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/prompts.js
CHANGED
|
@@ -31,20 +31,10 @@ export async function chooseAgents(initial = []) {
|
|
|
31
31
|
|
|
32
32
|
export async function chooseScope() {
|
|
33
33
|
return cancelled(await p.select({
|
|
34
|
-
message: 'Where should
|
|
34
|
+
message: 'Where should this skill be available?',
|
|
35
35
|
options: [
|
|
36
|
-
{ value: 'project', label: '
|
|
37
|
-
{ value: 'global', label: '
|
|
38
|
-
],
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function chooseMode() {
|
|
43
|
-
return cancelled(await p.select({
|
|
44
|
-
message: 'How should agent directories reference the package?',
|
|
45
|
-
options: [
|
|
46
|
-
{ value: 'symlink', label: 'Shared package', hint: 'recommended; one managed copy' },
|
|
47
|
-
{ value: 'copy', label: 'Copy files', hint: 'use when links are unavailable' },
|
|
36
|
+
{ value: 'project', label: 'Only in this project' },
|
|
37
|
+
{ value: 'global', label: 'Everywhere on this computer' },
|
|
48
38
|
],
|
|
49
39
|
}));
|
|
50
40
|
}
|
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) };
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: constraint-skills
|
|
3
|
-
description: Operate governed Agent Skills with the Constraint terminal CLI. Use when an agent needs to authenticate or diagnose Constraint access; discover upload domains; inspect, install, update, or upload skills; review run history; or report a session transcript when the user asks to put skill work on the record.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Constraint Skills
|
|
7
|
-
|
|
8
|
-
Use the `constraint` command for organization-authorized skills. Do not edit its
|
|
9
|
-
config, lockfiles, installation receipts, or authorization tokens directly.
|
|
10
|
-
|
|
11
|
-
## Choose the operation
|
|
12
|
-
|
|
13
|
-
- Authenticate or diagnose: `constraint login`, `constraint whoami`, `constraint doctor`.
|
|
14
|
-
- Discover or inspect skills: `constraint skills list`, `constraint skills inspect`.
|
|
15
|
-
- Install or update: `constraint skills install`, `constraint skills update`.
|
|
16
|
-
- Discover upload destinations: `constraint skills domains --json`.
|
|
17
|
-
- Upload a local skill folder: `constraint skills upload`.
|
|
18
|
-
- Report a session: `constraint report`.
|
|
19
|
-
- Review runs: `constraint skills run list`, `constraint skills run get`.
|
|
20
|
-
|
|
21
|
-
Read [references/commands.md](references/commands.md) before constructing flags
|
|
22
|
-
or handling a less common operation.
|
|
23
|
-
|
|
24
|
-
## Upload skills
|
|
25
|
-
|
|
26
|
-
Upload the skill directory, not an individual `SKILL.md`. Before every upload, run:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
constraint skills domains --json
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Use one returned `slug` exactly as `--domain`. Never guess a slug from a display
|
|
33
|
-
name or reuse a domain from another organization. If multiple destinations are
|
|
34
|
-
valid and ownership is unclear, ask the user.
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
constraint skills upload <skill-directory> --domain <returned-slug>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Install skills
|
|
41
|
-
|
|
42
|
-
Choose the target agent and scope explicitly. Prefer `--project`; use
|
|
43
|
-
`--global` only when the user wants the skill across projects. Prefer managed
|
|
44
|
-
links and use `--copy` only when independent files are required. Never
|
|
45
|
-
overwrite locally edited installed skills.
|
|
46
|
-
|
|
47
|
-
## Report a run
|
|
48
|
-
|
|
49
|
-
A run is the posting of a transcript in which a skill was invoked, and the user
|
|
50
|
-
drives it: report only when the user asks to put a session on the record. Never
|
|
51
|
-
report automatically or unprompted.
|
|
52
|
-
|
|
53
|
-
When asked, locate the current session's real JSON or JSONL transcript file and run:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
constraint report <transcript-path> --skill <skill-name>
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Pass the explicit local path to the transcript as it exists at that moment.
|
|
60
|
-
Use the invoked skill's frontmatter `name`. Client and session id are read
|
|
61
|
-
from the transcript path; re-reporting the same session later supersedes the
|
|
62
|
-
earlier report. Never create a replacement transcript, summarize it, copy it
|
|
63
|
-
elsewhere first, or pass a temporary file. If the transcript path cannot be
|
|
64
|
-
determined, say so instead of guessing.
|
|
65
|
-
|
|
66
|
-
## Work safely
|
|
67
|
-
|
|
68
|
-
- Use `--json` when parsing output.
|
|
69
|
-
- Respect WorkOS authorization failures; never bypass domain permissions.
|
|
70
|
-
- Do not invent an uninstall workflow.
|
|
71
|
-
- After mutations, report the affected skill, version, agent, scope, domain, or run ID as applicable.
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Constraint CLI command reference
|
|
2
|
-
|
|
3
|
-
## Install, authenticate, and configure
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npm install --global @constraint/cli
|
|
7
|
-
constraint --version
|
|
8
|
-
constraint login
|
|
9
|
-
constraint whoami
|
|
10
|
-
constraint logout
|
|
11
|
-
constraint setup
|
|
12
|
-
constraint doctor
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
`login` uses WorkOS device authorization. Never request or embed an API key or client secret. `setup` detects Codex and Claude Code and installs this skill globally. It never touches instruction files.
|
|
16
|
-
|
|
17
|
-
## Discover, inspect, install, and update
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
constraint skills list
|
|
21
|
-
constraint skills list --search <text>
|
|
22
|
-
constraint skills inspect <skill> [--version <number>] [--files]
|
|
23
|
-
|
|
24
|
-
constraint skills install <skill> --agent codex --project
|
|
25
|
-
constraint skills install <skill> --agent claude-code --global
|
|
26
|
-
constraint skills list --installed --project
|
|
27
|
-
constraint skills update <skill> --project
|
|
28
|
-
constraint skills update --all --global
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
The catalog is already authorization-filtered. Project installs create `skills-lock.json`. Global receipts live in Constraint's per-user configuration directory.
|
|
32
|
-
|
|
33
|
-
## Discover upload domains and upload
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
constraint skills domains
|
|
37
|
-
constraint skills domains --json
|
|
38
|
-
constraint skills upload <skill-directory> --domain <returned-slug>
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
`skills domains` returns only domains where the authenticated user can manage skills. JSON objects contain the exact `slug`, display `name`, and `description`. Discover first and pass one returned slug. Ask the user when multiple domains fit and ownership is unclear.
|
|
42
|
-
|
|
43
|
-
## Report and review runs
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
constraint report <actual-transcript-path> --skill <skill-name>
|
|
47
|
-
constraint report <actual-transcript-path> --skill <skill-name> --client codex --session <id>
|
|
48
|
-
|
|
49
|
-
constraint skills run list
|
|
50
|
-
constraint skills run list --team <domain> --user <workos-user-id> --skill <skill>
|
|
51
|
-
constraint skills run get <run-id>
|
|
52
|
-
constraint skills run get <run-id> --raw
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`report` is user-driven: run it only when the user asks to put a session on the
|
|
56
|
-
record. Client and session id are read from the transcript path itself (Claude
|
|
57
|
-
Code project transcripts and Codex rollouts are named by session id);
|
|
58
|
-
`--client`/`--session` are only needed for files outside those locations.
|
|
59
|
-
Re-reporting the same session supersedes the earlier report.
|
|
60
|
-
|
|
61
|
-
Repeat `--team`, `--user`, or `--skill` to request subsets. Review visibility is permission-controlled.
|
|
62
|
-
|
|
63
|
-
`run get` never prints a transcript to stdout: it writes the run JSON (or the
|
|
64
|
-
original transcript file with `--raw`) under the system temp directory and
|
|
65
|
-
prints a one-object summary containing the file `path`. Read the file from
|
|
66
|
-
that path. Pass `--output <path>` to choose the destination (single run only).
|
|
67
|
-
|
|
68
|
-
## Automation and recovery
|
|
69
|
-
|
|
70
|
-
Global flags: `--json`, `--quiet`, `--no-color`, `--help`, and `--version`. Installer workflows also accept `--yes`.
|
|
71
|
-
|
|
72
|
-
- Authentication failure: run `constraint login`, then `constraint whoami`.
|
|
73
|
-
- Setup uncertainty: run `constraint doctor`.
|
|
74
|
-
- Upload denied: rerun `constraint skills domains --json` and respect the returned scope.
|
|
75
|
-
- Install conflict: preserve local edits.
|
|
76
|
-
- Missing transcript: locate the real client JSON or JSONL file before reporting; if it cannot be found, tell the user instead of guessing.
|
package/src/setup.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
import { loadConfig, saveConfig } from './config.js';
|
|
6
|
-
import { packageDigest, readSkillDirectory, writeSkillDirectory } from './files.js';
|
|
7
|
-
import { agentPaths, normalizeAgent } from './paths.js';
|
|
8
|
-
|
|
9
|
-
async function companionFiles() {
|
|
10
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
return readSkillDirectory(path.resolve(here, '..', 'skills', 'constraint-skills'));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function expectedCompanionHash(config, selected, currentHash) {
|
|
15
|
-
const direct = config.companion_hashes?.[selected] || null;
|
|
16
|
-
if (direct) return direct;
|
|
17
|
-
const recorded = Object.values(config.companion_hashes || {});
|
|
18
|
-
return currentHash && recorded.includes(currentHash) ? currentHash : null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function skillPlan(selected, paths) {
|
|
22
|
-
const files = await companionFiles();
|
|
23
|
-
const bundledHash = packageDigest(files);
|
|
24
|
-
const config = await loadConfig();
|
|
25
|
-
const target = path.join(paths.skills, 'constraint-skills');
|
|
26
|
-
let currentHash = null;
|
|
27
|
-
try {
|
|
28
|
-
currentHash = packageDigest(await readSkillDirectory(target));
|
|
29
|
-
} catch (error) {
|
|
30
|
-
if (error && error.code !== 'ENOENT') throw error;
|
|
31
|
-
}
|
|
32
|
-
const expectedHash = expectedCompanionHash(config, selected, currentHash);
|
|
33
|
-
const action = currentHash === bundledHash
|
|
34
|
-
? 'current'
|
|
35
|
-
: currentHash === null
|
|
36
|
-
? 'install'
|
|
37
|
-
: expectedHash && currentHash === expectedHash
|
|
38
|
-
? 'update'
|
|
39
|
-
: 'conflict';
|
|
40
|
-
return { path: target, action, bundledHash, expectedHash };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function setupPlan(agent) {
|
|
44
|
-
const selected = normalizeAgent(agent);
|
|
45
|
-
const paths = agentPaths(selected);
|
|
46
|
-
return {
|
|
47
|
-
agent: selected,
|
|
48
|
-
skill: await skillPlan(selected, paths),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function recordConfiguredAgent(selected, config = null) {
|
|
53
|
-
const next = config || await loadConfig();
|
|
54
|
-
next.agents = [...new Set([...next.agents.map(normalizeAgent), selected])];
|
|
55
|
-
await saveConfig(next);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function installCompanionSkill(agent) {
|
|
59
|
-
const selected = normalizeAgent(agent);
|
|
60
|
-
const paths = agentPaths(selected);
|
|
61
|
-
const files = await companionFiles();
|
|
62
|
-
const config = await loadConfig();
|
|
63
|
-
config.companion_hashes ||= {};
|
|
64
|
-
const target = path.join(paths.skills, 'constraint-skills');
|
|
65
|
-
const plan = await skillPlan(selected, paths);
|
|
66
|
-
const result = await writeSkillDirectory(target, files, {
|
|
67
|
-
expectedHash: plan.expectedHash,
|
|
68
|
-
});
|
|
69
|
-
config.companion_hashes[selected] = result.hash;
|
|
70
|
-
await recordConfiguredAgent(selected, config);
|
|
71
|
-
return { agent: selected, path: target, changed: result.changed, applied: true };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function setupAgent(agent, {
|
|
75
|
-
dryRun = false,
|
|
76
|
-
output = console.log,
|
|
77
|
-
} = {}) {
|
|
78
|
-
const selected = normalizeAgent(agent);
|
|
79
|
-
const plan = await setupPlan(selected);
|
|
80
|
-
if (dryRun) {
|
|
81
|
-
output(`Install constraint-skills globally at ${plan.skill.path} (${plan.skill.action}).`);
|
|
82
|
-
return { agent: selected, skill: plan.skill, applied: false };
|
|
83
|
-
}
|
|
84
|
-
const skillResult = await installCompanionSkill(selected);
|
|
85
|
-
return { agent: selected, skill: skillResult, applied: true };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function setupStatus(agent) {
|
|
89
|
-
const selected = normalizeAgent(agent);
|
|
90
|
-
const paths = agentPaths(selected);
|
|
91
|
-
try {
|
|
92
|
-
const skill = await fs.stat(path.join(paths.skills, 'constraint-skills', 'SKILL.md'));
|
|
93
|
-
return { agent: selected, skill: skill.isFile() };
|
|
94
|
-
} catch {
|
|
95
|
-
return { agent: selected, skill: false };
|
|
96
|
-
}
|
|
97
|
-
}
|