@constraint/cli 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/constraint-skills/SKILL.md +25 -25
- package/skills/constraint-skills/references/commands.md +10 -5
- package/src/cli.js +31 -28
- package/src/setup.js +13 -18
- package/src/skills.js +30 -0
package/package.json
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: constraint-skills
|
|
3
|
-
description: Operate governed Agent Skills with the Constraint terminal CLI
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Constraint Skills
|
|
7
7
|
|
|
8
|
-
Use the `constraint` command for organization-authorized skills. Do not edit its
|
|
8
|
+
Use the `constraint` command for organization-authorized skills. Do not edit its
|
|
9
|
+
config, lockfiles, installation receipts, or authorization tokens directly.
|
|
9
10
|
|
|
10
11
|
## Choose the operation
|
|
11
12
|
|
|
12
13
|
- Authenticate or diagnose: `constraint login`, `constraint whoami`, `constraint doctor`.
|
|
13
|
-
- Configure agents: `constraint setup`.
|
|
14
14
|
- Discover or inspect skills: `constraint skills list`, `constraint skills inspect`.
|
|
15
15
|
- Install or update: `constraint skills install`, `constraint skills update`.
|
|
16
16
|
- Discover upload destinations: `constraint skills domains --json`.
|
|
17
17
|
- Upload a local skill folder: `constraint skills upload`.
|
|
18
|
+
- Report a session: `constraint report`.
|
|
18
19
|
- Review runs: `constraint skills run list`, `constraint skills run get`.
|
|
19
20
|
|
|
20
|
-
Read [references/commands.md](references/commands.md) before constructing flags
|
|
21
|
+
Read [references/commands.md](references/commands.md) before constructing flags
|
|
22
|
+
or handling a less common operation.
|
|
21
23
|
|
|
22
24
|
## Upload skills
|
|
23
25
|
|
|
@@ -27,7 +29,9 @@ Upload the skill directory, not an individual `SKILL.md`. Before every upload, r
|
|
|
27
29
|
constraint skills domains --json
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
Use one returned `slug` exactly as `--domain`. Never guess a slug from a display
|
|
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.
|
|
31
35
|
|
|
32
36
|
```bash
|
|
33
37
|
constraint skills upload <skill-directory> --domain <returned-slug>
|
|
@@ -35,33 +39,29 @@ constraint skills upload <skill-directory> --domain <returned-slug>
|
|
|
35
39
|
|
|
36
40
|
## Install skills
|
|
37
41
|
|
|
38
|
-
Choose the target agent and scope explicitly. Prefer `--project`; use
|
|
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.
|
|
39
46
|
|
|
40
|
-
##
|
|
47
|
+
## Report a run
|
|
41
48
|
|
|
42
|
-
|
|
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.
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
constraint skills run start <skill-name> --client <client> --session <session-id>
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Use the invoked skill's frontmatter `name`. Normalize the client to `codex` or `claude-code`. Use the current client session identifier. Retain the exact run ID printed by the command. If one session invokes multiple skills, start and retain a distinct run ID for each invocation.
|
|
49
|
-
|
|
50
|
-
If the command fails, report the failure briefly and continue the user's work. Do not fabricate a run ID.
|
|
51
|
-
|
|
52
|
-
## Complete a run
|
|
53
|
-
|
|
54
|
-
After finishing the governed skill's work and before ending the response, locate the current session's real JSON or JSONL transcript file and run:
|
|
53
|
+
When asked, locate the current session's real JSON or JSONL transcript file and run:
|
|
55
54
|
|
|
56
55
|
```bash
|
|
57
|
-
constraint
|
|
56
|
+
constraint report <transcript-path> --skill <skill-name>
|
|
58
57
|
```
|
|
59
58
|
|
|
60
|
-
Pass the explicit local path
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
Pass the explicit local path to the transcript as it exists at that moment.
|
|
60
|
+
Use the invoked skill's frontmatter `name`. The server detects the client and
|
|
61
|
+
session id from the transcript itself; re-reporting the same session later
|
|
62
|
+
supersedes the earlier report. Never create a replacement transcript,
|
|
63
|
+
summarize it, or pass a temporary file. If the transcript path cannot be
|
|
64
|
+
determined, say so instead of guessing.
|
|
65
65
|
|
|
66
66
|
## Work safely
|
|
67
67
|
|
|
@@ -12,7 +12,7 @@ constraint setup
|
|
|
12
12
|
constraint doctor
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`login` uses WorkOS device authorization. Never request or embed an API key or client secret. `setup` detects Codex and Claude Code,
|
|
15
|
+
`login` uses WorkOS device authorization. Never request or embed an API key or client secret. `setup` detects Codex and Claude Code, installs this skill globally, and removes the legacy managed block that earlier releases injected into global instruction files. It never adds to or replaces user instructions.
|
|
16
16
|
|
|
17
17
|
## Discover, inspect, install, and update
|
|
18
18
|
|
|
@@ -40,11 +40,11 @@ constraint skills upload <skill-directory> --domain <returned-slug>
|
|
|
40
40
|
|
|
41
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
42
|
|
|
43
|
-
##
|
|
43
|
+
## Report and review runs
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
constraint
|
|
47
|
-
constraint
|
|
46
|
+
constraint report <actual-transcript-path> --skill <skill-name>
|
|
47
|
+
constraint report <actual-transcript-path> --skill <skill-name> --client codex --session <id>
|
|
48
48
|
|
|
49
49
|
constraint skills run list
|
|
50
50
|
constraint skills run list --team <domain> --user <workos-user-id> --skill <skill>
|
|
@@ -52,6 +52,11 @@ constraint skills run get <run-id>
|
|
|
52
52
|
constraint skills run get <run-id> --raw
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
`report` is user-driven: run it only when the user asks to put a session on the
|
|
56
|
+
record. The server detects client and session id from the transcript;
|
|
57
|
+
`--client`/`--session` are overrides for transcripts it cannot parse.
|
|
58
|
+
Re-reporting the same session supersedes the earlier report.
|
|
59
|
+
|
|
55
60
|
Repeat `--team`, `--user`, or `--skill` to request subsets. Review visibility is permission-controlled.
|
|
56
61
|
|
|
57
62
|
`run get` never prints a transcript to stdout: it writes the run JSON (or the
|
|
@@ -67,4 +72,4 @@ Global flags: `--json`, `--quiet`, `--no-color`, `--help`, and `--version`. Inst
|
|
|
67
72
|
- Setup uncertainty: run `constraint doctor`.
|
|
68
73
|
- Upload denied: rerun `constraint skills domains --json` and respect the returned scope.
|
|
69
74
|
- Install conflict: preserve local edits.
|
|
70
|
-
- Missing transcript: locate the real client JSON or JSONL file before
|
|
75
|
+
- 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/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
uploadSkill,
|
|
37
37
|
} from './skills.js';
|
|
38
38
|
|
|
39
|
-
const VERSION = '0.
|
|
39
|
+
const VERSION = '0.4.0';
|
|
40
40
|
|
|
41
41
|
const HELP = `Constraint Skills CLI
|
|
42
42
|
|
|
@@ -54,8 +54,7 @@ Usage:
|
|
|
54
54
|
[--copy] [--yes]
|
|
55
55
|
constraint skills update --all [--agent <agent>]... [--project|--global] [--yes]
|
|
56
56
|
constraint skills upload <path> --domain <domain>
|
|
57
|
-
constraint
|
|
58
|
-
constraint skills run complete <run-id> <transcript-path>
|
|
57
|
+
constraint report <transcript-path> --skill <skill> [--client <client>] [--session <id>]
|
|
59
58
|
constraint skills run list [--team <team>]... [--user <user>]... [--skill <skill>]...
|
|
60
59
|
constraint skills run get <run-id>... [--raw] [--output <path>] (always writes a file, prints summary + path)
|
|
61
60
|
|
|
@@ -269,10 +268,10 @@ async function setupCommand(args, out) {
|
|
|
269
268
|
let installSkill = true;
|
|
270
269
|
if (useUi) {
|
|
271
270
|
note(
|
|
272
|
-
plans.map((plan) => `${humanize(plan.agent)}\n${plan.instructions.path}\n${plan.instructions.changed ? '
|
|
273
|
-
'1.
|
|
271
|
+
plans.map((plan) => `${humanize(plan.agent)}\n${plan.instructions.path}\n${plan.instructions.changed ? 'Legacy Constraint block will be removed' : 'Nothing to clean up'}`).join('\n\n'),
|
|
272
|
+
'1. Remove legacy instruction blocks',
|
|
274
273
|
);
|
|
275
|
-
configureInstructions = !hasInstructionChanges || yes || await confirmPlan('
|
|
274
|
+
configureInstructions = !hasInstructionChanges || yes || await confirmPlan('Remove the legacy Constraint block from these files?');
|
|
276
275
|
|
|
277
276
|
note(
|
|
278
277
|
[
|
|
@@ -294,10 +293,10 @@ async function setupCommand(args, out) {
|
|
|
294
293
|
if (configureInstructions) {
|
|
295
294
|
const spinner = taskSpinner(useUi && hasInstructionChanges);
|
|
296
295
|
for (const plan of plans) {
|
|
297
|
-
spinner.message(`
|
|
296
|
+
spinner.message(`Cleaning ${humanize(plan.agent)} instructions`);
|
|
298
297
|
result.instructions.push(await applyInstructionSetup(plan.agent));
|
|
299
298
|
}
|
|
300
|
-
spinner.stop('Agent instructions
|
|
299
|
+
spinner.stop('Agent instructions cleaned.');
|
|
301
300
|
}
|
|
302
301
|
if (installSkill) {
|
|
303
302
|
const spinner = taskSpinner(useUi && hasSkillChanges);
|
|
@@ -471,31 +470,36 @@ async function uploadCommand(args, config, out) {
|
|
|
471
470
|
out.data({ skill_id: detail.skill_id, slug: detail.slug, domain: detail.domain, version: detail.version.version });
|
|
472
471
|
}
|
|
473
472
|
|
|
474
|
-
|
|
473
|
+
// A run is the posting of a transcript in which a skill was invoked. The user
|
|
474
|
+
// drives reporting: one command, after the session, with the finished
|
|
475
|
+
// transcript. Client and session id are detected server-side from the
|
|
476
|
+
// transcript; flags override when detection cannot.
|
|
477
|
+
async function reportCommand(args, config, out) {
|
|
478
|
+
const skill = takeOption(args, '--skill');
|
|
475
479
|
const client = takeOption(args, '--client');
|
|
476
480
|
const session = takeOption(args, '--session');
|
|
477
481
|
rejectUnknown(args);
|
|
478
|
-
if (args.length !== 1 || !
|
|
479
|
-
throw new Error('Usage: constraint
|
|
482
|
+
if (args.length !== 1 || !skill) {
|
|
483
|
+
throw new Error('Usage: constraint report <transcript-path> --skill <skill> [--client <client>] [--session <session-id>]');
|
|
480
484
|
}
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
else out.line(run.run_id);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
async function runComplete(args, config, out) {
|
|
490
|
-
rejectUnknown(args);
|
|
491
|
-
if (args.length !== 2) throw new Error('Usage: constraint skills run complete <run-id> <transcript-path>');
|
|
492
|
-
const transcript = await readTranscript(args[1]);
|
|
493
|
-
const run = await apiRequest(config, `/skill-runs/${encodeURIComponent(args[0])}/complete`, {
|
|
485
|
+
const transcript = await readTranscript(args[0]);
|
|
486
|
+
const query = new URLSearchParams({ skill });
|
|
487
|
+
if (client) query.set('client', client);
|
|
488
|
+
if (session) query.set('session', session);
|
|
489
|
+
const run = await apiRequest(config, `/skill-reports?${query.toString()}`, {
|
|
494
490
|
method: 'POST',
|
|
495
491
|
headers: { 'Content-Type': mediaType(transcript.absolute), 'X-Transcript-Filename': transcript.filename },
|
|
496
492
|
body: transcript.content,
|
|
497
493
|
});
|
|
498
|
-
out.data(
|
|
494
|
+
out.data({
|
|
495
|
+
run_id: run.run_id,
|
|
496
|
+
skill: run.skill_slug,
|
|
497
|
+
version: run.skill_version,
|
|
498
|
+
state: run.state,
|
|
499
|
+
client: run.client,
|
|
500
|
+
session: run.client_session_id,
|
|
501
|
+
transcript_bytes: run.transcript_size,
|
|
502
|
+
});
|
|
499
503
|
}
|
|
500
504
|
|
|
501
505
|
async function runList(args, config, out) {
|
|
@@ -560,11 +564,9 @@ async function runGet(args, config, out) {
|
|
|
560
564
|
|
|
561
565
|
async function runCommand(args, config, out) {
|
|
562
566
|
const command = args.shift();
|
|
563
|
-
if (command === 'start') return runStart(args, config, out);
|
|
564
|
-
if (command === 'complete') return runComplete(args, config, out);
|
|
565
567
|
if (command === 'list') return runList(args, config, out);
|
|
566
568
|
if (command === 'get') return runGet(args, config, out);
|
|
567
|
-
throw new Error('Expected skills run
|
|
569
|
+
throw new Error('Expected skills run list or get. Post a transcript with constraint report.');
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
async function skillsCommand(args, config, out) {
|
|
@@ -597,6 +599,7 @@ export async function main(argv) {
|
|
|
597
599
|
if (['login', 'logout', 'whoami'].includes(command)) return authCommand(command, args, config, out);
|
|
598
600
|
if (command === 'setup') return setupCommand(args, out);
|
|
599
601
|
if (command === 'doctor') return doctorCommand(args, config, out);
|
|
602
|
+
if (command === 'report') return reportCommand(args, config, out);
|
|
600
603
|
if (command === 'skills') return skillsCommand(args, config, out);
|
|
601
604
|
throw new Error(`Unknown command: ${command}`);
|
|
602
605
|
}
|
package/src/setup.js
CHANGED
|
@@ -9,25 +9,20 @@ import { agentPaths, normalizeAgent } from './paths.js';
|
|
|
9
9
|
export const BLOCK_START = '<!-- constraint-skills:start -->';
|
|
10
10
|
export const BLOCK_END = '<!-- constraint-skills:end -->';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
follow the global \`constraint-skills\` companion skill. It contains the CLI
|
|
17
|
-
workflow and records governed runs without modifying the invoked skill.
|
|
18
|
-
${BLOCK_END}`;
|
|
19
|
-
|
|
20
|
-
export function mergeInstructionBlock(existing) {
|
|
12
|
+
// Earlier releases injected a managed instruction block into the user's global
|
|
13
|
+
// CLAUDE.md/AGENTS.md. Writing into user-owned instruction files was never
|
|
14
|
+
// trustworthy behavior; setup now only removes that legacy block if present.
|
|
15
|
+
export function stripInstructionBlock(existing) {
|
|
21
16
|
const start = existing.indexOf(BLOCK_START);
|
|
22
17
|
const end = existing.indexOf(BLOCK_END);
|
|
18
|
+
if (start < 0 && end < 0) return existing;
|
|
23
19
|
if ((start >= 0) !== (end >= 0)) {
|
|
24
20
|
throw new Error('The existing Constraint Skills instruction block is incomplete; fix it before running setup.');
|
|
25
21
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return `${existing}${separator}${BLOCK}\n`;
|
|
22
|
+
const before = existing.slice(0, start).replace(/\n+$/, '\n');
|
|
23
|
+
const after = existing.slice(end + BLOCK_END.length).replace(/^\n+/, '\n');
|
|
24
|
+
const stripped = `${before}${after}`;
|
|
25
|
+
return stripped.trim() === '' ? '' : stripped.replace(/\n{3,}/g, '\n\n');
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
async function writeInstructions(file, content, mode) {
|
|
@@ -58,7 +53,7 @@ async function instructionPlan(selected, paths) {
|
|
|
58
53
|
} catch (error) {
|
|
59
54
|
if (error && error.code !== 'ENOENT') throw error;
|
|
60
55
|
}
|
|
61
|
-
const content =
|
|
56
|
+
const content = stripInstructionBlock(existing);
|
|
62
57
|
return { path: paths.instructions, changed: content !== existing, content, mode };
|
|
63
58
|
}
|
|
64
59
|
|
|
@@ -145,7 +140,7 @@ export async function setupAgent(agent, {
|
|
|
145
140
|
let skillResult = null;
|
|
146
141
|
if (instructions) {
|
|
147
142
|
const approved = !plan.instructions.changed || yes || await confirmChange(
|
|
148
|
-
`
|
|
143
|
+
`Remove the legacy Constraint block from ${plan.instructions.path}?`,
|
|
149
144
|
);
|
|
150
145
|
if (approved) instructionResult = await applyInstructionSetup(selected);
|
|
151
146
|
}
|
|
@@ -161,8 +156,8 @@ export async function setupStatus(agent) {
|
|
|
161
156
|
fs.readFile(paths.instructions, 'utf8'),
|
|
162
157
|
fs.stat(path.join(paths.skills, 'constraint-skills', 'SKILL.md')),
|
|
163
158
|
]);
|
|
164
|
-
return { agent: selected,
|
|
159
|
+
return { agent: selected, legacy_instruction_block: instructions.includes(BLOCK_START), skill: skill.isFile() };
|
|
165
160
|
} catch {
|
|
166
|
-
return { agent: selected,
|
|
161
|
+
return { agent: selected, legacy_instruction_block: false, skill: false };
|
|
167
162
|
}
|
|
168
163
|
}
|
package/src/skills.js
CHANGED
|
@@ -45,6 +45,35 @@ async function loadLock(scope, projectRoot) {
|
|
|
45
45
|
return scope === 'global' ? loadInstallations() : loadProjectInstallations(projectRoot);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Installed skill files are per-machine (symlinks into the canonical store);
|
|
49
|
+
// only skills-lock.json belongs in version control. Keep the project's
|
|
50
|
+
// .gitignore covering the generated paths, npm-style.
|
|
51
|
+
const GITIGNORE_ENTRIES = ['.constraint/', '.claude/skills/', '.agents/skills/'];
|
|
52
|
+
|
|
53
|
+
async function ensureProjectGitignore(projectRoot) {
|
|
54
|
+
try {
|
|
55
|
+
await fs.stat(path.join(projectRoot, '.git'));
|
|
56
|
+
} catch {
|
|
57
|
+
return; // not a git repository; nothing to manage
|
|
58
|
+
}
|
|
59
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
60
|
+
let existing = '';
|
|
61
|
+
try {
|
|
62
|
+
existing = await fs.readFile(gitignorePath, 'utf8');
|
|
63
|
+
} catch {
|
|
64
|
+
existing = '';
|
|
65
|
+
}
|
|
66
|
+
const lines = new Set(existing.split('\n').map((line) => line.trim()));
|
|
67
|
+
const missing = GITIGNORE_ENTRIES.filter(
|
|
68
|
+
(entry) => !lines.has(entry) && !lines.has(entry.slice(0, -1)),
|
|
69
|
+
);
|
|
70
|
+
if (!missing.length) return;
|
|
71
|
+
const block = `${missing.join('\n')}\n`;
|
|
72
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
73
|
+
const header = existing ? '\n# Constraint skill installs (commit skills-lock.json instead)\n' : '# Constraint skill installs (commit skills-lock.json instead)\n';
|
|
74
|
+
await fs.writeFile(gitignorePath, `${existing}${prefix}${header}${block}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
async function saveLock(scope, projectRoot, value) {
|
|
49
78
|
if (scope === 'global') return saveInstallations(value);
|
|
50
79
|
return saveProjectInstallations(projectRoot, value);
|
|
@@ -96,6 +125,7 @@ export async function installSkill(config, identifier, {
|
|
|
96
125
|
const lock = await loadLock(scope, root);
|
|
97
126
|
lock.lockfile_version = 1;
|
|
98
127
|
lock.installations ||= {};
|
|
128
|
+
if (scope === 'project') await ensureProjectGitignore(root);
|
|
99
129
|
const packageFiles = filesFromDetail(detail);
|
|
100
130
|
const contentHash = packageDigest(packageFiles);
|
|
101
131
|
const canonical = copy === true ? null : canonicalSkillPath({
|