@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constraint/cli",
3
- "version": "0.4.1",
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.1';
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>]... [--user <user>]... [--skill <skill>]...
58
- constraint skills run get <run-id>... [--raw] [--output <path>] (always writes a file, prints summary + path)
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, { installed = false, search = null, scope = 'project' } = {}) {
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} (${item.slug}) · v${item.latest_version} · ${humanize(item.domain)}`,
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('Upload has more than one permitted domain and requires an interactive terminal.');
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
- out.data(await apiRequest(config, '/me'));
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
- spinner.stop('Constraint Skills installed globally.');
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
- out.data({
308
- api_url: config.apiUrl,
309
- authenticated: Boolean(identity),
310
- identity,
311
- auth_error: authError,
312
- agents: await Promise.all(agents.map(setupStatus)),
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'}: ${args[0]}${version ? ` v${version}` : ' (latest)'}`,
371
- `Agents: ${agents.join(', ')}`,
372
- `Scope: ${scope}${root ? ` (${root})` : ''}`,
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 (!args.length) throw new Error('Usage: constraint skills run get <run-id>...');
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 summaries = [];
541
- for (const runId of args) {
542
- const run = await apiRequest(config, `/skill-runs/${encodeURIComponent(runId)}`);
543
- const destination = await runGetDestination(output, runId, '.json');
544
- await fs.writeFile(destination, `${JSON.stringify(run, null, 2)}\n`);
545
- const events = Array.isArray(run.normalized_events) ? run.normalized_events : [];
546
- summaries.push({
547
- run_id: run.run_id,
548
- skill: run.skill_slug,
549
- version: run.skill_version,
550
- state: run.state,
551
- client: run.client,
552
- completed_at: run.completed_at,
553
- events: events.length,
554
- events_with_text: events.filter((event) => event.content).length,
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
- for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
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 the skill be installed?',
34
+ message: 'Where should this skill be available?',
35
35
  options: [
36
- { value: 'project', label: 'This project', hint: 'portable lockfile, repository-local skill' },
37
- { value: 'global', label: 'Globally', hint: 'available in every project for this user' },
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,4 +0,0 @@
1
- interface:
2
- display_name: "Constraint Skills"
3
- short_description: "Operate and report governed agent skills"
4
- default_prompt: "Use $constraint-skills to manage a governed skill and report its run."
@@ -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
- }