@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 +1 -1
- package/skills/constraint-skills/SKILL.md +4 -4
- package/skills/constraint-skills/references/commands.md +4 -3
- package/src/cli.js +28 -32
- package/src/paths.js +4 -7
- package/src/setup.js +7 -73
package/package.json
CHANGED
|
@@ -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`.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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.
|
|
57
|
-
|
|
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.
|
|
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 &&
|
|
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
|
-
'
|
|
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 = {
|
|
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
|
|
476
|
-
//
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
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')
|
|
31
|
+
return { skills: path.join(projectRoot, '.agents', 'skills') };
|
|
32
32
|
}
|
|
33
|
-
return { skills: path.join(projectRoot, '.claude', 'skills')
|
|
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 {
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
148
|
-
return { agent: selected,
|
|
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
|
|
156
|
-
|
|
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,
|
|
95
|
+
return { agent: selected, skill: false };
|
|
162
96
|
}
|
|
163
97
|
}
|