@constraint/cli 0.4.0 → 0.4.1

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.0",
3
+ "version": "0.4.1",
4
4
  "description": "Install, publish, and report Agent Skills with Constraint",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,10 +57,10 @@ constraint report <transcript-path> --skill <skill-name>
57
57
  ```
58
58
 
59
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
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
64
  determined, say so instead of guessing.
65
65
 
66
66
  ## Work safely
@@ -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, 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.
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
16
 
17
17
  ## Discover, inspect, install, and update
18
18
 
@@ -53,8 +53,9 @@ constraint skills run get <run-id> --raw
53
53
  ```
54
54
 
55
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.
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.
58
59
  Re-reporting the same session supersedes the earlier report.
59
60
 
60
61
  Repeat `--team`, `--user`, or `--skill` to request subsets. Review visibility is permission-controlled.
package/src/cli.js CHANGED
@@ -20,7 +20,6 @@ import {
20
20
  taskSpinner,
21
21
  } from './prompts.js';
22
22
  import {
23
- applyInstructionSetup,
24
23
  installCompanionSkill,
25
24
  setupPlan,
26
25
  setupStatus,
@@ -36,7 +35,7 @@ import {
36
35
  uploadSkill,
37
36
  } from './skills.js';
38
37
 
39
- const VERSION = '0.4.0';
38
+ const VERSION = '0.4.1';
40
39
 
41
40
  const HELP = `Constraint Skills CLI
42
41
 
@@ -249,30 +248,17 @@ async function setupCommand(args, out) {
249
248
  if (dryRun) {
250
249
  out.data(plans.map((plan) => ({
251
250
  agent: plan.agent,
252
- instructions: {
253
- path: plan.instructions.path,
254
- action: plan.instructions.changed ? 'update' : 'current',
255
- content: plan.instructions.content,
256
- },
257
251
  skill: { path: plan.skill.path, scope: 'global', action: plan.skill.action },
258
252
  })));
259
253
  return;
260
254
  }
261
- const hasInstructionChanges = plans.some((plan) => plan.instructions.changed);
262
255
  const hasSkillChanges = plans.some((plan) => plan.skill.action !== 'current');
263
- if (!useUi && !yes && (hasInstructionChanges || hasSkillChanges)) {
256
+ if (!useUi && !yes && hasSkillChanges) {
264
257
  throw new Error('Non-interactive setup has pending changes. Pass --yes or use --dry-run.');
265
258
  }
266
259
 
267
- let configureInstructions = true;
268
260
  let installSkill = true;
269
261
  if (useUi) {
270
- note(
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',
273
- );
274
- configureInstructions = !hasInstructionChanges || yes || await confirmPlan('Remove the legacy Constraint block from these files?');
275
-
276
262
  note(
277
263
  [
278
264
  'Skill: constraint-skills',
@@ -284,20 +270,12 @@ async function setupCommand(args, out) {
284
270
  '',
285
271
  ]),
286
272
  ].join('\n').trim(),
287
- '2. Install Constraint Skills',
273
+ 'Install Constraint Skills',
288
274
  );
289
275
  installSkill = !hasSkillChanges || yes || await confirmPlan(`Install globally for ${agents.map(humanize).join(' and ')}?`);
290
276
  }
291
277
 
292
- const result = { instructions: [], skill: [] };
293
- if (configureInstructions) {
294
- const spinner = taskSpinner(useUi && hasInstructionChanges);
295
- for (const plan of plans) {
296
- spinner.message(`Cleaning ${humanize(plan.agent)} instructions`);
297
- result.instructions.push(await applyInstructionSetup(plan.agent));
298
- }
299
- spinner.stop('Agent instructions cleaned.');
300
- }
278
+ const result = { skill: [] };
301
279
  if (installSkill) {
302
280
  const spinner = taskSpinner(useUi && hasSkillChanges);
303
281
  for (const plan of plans) {
@@ -309,7 +287,6 @@ async function setupCommand(args, out) {
309
287
  if (useUi) {
310
288
  finish([
311
289
  'Setup complete.',
312
- `Instructions: ${configureInstructions ? `${agents.length} configured` : 'skipped'}`,
313
290
  `Skill: ${installSkill ? `installed for ${agents.map(humanize).join(' and ')}` : 'skipped'}`,
314
291
  ].join('\n'));
315
292
  } else out.data(result);
@@ -472,8 +449,23 @@ async function uploadCommand(args, config, out) {
472
449
 
473
450
  // A run is the posting of a transcript in which a skill was invoked. The user
474
451
  // 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.
452
+ // transcript. Client and session id come straight from the transcript path —
453
+ // agent clients keep sessions at well-known locations named by session id.
454
+ export function transcriptIdentity(absolute) {
455
+ const basename = path.basename(absolute);
456
+ const stem = basename.replace(/\.(jsonl?|json)$/i, '');
457
+ const uuid = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
458
+ if (absolute.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`) || stem.startsWith('rollout-')) {
459
+ const match = stem.match(uuid);
460
+ return { client: 'codex', session: match ? match[0] : stem };
461
+ }
462
+ if (absolute.includes(`${path.sep}.claude${path.sep}projects${path.sep}`) || uuid.test(stem)) {
463
+ const match = stem.match(uuid);
464
+ return { client: 'claude-code', session: match ? match[0] : stem };
465
+ }
466
+ return { client: null, session: null };
467
+ }
468
+
477
469
  async function reportCommand(args, config, out) {
478
470
  const skill = takeOption(args, '--skill');
479
471
  const client = takeOption(args, '--client');
@@ -483,9 +475,13 @@ async function reportCommand(args, config, out) {
483
475
  throw new Error('Usage: constraint report <transcript-path> --skill <skill> [--client <client>] [--session <session-id>]');
484
476
  }
485
477
  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);
