@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constraint/cli",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "Install, publish, and report Agent Skills with Constraint",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,23 +1,25 @@
1
1
  ---
2
2
  name: constraint-skills
3
- description: Operate governed Agent Skills with the Constraint terminal CLI and report their invocations. Use when an agent needs to authenticate or diagnose Constraint access; configure agents; discover upload domains; inspect, install, update, or upload skills; query run history; or invoke any skill installed from or governed by Constraint.
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 config, lockfiles, instruction blocks, installation receipts, or authorization tokens directly.
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 or handling a less common operation.
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 name or reuse a domain from another organization. If multiple destinations are valid and ownership is unclear, ask the user.
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 `--global` only when the user wants the skill across projects. Prefer managed links and use `--copy` only when independent files are required. Never overwrite locally edited installed skills.
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
- ## Start a run
47
+ ## Report a run
41
48
 
42
- Immediately after selecting and loading a governed skill, run:
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
- ```bash
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 skills run complete <run-id> <transcript-path>
56
+ constraint report <transcript-path> --skill <skill-name>
58
57
  ```
59
58
 
60
- Pass the explicit local path. The CLI uploads the whole transcript file as it exists at that moment and completes only that run. Never create a replacement transcript, summarize it, pass a temporary file, or guess a path.
61
-
62
- For multiple invocations, complete each retained run separately. Overlapping full-session transcript prefixes are expected. If the transcript path cannot be determined, leave the run started and tell the user that reporting could not be completed.
63
-
64
- Do not run completion hooks, background flushes, or pending-run scans.
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, then separately presents the managed global-instruction change and this skill's global installation destinations. It never replaces unrelated instructions.
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
- ## Start, complete, and review runs
43
+ ## Report and review runs
44
44
 
45
45
  ```bash
46
- constraint skills run start <skill> --client codex --session <stable-session-id>
47
- constraint skills run complete <run-id> <actual-transcript-path>
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 completion.
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.3.6';
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 skills run start <skill> --client <client> --session <session-id>
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 ? 'Managed block will be added or updated' : 'Already configured'}`).join('\n\n'),
273
- '1. Configure agent instructions',
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('Apply these instruction changes?');
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(`Configuring ${humanize(plan.agent)} instructions`);
296
+ spinner.message(`Cleaning ${humanize(plan.agent)} instructions`);
298
297
  result.instructions.push(await applyInstructionSetup(plan.agent));
299
298
  }
300
- spinner.stop('Agent instructions configured.');
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
- async function runStart(args, config, out) {
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 || !client || !session) {
479
- throw new Error('Usage: constraint skills run start <skill> --client <client> --session <session-id>');
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 run = await apiRequest(config, '/skill-runs', {
482
- method: 'POST',
483
- ...jsonRequest({ skill: args[0], client, session_id: session }),
484
- });
485
- if (out.json) out.data(run);
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(run);
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 start, complete, list, or get.');
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
- const BLOCK = `${BLOCK_START}
13
- ## Constraint Skills
14
-
15
- When using the Constraint CLI or invoking any installed Agent Skill, load and
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
- if (start >= 0 && end >= start) {
27
- return `${existing.slice(0, start)}${BLOCK}${existing.slice(end + BLOCK_END.length)}`;
28
- }
29
- const separator = existing.length === 0 ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
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 = mergeInstructionBlock(existing);
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
- `Add the Constraint Skills block to ${plan.instructions.path}?`,
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, instructions: instructions.includes(BLOCK_START), skill: skill.isFile() };
159
+ return { agent: selected, legacy_instruction_block: instructions.includes(BLOCK_START), skill: skill.isFile() };
165
160
  } catch {
166
- return { agent: selected, instructions: false, skill: false };
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({