@constraint/cli 0.2.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/LICENSE +92 -0
- package/README.md +203 -0
- package/bin/constraint.js +9 -0
- package/package.json +19 -0
- package/skills/constraint-skills/SKILL.md +44 -0
- package/skills/constraint-skills/agents/openai.yaml +4 -0
- package/src/api.js +37 -0
- package/src/arguments.js +26 -0
- package/src/auth-store.js +58 -0
- package/src/auth.js +103 -0
- package/src/cli.js +405 -0
- package/src/config.js +61 -0
- package/src/files.js +146 -0
- package/src/paths.js +83 -0
- package/src/prompts.js +83 -0
- package/src/setup.js +94 -0
- package/src/skills.js +209 -0
package/src/prompts.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
import { AGENTS } from './paths.js';
|
|
5
|
+
|
|
6
|
+
const AGENT_OPTIONS = [
|
|
7
|
+
{ value: 'codex', label: 'Codex', hint: '.agents/skills or ~/.codex/skills' },
|
|
8
|
+
{ value: 'claude-code', label: 'Claude Code', hint: '.claude/skills or ~/.claude/skills' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function cancelled(value) {
|
|
12
|
+
if (!p.isCancel(value)) return value;
|
|
13
|
+
p.cancel('No changes made.');
|
|
14
|
+
throw new Error('Operation cancelled.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function begin(title) {
|
|
18
|
+
p.intro(pc.bgCyan(pc.black(` ${title} `)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function chooseAgents(initial = []) {
|
|
22
|
+
const values = await p.autocompleteMultiselect({
|
|
23
|
+
message: 'Which agents should receive the skill?',
|
|
24
|
+
placeholder: 'Type to filter agents',
|
|
25
|
+
options: AGENT_OPTIONS,
|
|
26
|
+
initialValues: initial.filter((value) => AGENTS.includes(value)),
|
|
27
|
+
required: true,
|
|
28
|
+
});
|
|
29
|
+
return cancelled(values);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function chooseScope() {
|
|
33
|
+
return cancelled(await p.select({
|
|
34
|
+
message: 'Where should the skill be installed?',
|
|
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' },
|
|
48
|
+
],
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function confirmPlan(message) {
|
|
53
|
+
return cancelled(await p.confirm({ message, initialValue: true }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function taskSpinner(enabled = true) {
|
|
57
|
+
if (!enabled) return { message() {}, stop() {} };
|
|
58
|
+
const spinner = p.spinner();
|
|
59
|
+
let started = false;
|
|
60
|
+
return {
|
|
61
|
+
message(value) {
|
|
62
|
+
if (!started) {
|
|
63
|
+
spinner.start(value);
|
|
64
|
+
started = true;
|
|
65
|
+
} else spinner.message(value);
|
|
66
|
+
},
|
|
67
|
+
stop(value) {
|
|
68
|
+
if (started) spinner.stop(value);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function finish(message) {
|
|
74
|
+
p.outro(pc.green(message));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function note(message, title = 'Install plan') {
|
|
78
|
+
p.note(message, title);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function warn(message) {
|
|
82
|
+
p.log.warn(message);
|
|
83
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
export const BLOCK_START = '<!-- constraint-skills:start -->';
|
|
10
|
+
export const BLOCK_END = '<!-- constraint-skills:end -->';
|
|
11
|
+
|
|
12
|
+
const BLOCK = `${BLOCK_START}
|
|
13
|
+
## Constraint Skills
|
|
14
|
+
|
|
15
|
+
When invoking any installed Agent Skill, load and follow the global
|
|
16
|
+
\`constraint-skills\` companion skill. It records the run with the Constraint
|
|
17
|
+
CLI without modifying the invoked skill.
|
|
18
|
+
${BLOCK_END}`;
|
|
19
|
+
|
|
20
|
+
export function mergeInstructionBlock(existing) {
|
|
21
|
+
const start = existing.indexOf(BLOCK_START);
|
|
22
|
+
const end = existing.indexOf(BLOCK_END);
|
|
23
|
+
if ((start >= 0) !== (end >= 0)) {
|
|
24
|
+
throw new Error('The existing Constraint Skills instruction block is incomplete; fix it before running setup.');
|
|
25
|
+
}
|
|
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`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function writeInstructions(file, content, mode) {
|
|
34
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
35
|
+
const staging = `${file}.constraint-${process.pid}`;
|
|
36
|
+
await fs.writeFile(staging, content, { mode });
|
|
37
|
+
await fs.rename(staging, file);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function companionFiles() {
|
|
41
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
return readSkillDirectory(path.resolve(here, '..', 'skills', 'constraint-skills'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function setupAgent(agent, {
|
|
46
|
+
dryRun = false,
|
|
47
|
+
yes = false,
|
|
48
|
+
confirmChange = async () => false,
|
|
49
|
+
output = console.log,
|
|
50
|
+
} = {}) {
|
|
51
|
+
const selected = normalizeAgent(agent);
|
|
52
|
+
const paths = agentPaths(selected);
|
|
53
|
+
let existing = '';
|
|
54
|
+
let instructionMode = 0o644;
|
|
55
|
+
try {
|
|
56
|
+
existing = await fs.readFile(paths.instructions, 'utf8');
|
|
57
|
+
instructionMode = (await fs.stat(paths.instructions)).mode & 0o777;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
60
|
+
}
|
|
61
|
+
const merged = mergeInstructionBlock(existing);
|
|
62
|
+
output(`${selected}: ${paths.instructions}`);
|
|
63
|
+
if (dryRun) {
|
|
64
|
+
output(merged);
|
|
65
|
+
return { agent: selected, changed: merged !== existing, applied: false };
|
|
66
|
+
}
|
|
67
|
+
if (merged !== existing && !yes && !(await confirmChange(`Add the Constraint Skills block to ${paths.instructions}?`))) {
|
|
68
|
+
return { agent: selected, changed: true, applied: false };
|
|
69
|
+
}
|
|
70
|
+
const files = await companionFiles();
|
|
71
|
+
const config = await loadConfig();
|
|
72
|
+
config.companion_hashes ||= {};
|
|
73
|
+
const target = path.join(paths.skills, 'constraint-skills');
|
|
74
|
+
await writeSkillDirectory(target, files, { expectedHash: config.companion_hashes[selected] || null });
|
|
75
|
+
if (merged !== existing) await writeInstructions(paths.instructions, merged, instructionMode);
|
|
76
|
+
config.companion_hashes[selected] = packageDigest(files);
|
|
77
|
+
config.agents = [...new Set([...config.agents.map(normalizeAgent), selected])];
|
|
78
|
+
await saveConfig(config);
|
|
79
|
+
return { agent: selected, changed: merged !== existing, applied: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function setupStatus(agent) {
|
|
83
|
+
const selected = normalizeAgent(agent);
|
|
84
|
+
const paths = agentPaths(selected);
|
|
85
|
+
try {
|
|
86
|
+
const [instructions, skill] = await Promise.all([
|
|
87
|
+
fs.readFile(paths.instructions, 'utf8'),
|
|
88
|
+
fs.stat(path.join(paths.skills, 'constraint-skills', 'SKILL.md')),
|
|
89
|
+
]);
|
|
90
|
+
return { agent: selected, instructions: instructions.includes(BLOCK_START), skill: skill.isFile() };
|
|
91
|
+
} catch {
|
|
92
|
+
return { agent: selected, instructions: false, skill: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { apiRequest, jsonRequest } from './api.js';
|
|
5
|
+
import {
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadInstallations,
|
|
8
|
+
loadProjectInstallations,
|
|
9
|
+
saveInstallations,
|
|
10
|
+
saveProjectInstallations,
|
|
11
|
+
} from './config.js';
|
|
12
|
+
import {
|
|
13
|
+
assertManagedSkillUnchanged,
|
|
14
|
+
linkSkillDirectory,
|
|
15
|
+
packageDigest,
|
|
16
|
+
readSkillDirectory,
|
|
17
|
+
writeSkillDirectory,
|
|
18
|
+
} from './files.js';
|
|
19
|
+
import {
|
|
20
|
+
agentPaths,
|
|
21
|
+
canonicalSkillPath,
|
|
22
|
+
findProjectRoot,
|
|
23
|
+
normalizeAgent,
|
|
24
|
+
workspaceId,
|
|
25
|
+
} from './paths.js';
|
|
26
|
+
|
|
27
|
+
export const DOMAINS = [
|
|
28
|
+
'sales_pipeline', 'content_audience', 'marketing_demand', 'offers_monetization',
|
|
29
|
+
'operations_team', 'partnerships_bd',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function filesFromDetail(detail) {
|
|
33
|
+
return detail.version.files.map((file) => ({
|
|
34
|
+
path: file.path,
|
|
35
|
+
media_type: file.media_type,
|
|
36
|
+
content: Buffer.from(file.content_base64, 'base64'),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function installKey(agent, skillId) {
|
|
41
|
+
return `${agent}:${skillId}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function portablePath(value, scope, projectRoot) {
|
|
45
|
+
if (scope === 'global') return value;
|
|
46
|
+
return path.relative(projectRoot, value).split(path.sep).join('/');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function loadLock(scope, projectRoot) {
|
|
50
|
+
return scope === 'global' ? loadInstallations() : loadProjectInstallations(projectRoot);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function saveLock(scope, projectRoot, value) {
|
|
54
|
+
if (scope === 'global') return saveInstallations(value);
|
|
55
|
+
return saveProjectInstallations(projectRoot, value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function uploadSkill(config, localPath, domain) {
|
|
59
|
+
const files = await readSkillDirectory(localPath);
|
|
60
|
+
return apiRequest(config, '/skills/uploads', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
...jsonRequest({
|
|
63
|
+
domain,
|
|
64
|
+
files: files.map((file) => ({
|
|
65
|
+
path: file.path,
|
|
66
|
+
media_type: file.media_type,
|
|
67
|
+
content_base64: file.content.toString('base64'),
|
|
68
|
+
})),
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function manageableDomains(config) {
|
|
74
|
+
const actor = await apiRequest(config, '/me');
|
|
75
|
+
if (actor.permissions.includes('org.manage')) return DOMAINS;
|
|
76
|
+
return DOMAINS.filter((domain) => actor.permissions.includes(`${domain}.skill.manage`));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function configuredAgents() {
|
|
80
|
+
const config = await loadConfig();
|
|
81
|
+
return [...new Set(config.agents.map(normalizeAgent))];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function installSkill(config, identifier, {
|
|
85
|
+
version,
|
|
86
|
+
agents,
|
|
87
|
+
scope = 'project',
|
|
88
|
+
projectRoot,
|
|
89
|
+
copy = false,
|
|
90
|
+
update = false,
|
|
91
|
+
progress = () => {},
|
|
92
|
+
} = {}) {
|
|
93
|
+
const suffix = version ? `/versions/${version}` : '';
|
|
94
|
+
const detail = await apiRequest(config, `/skills/${encodeURIComponent(identifier)}${suffix}?include_content=true`);
|
|
95
|
+
const root = scope === 'project' ? (projectRoot || await findProjectRoot()) : null;
|
|
96
|
+
const selectedAgents = [...new Set((agents || []).map(normalizeAgent))];
|
|
97
|
+
if (selectedAgents.length === 0) throw new Error('Choose at least one agent with --agent codex or --agent claude-code.');
|
|
98
|
+
const lock = await loadLock(scope, root);
|
|
99
|
+
lock.lockfile_version = 1;
|
|
100
|
+
lock.installations ||= {};
|
|
101
|
+
const packageFiles = filesFromDetail(detail);
|
|
102
|
+
const contentHash = packageDigest(packageFiles);
|
|
103
|
+
const canonical = copy === true ? null : canonicalSkillPath({
|
|
104
|
+
scope,
|
|
105
|
+
projectRoot: root,
|
|
106
|
+
skillId: detail.skill_id,
|
|
107
|
+
slug: detail.slug,
|
|
108
|
+
version: detail.version.version,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (canonical) {
|
|
112
|
+
progress(`Staging ${detail.slug} v${detail.version.version}`);
|
|
113
|
+
await writeSkillDirectory(canonical, packageFiles);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const agent of selectedAgents) {
|
|
118
|
+
progress(`Installing for ${agent}`);
|
|
119
|
+
const key = installKey(agent, detail.skill_id);
|
|
120
|
+
const previous = lock.installations[key];
|
|
121
|
+
const target = path.join(agentPaths(agent, { scope, projectRoot: root }).skills, detail.slug);
|
|
122
|
+
if (update && !previous) throw new Error(`${detail.slug} is not installed for ${agent} in ${scope} scope.`);
|
|
123
|
+
|
|
124
|
+
const useCopy = copy === true || (copy === undefined && previous?.mode === 'copy');
|
|
125
|
+
let mode = useCopy ? 'copy' : 'symlink';
|
|
126
|
+
let changed = false;
|
|
127
|
+
if (useCopy) {
|
|
128
|
+
const result = await writeSkillDirectory(target, packageFiles, {
|
|
129
|
+
expectedHash: previous?.content_sha256 || null,
|
|
130
|
+
forceDirectory: true,
|
|
131
|
+
});
|
|
132
|
+
changed = result.changed;
|
|
133
|
+
} else {
|
|
134
|
+
if (previous) await assertManagedSkillUnchanged(target, previous.content_sha256);
|
|
135
|
+
const linked = await linkSkillDirectory(target, canonical, {
|
|
136
|
+
managed: Boolean(previous),
|
|
137
|
+
expectedHash: previous?.content_sha256 || null,
|
|
138
|
+
});
|
|
139
|
+
if (linked.supported) {
|
|
140
|
+
changed = linked.changed;
|
|
141
|
+
} else {
|
|
142
|
+
mode = 'copy';
|
|
143
|
+
const result = await writeSkillDirectory(target, packageFiles, {
|
|
144
|
+
expectedHash: previous?.content_sha256 || null,
|
|
145
|
+
forceDirectory: true,
|
|
146
|
+
});
|
|
147
|
+
changed = result.changed;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const receiptWorkspace = scope === 'project' ? workspaceId(root) : null;
|
|
152
|
+
lock.installations[key] = {
|
|
153
|
+
skill_id: detail.skill_id,
|
|
154
|
+
slug: detail.slug,
|
|
155
|
+
version: detail.version.version,
|
|
156
|
+
content_sha256: contentHash,
|
|
157
|
+
agent,
|
|
158
|
+
scope,
|
|
159
|
+
mode,
|
|
160
|
+
workspace_id: receiptWorkspace,
|
|
161
|
+
path: portablePath(target, scope, root),
|
|
162
|
+
canonical_path: canonical ? portablePath(canonical, scope, root) : null,
|
|
163
|
+
};
|
|
164
|
+
await saveLock(scope, root, lock);
|
|
165
|
+
const receipt = await apiRequest(config, `/skills/${encodeURIComponent(detail.skill_id)}/installations`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
...jsonRequest({
|
|
168
|
+
agent,
|
|
169
|
+
scope,
|
|
170
|
+
mode,
|
|
171
|
+
version: detail.version.version,
|
|
172
|
+
...(receiptWorkspace ? { workspace_id: receiptWorkspace } : {}),
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
results.push({ detail, agent, scope, mode, target, changed, receipt });
|
|
176
|
+
}
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function updateAll(config, { agents, scope = 'project', projectRoot, progress } = {}) {
|
|
181
|
+
const root = scope === 'project' ? (projectRoot || await findProjectRoot()) : null;
|
|
182
|
+
const lock = await loadLock(scope, root);
|
|
183
|
+
const selected = agents?.length ? new Set(agents.map(normalizeAgent)) : null;
|
|
184
|
+
const entries = Object.values(lock.installations || {}).filter((item) => !selected || selected.has(item.agent));
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
results.push(...await installSkill(config, entry.skill_id, {
|
|
188
|
+
agents: [entry.agent],
|
|
189
|
+
scope,
|
|
190
|
+
projectRoot: root,
|
|
191
|
+
copy: entry.mode === 'copy',
|
|
192
|
+
update: true,
|
|
193
|
+
progress,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
return results;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function installedSkills({ scope = 'project', projectRoot, agents } = {}) {
|
|
200
|
+
const root = scope === 'project' ? (projectRoot || await findProjectRoot()) : null;
|
|
201
|
+
const lock = await loadLock(scope, root);
|
|
202
|
+
const selected = agents?.length ? new Set(agents.map(normalizeAgent)) : null;
|
|
203
|
+
return Object.values(lock.installations || {}).filter((item) => !selected || selected.has(item.agent));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function readTranscript(file) {
|
|
207
|
+
const absolute = path.resolve(file);
|
|
208
|
+
return { absolute, filename: path.basename(absolute), content: await fs.readFile(absolute) };
|
|
209
|
+
}
|