478
+ const identity = transcriptIdentity(transcript.absolute);
479
+ const resolvedClient = client || identity.client;
480
+ const resolvedSession = session || identity.session;
481
+ if (!resolvedClient || !resolvedSession) {
482
+ throw new Error('Could not tell the client and session from this path. Pass --client and --session.');
483
+ }
484
+ const query = new URLSearchParams({ skill, client: resolvedClient, session: resolvedSession });
489
485
  const run = await apiRequest(config, `/skill-reports?${query.toString()}`, {
490
486
  method: 'POST',
491
487
  headers: { 'Content-Type': mediaType(transcript.absolute), 'X-Transcript-Filename': transcript.filename },
package/src/paths.js CHANGED
@@ -28,18 +28,15 @@ export function agentPaths(agent, { scope = 'global', projectRoot = process.cwd(
28
28
  const selected = normalizeAgent(agent);
29
29
  if (scope === 'project') {
30
30
  if (selected === 'codex') {
31
- return { skills: path.join(projectRoot, '.agents', 'skills'), instructions: null };
31
+ return { skills: path.join(projectRoot, '.agents', 'skills') };
32
32
  }
33
- return { skills: path.join(projectRoot, '.claude', 'skills'), instructions: null };
33
+ return { skills: path.join(projectRoot, '.claude', 'skills') };
34
34
  }
35
35
  if (selected === 'codex') {
36
36
  const root = process.env.CODEX_HOME ? expandPath(process.env.CODEX_HOME) : path.join(os.homedir(), '.codex');
37
- return { instructions: path.join(root, 'AGENTS.md'), skills: path.join(root, 'skills') };
37
+ return { skills: path.join(root, 'skills') };
38
38
  }
39
- return {
40
- instructions: path.join(os.homedir(), '.claude', 'CLAUDE.md'),
41
- skills: path.join(os.homedir(), '.claude', 'skills'),
42
- };
39
+ return { skills: path.join(os.homedir(), '.claude', 'skills') };
43
40
  }
44
41
 
45
42
  async function exists(candidate) {
package/src/setup.js CHANGED
@@ -6,32 +6,6 @@ import { loadConfig, saveConfig } from './config.js';
6
6
  import { packageDigest, readSkillDirectory, writeSkillDirectory } from './files.js';
7
7
  import { agentPaths, normalizeAgent } from './paths.js';
8
8
 
9
- export const BLOCK_START = '<!-- constraint-skills:start -->';
10
- export const BLOCK_END = '<!-- constraint-skills:end -->';
11
-
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) {
16
- const start = existing.indexOf(BLOCK_START);
17
- const end = existing.indexOf(BLOCK_END);
18
- if (start < 0 && end < 0) return existing;
19
- if ((start >= 0) !== (end >= 0)) {
20
- throw new Error('The existing Constraint Skills instruction block is incomplete; fix it before running setup.');
21
- }
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');
26
- }
27
-
28
- async function writeInstructions(file, content, mode) {
29
- await fs.mkdir(path.dirname(file), { recursive: true });
30
- const staging = `${file}.constraint-${process.pid}`;
31
- await fs.writeFile(staging, content, { mode });
32
- await fs.rename(staging, file);
33
- }
34
-
35
9
  async function companionFiles() {
36
10
  const here = path.dirname(fileURLToPath(import.meta.url));
37
11
  return readSkillDirectory(path.resolve(here, '..', 'skills', 'constraint-skills'));
@@ -44,19 +18,6 @@ export function expectedCompanionHash(config, selected, currentHash) {
44
18
  return currentHash && recorded.includes(currentHash) ? currentHash : null;
45
19
  }
46
20
 
47
- async function instructionPlan(selected, paths) {
48
- let existing = '';
49
- let mode = 0o644;
50
- try {
51
- existing = await fs.readFile(paths.instructions, 'utf8');
52
- mode = (await fs.stat(paths.instructions)).mode & 0o777;
53
- } catch (error) {
54
- if (error && error.code !== 'ENOENT') throw error;
55
- }
56
- const content = stripInstructionBlock(existing);
57
- return { path: paths.instructions, changed: content !== existing, content, mode };
58
- }
59
-
60
21
  async function skillPlan(selected, paths) {
61
22
  const files = await companionFiles();
62
23
  const bundledHash = packageDigest(files);
@@ -84,7 +45,6 @@ export async function setupPlan(agent) {
84
45
  const paths = agentPaths(selected);
85
46
  return {
86
47
  agent: selected,
87
- instructions: await instructionPlan(selected, paths),
88
48
  skill: await skillPlan(selected, paths),
89
49
  };
90
50
  }
@@ -95,15 +55,6 @@ async function recordConfiguredAgent(selected, config = null) {
95
55
  await saveConfig(next);
96
56
  }
97
57
 
98
- export async function applyInstructionSetup(agent) {
99
- const selected = normalizeAgent(agent);
100
- const paths = agentPaths(selected);
101
- const plan = await instructionPlan(selected, paths);
102
- if (plan.changed) await writeInstructions(plan.path, plan.content, plan.mode);
103
- await recordConfiguredAgent(selected);
104
- return { agent: selected, path: plan.path, changed: plan.changed, applied: true };
105
- }
106
-
107
58
  export async function installCompanionSkill(agent) {
108
59
  const selected = normalizeAgent(agent);
109
60
  const paths = agentPaths(selected);
@@ -122,42 +73,25 @@ export async function installCompanionSkill(agent) {
122
73
 
123
74
  export async function setupAgent(agent, {
124
75
  dryRun = false,
125
- yes = false,
126
- confirmChange = async () => false,
127
76
  output = console.log,
128
- instructions = true,
129
- skill = true,
130
77
  } = {}) {
131
78
  const selected = normalizeAgent(agent);
132
79
  const plan = await setupPlan(selected);
133
- output(`${selected}: ${plan.instructions.path}`);
134
80
  if (dryRun) {
135
- if (instructions) output(plan.instructions.content);
136
- if (skill) output(`Install constraint-skills globally at ${plan.skill.path} (${plan.skill.action}).`);
137
- return { agent: selected, instructions: plan.instructions, skill: plan.skill, applied: false };
138
- }
139
- let instructionResult = null;
140
- let skillResult = null;
141
- if (instructions) {
142
- const approved = !plan.instructions.changed || yes || await confirmChange(
143
- `Remove the legacy Constraint block from ${plan.instructions.path}?`,
144
- );
145
- if (approved) instructionResult = await applyInstructionSetup(selected);
81
+ output(`Install constraint-skills globally at ${plan.skill.path} (${plan.skill.action}).`);
82
+ return { agent: selected, skill: plan.skill, applied: false };
146
83
  }
147
- if (skill) skillResult = await installCompanionSkill(selected);
148
- return { agent: selected, instructions: instructionResult, skill: skillResult, applied: true };
84
+ const skillResult = await installCompanionSkill(selected);
85
+ return { agent: selected, skill: skillResult, applied: true };
149
86
  }
150
87
 
151
88
  export async function setupStatus(agent) {
152
89
  const selected = normalizeAgent(agent);
153
90
  const paths = agentPaths(selected);
154
91
  try {
155
- const [instructions, skill] = await Promise.all([
156
- fs.readFile(paths.instructions, 'utf8'),
157
- fs.stat(path.join(paths.skills, 'constraint-skills', 'SKILL.md')),
158
- ]);
159
- return { agent: selected, legacy_instruction_block: instructions.includes(BLOCK_START), skill: skill.isFile() };
92
+ const skill = await fs.stat(path.join(paths.skills, 'constraint-skills', 'SKILL.md'));
93
+ return { agent: selected, skill: skill.isFile() };
160
94
  } catch {
161
- return { agent: selected, legacy_instruction_block: false, skill: false };
95
+ return { agent: selected, skill: false };
162
96
  }
163
97
  }