@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 ADDED
@@ -0,0 +1,92 @@
1
+ Constraint CLI Proprietary License
2
+
3
+ Copyright (c) 2026 Constraint. All rights reserved.
4
+
5
+ This Constraint CLI Proprietary License (the "License") governs use of the
6
+ Constraint command-line software distributed with this License (the
7
+ "Software"). By installing or using the Software, you agree to this License.
8
+
9
+ 1. Limited License
10
+
11
+ Subject to this License and any applicable agreement governing your access to
12
+ Constraint's products or services (a "Service Agreement"), Constraint grants
13
+ you a limited, non-exclusive, non-transferable, non-sublicensable, revocable
14
+ license to install and use the Software solely to access and use Constraint
15
+ products and services that you are authorized to use.
16
+
17
+ 2. Restrictions
18
+
19
+ Except where applicable law expressly permits otherwise, you may not:
20
+
21
+ (a) copy the Software except as reasonably necessary to install, use, or make
22
+ one backup copy of it;
23
+
24
+ (b) modify, adapt, translate, or create derivative works of the Software;
25
+
26
+ (c) distribute, publish, sublicense, sell, rent, lease, lend, transfer, or
27
+ otherwise make the Software available to any third party;
28
+
29
+ (d) reverse engineer, decompile, disassemble, or attempt to discover the source
30
+ code, underlying ideas, algorithms, or non-public interfaces of the Software;
31
+
32
+ (e) remove or alter proprietary notices contained in the Software; or
33
+
34
+ (f) use the Software to bypass access controls, security controls, usage limits,
35
+ or permissions of any Constraint product or service.
36
+
37
+ 3. Service Agreement
38
+
39
+ Your access to Constraint products and services remains governed by the
40
+ applicable Service Agreement. If this License conflicts with a signed Service
41
+ Agreement, the signed Service Agreement controls to the extent of the conflict.
42
+ The Software does not grant access to any product, service, data, skill, or
43
+ other resource that you are not otherwise authorized to access.
44
+
45
+ 4. Ownership
46
+
47
+ The Software is licensed, not sold. Constraint and its licensors retain all
48
+ right, title, and interest in and to the Software, including all intellectual
49
+ property rights. No rights are granted except those expressly stated in this
50
+ License.
51
+
52
+ 5. Updates
53
+
54
+ Constraint may provide updates, modifications, or replacement versions of the
55
+ Software. Unless accompanied by different terms, those versions are governed
56
+ by this License. Constraint is not obligated to provide any update, support, or
57
+ maintenance.
58
+
59
+ 6. Term and Termination
60
+
61
+ This License ends automatically if you materially breach it or if your
62
+ authorization to use the applicable Constraint products and services ends.
63
+ Upon termination, you must stop using and delete all copies of the Software in
64
+ your possession or control. Sections 2, 4, 7, 8, and 9 survive termination.
65
+
66
+ 7. Disclaimer of Warranties
67
+
68
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED "AS IS" AND
69
+ "AS AVAILABLE," WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED,
70
+ STATUTORY, OR OTHERWISE, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
71
+ PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. CONSTRAINT DOES NOT WARRANT
72
+ THAT THE SOFTWARE WILL BE UNINTERRUPTED, ERROR-FREE, OR SECURE.
73
+
74
+ 8. Limitation of Liability
75
+
76
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, CONSTRAINT AND ITS LICENSORS WILL NOT BE
77
+ LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR
78
+ PUNITIVE DAMAGES, OR FOR ANY LOSS OF DATA, PROFITS, REVENUE, BUSINESS, OR
79
+ GOODWILL, ARISING OUT OF OR RELATED TO THE SOFTWARE, EVEN IF ADVISED OF THE
80
+ POSSIBILITY OF THOSE DAMAGES. TO THE MAXIMUM EXTENT PERMITTED BY LAW,
81
+ CONSTRAINT'S TOTAL LIABILITY ARISING OUT OF OR RELATED TO THE SOFTWARE WILL NOT
82
+ EXCEED THE AMOUNT PAID SPECIFICALLY FOR THE SOFTWARE DURING THE TWELVE MONTHS
83
+ BEFORE THE EVENT GIVING RISE TO LIABILITY.
84
+
85
+ 9. General
86
+
87
+ You may not assign this License without Constraint's prior written consent.
88
+ Constraint may assign this License in connection with a merger, acquisition,
89
+ reorganization, or sale of assets. If any provision is unenforceable, it will
90
+ be enforced to the maximum extent permitted and the remaining provisions will
91
+ remain in effect. A failure to enforce a provision is not a waiver. This
92
+ License does not limit rights that cannot lawfully be limited.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # Constraint CLI
2
+
3
+ Install, publish, and report governed Agent Skills with Constraint.
4
+
5
+ The package is named `@constraint/cli`; after installation, the terminal
6
+ command is simply `constraint`.
7
+
8
+ ## Requirements
9
+
10
+ - Node.js 20 or newer
11
+ - A Constraint account with access to a WorkOS organization
12
+ - Codex, Claude Code, or both
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install --global @constraint/cli
18
+ ```
19
+
20
+ Confirm the installation:
21
+
22
+ ```bash
23
+ constraint --version
24
+ constraint --help
25
+ ```
26
+
27
+ ## Sign in
28
+
29
+ ```bash
30
+ constraint login
31
+ ```
32
+
33
+ The CLI displays a verification URL and code, then uses WorkOS device
34
+ authorization to sign in. It never asks for a Constraint API key or client
35
+ secret.
36
+
37
+ Inspect or clear the current session:
38
+
39
+ ```bash
40
+ constraint whoami
41
+ constraint logout
42
+ ```
43
+
44
+ ## Configure agents
45
+
46
+ Run the interactive setup:
47
+
48
+ ```bash
49
+ constraint setup
50
+ ```
51
+
52
+ Or target agents explicitly:
53
+
54
+ ```bash
55
+ constraint setup --agent codex --agent claude-code
56
+ ```
57
+
58
+ Setup installs the `constraint-skills` companion skill and offers to add a
59
+ marked Constraint block to the agent's global instructions. Existing
60
+ `AGENTS.md` and `CLAUDE.md` content outside that block is preserved.
61
+
62
+ Preview the instruction change without applying it:
63
+
64
+ ```bash
65
+ constraint setup --agent codex --dry-run
66
+ ```
67
+
68
+ ## Discover skills
69
+
70
+ ```bash
71
+ constraint skills list
72
+ constraint skills list --search research
73
+ constraint skills inspect account-research
74
+ constraint skills inspect account-research --files
75
+ ```
76
+
77
+ The server returns only skills the signed-in user is authorized to access.
78
+ There is no domain-selection flag for reading or installing skills.
79
+
80
+ ## Install skills
81
+
82
+ Start the interactive installer:
83
+
84
+ ```bash
85
+ constraint skills install account-research
86
+ ```
87
+
88
+ Install for multiple agents in the current project:
89
+
90
+ ```bash
91
+ constraint skills install account-research \
92
+ --agent codex \
93
+ --agent claude-code \
94
+ --project
95
+ ```
96
+
97
+ Install globally:
98
+
99
+ ```bash
100
+ constraint skills install account-research --agent codex --global
101
+ ```
102
+
103
+ Project installations write a portable `skills-lock.json`. Global
104
+ installations use Constraint's private per-user configuration directory. The
105
+ installer prefers managed links so agents can share one package; pass `--copy`
106
+ to install independent copies. If links are unavailable, the installer falls
107
+ back to copying and records the mode that actually occurred.
108
+
109
+ The installer stages changes, replaces managed paths atomically, and refuses
110
+ to overwrite local edits.
111
+
112
+ List local installation records:
113
+
114
+ ```bash
115
+ constraint skills list --installed --project
116
+ constraint skills list --installed --global
117
+ ```
118
+
119
+ Update one skill or every skill in a scope:
120
+
121
+ ```bash
122
+ constraint skills update account-research --project
123
+ constraint skills update --all --project
124
+ constraint skills update --all --global
125
+ ```
126
+
127
+ ## Upload a skill
128
+
129
+ Create or edit a standard Agent Skill folder locally, then upload it:
130
+
131
+ ```bash
132
+ constraint skills upload ./account-research
133
+ ```
134
+
135
+ The folder must contain a root `SKILL.md` with `name` and `description` YAML
136
+ frontmatter. If the user can manage more than one Constraint domain, the CLI
137
+ asks which domain owns the skill. Skill versions are immutable; uploading
138
+ changed content creates the next version.
139
+
140
+ ## Report skill runs
141
+
142
+ The installed companion skill directs an agent to start a run after loading a
143
+ governed skill:
144
+
145
+ ```bash
146
+ constraint skills run start account-research \
147
+ --client codex \
148
+ --session <session-id>
149
+ ```
150
+
151
+ The command prints a run ID. After completing the work, the agent uploads the
152
+ real client transcript file for that run:
153
+
154
+ ```bash
155
+ constraint skills run complete <run-id> <transcript-path>
156
+ ```
157
+
158
+ The CLI uploads the transcript exactly as it exists on disk. It does not create
159
+ a replacement transcript or modify the invoked skill.
160
+
161
+ Inspect completed runs:
162
+
163
+ ```bash
164
+ constraint skills run list
165
+ constraint skills run list --skill account-research
166
+ constraint skills run list --team sales_pipeline --user <user-id>
167
+ constraint skills run get <run-id>
168
+ constraint skills run get <run-id> --raw --output transcript.jsonl
169
+ ```
170
+
171
+ Team and user filters are enforced by Constraint's domain permissions.
172
+
173
+ ## Automation
174
+
175
+ Use `--json` for machine-readable output, `--quiet` to suppress normal output,
176
+ and `--yes` to accept deterministic installer defaults. Non-interactive
177
+ environments skip the terminal wizard.
178
+
179
+ ```bash
180
+ constraint skills install account-research \
181
+ --agent codex \
182
+ --project \
183
+ --yes \
184
+ --json
185
+ ```
186
+
187
+ ## Diagnostics
188
+
189
+ ```bash
190
+ constraint doctor
191
+ constraint doctor --agent codex --agent claude-code
192
+ ```
193
+
194
+ `doctor` reports API authentication and whether each agent has the companion
195
+ skill and managed instruction block.
196
+
197
+ ## License
198
+
199
+ Constraint CLI is proprietary software. Use is permitted only under the
200
+ [Constraint CLI Proprietary License](./LICENSE) and any applicable agreement
201
+ governing access to Constraint products and services.
202
+
203
+ Copyright (c) 2026 Constraint. All rights reserved.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from '../src/cli.js';
4
+
5
+ main(process.argv.slice(2)).catch((error) => {
6
+ const message = error instanceof Error ? error.message : String(error);
7
+ process.stderr.write(`Error: ${message}\n`);
8
+ process.exitCode = 1;
9
+ });
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@constraint/cli",
3
+ "version": "0.2.0",
4
+ "description": "Install, publish, and report Agent Skills with Constraint",
5
+ "type": "module",
6
+ "bin": { "constraint": "bin/constraint.js" },
7
+ "files": ["bin", "src", "skills"],
8
+ "scripts": {
9
+ "check": "node --check bin/constraint.js && node --check src/cli.js",
10
+ "test": "node --test"
11
+ },
12
+ "dependencies": {
13
+ "@clack/prompts": "1.7.0",
14
+ "picocolors": "1.1.1"
15
+ },
16
+ "engines": { "node": ">=20" },
17
+ "license": "SEE LICENSE IN LICENSE",
18
+ "publishConfig": { "access": "public", "provenance": true }
19
+ }
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: constraint-skills
3
+ description: Report Agent Skill invocations to Constraint. Use whenever an agent invokes a skill installed from or governed by Constraint, including nested or multiple skill invocations in one client session.
4
+ ---
5
+
6
+ # Constraint Skills
7
+
8
+ Record each invocation without editing the invoked skill or changing its work.
9
+
10
+ ## Start a run
11
+
12
+ Immediately after selecting and loading a skill, run:
13
+
14
+ ```bash
15
+ constraint skills run start <skill-name> --client <client> --session <session-id>
16
+ ```
17
+
18
+ Use the invoked skill's frontmatter `name`. Normalize the client to `codex` or
19
+ `claude-code`. Use the current client session identifier. Retain the exact run ID
20
+ printed by the command. If one session invokes multiple skills, start and retain
21
+ a distinct run ID for each invocation.
22
+
23
+ If the command fails, report the failure briefly and continue the user's work.
24
+ Do not fabricate a run ID.
25
+
26
+ ## Complete a run
27
+
28
+ After finishing the invoked skill's work and before ending the response, locate
29
+ the current session's real JSON or JSONL transcript file and run:
30
+
31
+ ```bash
32
+ constraint skills run complete <run-id> <transcript-path>
33
+ ```
34
+
35
+ Pass the explicit local path. The CLI uploads the whole transcript file as it
36
+ exists at that moment and completes only that run. Never create a replacement
37
+ transcript, summarize it, pass a temporary file, or guess a path.
38
+
39
+ For multiple invocations, complete each retained run separately. Overlapping
40
+ full-session transcript prefixes are expected. If the transcript path cannot be
41
+ determined, leave the run started and tell the user that reporting could not be
42
+ completed.
43
+
44
+ Do not run completion hooks, background flushes, or pending-run scans.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Constraint Skills"
3
+ short_description: "Report skill runs to Constraint"
4
+ default_prompt: "Use $constraint-skills to report this skill run to Constraint."
package/src/api.js ADDED
@@ -0,0 +1,37 @@
1
+ import { accessToken } from './auth.js';
2
+
3
+ export class ConstraintApiError extends Error {
4
+ constructor(status, code, message) {
5
+ super(message);
6
+ this.status = status;
7
+ this.code = code;
8
+ }
9
+ }
10
+
11
+ export async function apiRequest(config, requestPath, init = {}) {
12
+ const token = await accessToken(config);
13
+ const response = await fetch(`${config.apiUrl}${requestPath}`, {
14
+ ...init,
15
+ headers: { Accept: 'application/json', Authorization: `Bearer ${token}`, ...init.headers },
16
+ });
17
+ if (!response.ok) {
18
+ let data = null;
19
+ try {
20
+ data = await response.json();
21
+ } catch {
22
+ data = null;
23
+ }
24
+ throw new ConstraintApiError(
25
+ response.status,
26
+ data?.error?.code || 'request_failed',
27
+ data?.error?.message || `Constraint API request failed (${response.status})`,
28
+ );
29
+ }
30
+ if (init.rawResponse) return response;
31
+ if (response.status === 204) return null;
32
+ return response.json();
33
+ }
34
+
35
+ export function jsonRequest(body) {
36
+ return { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) };
37
+ }
@@ -0,0 +1,26 @@
1
+ export function takeFlag(args, name) {
2
+ const index = args.indexOf(name);
3
+ if (index < 0) return false;
4
+ args.splice(index, 1);
5
+ return true;
6
+ }
7
+
8
+ export function takeOption(args, name, { multiple = false } = {}) {
9
+ const values = [];
10
+ for (let index = 0; index < args.length; ) {
11
+ if (args[index] !== name) {
12
+ index += 1;
13
+ continue;
14
+ }
15
+ if (index + 1 >= args.length || args[index + 1].startsWith('--')) throw new Error(`${name} requires a value`);
16
+ values.push(args[index + 1]);
17
+ args.splice(index, 2);
18
+ if (!multiple) break;
19
+ }
20
+ return multiple ? values : values[0];
21
+ }
22
+
23
+ export function rejectUnknown(args) {
24
+ const unknown = args.find((value) => value.startsWith('-'));
25
+ if (unknown) throw new Error(`Unknown option: ${unknown}`);
26
+ }
@@ -0,0 +1,58 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ import { configDirectory } from './paths.js';
6
+
7
+ const SERVICE = 'com.constraint.cli';
8
+
9
+ function useKeychain() {
10
+ return process.platform === 'darwin' && process.env.CONSTRAINT_TOKEN_STORE !== 'file';
11
+ }
12
+
13
+ function account(apiUrl) {
14
+ return new URL(apiUrl).host;
15
+ }
16
+
17
+ function keychain(args) {
18
+ return spawnSync('security', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
19
+ }
20
+
21
+ function tokenFile() {
22
+ return path.join(configDirectory(), 'auth.json');
23
+ }
24
+
25
+ export async function readTokens(apiUrl) {
26
+ if (useKeychain()) {
27
+ const result = keychain(['find-generic-password', '-s', SERVICE, '-a', account(apiUrl), '-w']);
28
+ if (result.status === 0 && result.stdout.trim()) return JSON.parse(result.stdout.trim());
29
+ return null;
30
+ }
31
+ try {
32
+ return JSON.parse(await fs.readFile(tokenFile(), 'utf8'));
33
+ } catch (error) {
34
+ if (error && error.code === 'ENOENT') return null;
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ export async function writeTokens(apiUrl, tokens) {
40
+ if (useKeychain()) {
41
+ const result = keychain([
42
+ 'add-generic-password', '-U', '-s', SERVICE, '-a', account(apiUrl), '-w', JSON.stringify(tokens),
43
+ ]);
44
+ if (result.status !== 0) throw new Error(`Could not store login in macOS Keychain: ${result.stderr.trim()}`);
45
+ return;
46
+ }
47
+ await fs.mkdir(configDirectory(), { recursive: true, mode: 0o700 });
48
+ await fs.writeFile(tokenFile(), `${JSON.stringify(tokens)}\n`, { mode: 0o600 });
49
+ await fs.chmod(tokenFile(), 0o600);
50
+ }
51
+
52
+ export async function deleteTokens(apiUrl) {
53
+ if (useKeychain()) {
54
+ keychain(['delete-generic-password', '-s', SERVICE, '-a', account(apiUrl)]);
55
+ return;
56
+ }
57
+ await fs.rm(tokenFile(), { force: true });
58
+ }
package/src/auth.js ADDED
@@ -0,0 +1,103 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ import { deleteTokens, readTokens, writeTokens } from './auth-store.js';
4
+ import { fetchCliConfig } from './config.js';
5
+
6
+ const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
7
+
8
+ function sleep(milliseconds) {
9
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
10
+ }
11
+
12
+ function openBrowser(url) {
13
+ if (process.env.CONSTRAINT_NO_BROWSER === '1') return;
14
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
15
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
16
+ try {
17
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
18
+ child.unref();
19
+ } catch {
20
+ // The printed URL is always the fallback.
21
+ }
22
+ }
23
+
24
+ async function formPost(url, values) {
25
+ return fetch(url, {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
28
+ body: new URLSearchParams(values),
29
+ });
30
+ }
31
+
32
+ export async function login(config, output) {
33
+ const oauth = await fetchCliConfig(config.apiUrl);
34
+ const authorization = await formPost(oauth.device_authorization_endpoint, { client_id: oauth.workos_client_id });
35
+ const request = await authorization.json();
36
+ if (!authorization.ok) throw new Error(request.error_description || request.error || 'Device authorization failed');
37
+
38
+ output(`Open ${request.verification_uri}`);
39
+ output(`Enter code: ${request.user_code}`);
40
+ if (request.verification_uri_complete) openBrowser(request.verification_uri_complete);
41
+
42
+ let interval = Number(request.interval || 5);
43
+ const expiresAt = Date.now() + Number(request.expires_in || 300) * 1000;
44
+ while (Date.now() < expiresAt) {
45
+ await sleep(interval * 1000);
46
+ const response = await formPost(oauth.token_endpoint, {
47
+ grant_type: DEVICE_GRANT,
48
+ device_code: request.device_code,
49
+ client_id: oauth.workos_client_id,
50
+ });
51
+ const data = await response.json();
52
+ if (response.ok) {
53
+ await writeTokens(config.apiUrl, {
54
+ ...data,
55
+ client_id: oauth.workos_client_id,
56
+ token_endpoint: oauth.token_endpoint,
57
+ });
58
+ return data;
59
+ }
60
+ if (data.error === 'authorization_pending') continue;
61
+ if (data.error === 'slow_down') {
62
+ interval += 5;
63
+ continue;
64
+ }
65
+ throw new Error(data.error_description || data.error || 'Authentication failed');
66
+ }
67
+ throw new Error('Authentication timed out. Run constraint login again.');
68
+ }
69
+
70
+ function tokenExpiresSoon(token) {
71
+ try {
72
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
73
+ return !payload.exp || payload.exp * 1000 < Date.now() + 30_000;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ async function refresh(config, tokens) {
80
+ if (!tokens.refresh_token || !tokens.client_id || !tokens.token_endpoint) return null;
81
+ const response = await formPost(tokens.token_endpoint, {
82
+ grant_type: 'refresh_token',
83
+ refresh_token: tokens.refresh_token,
84
+ client_id: tokens.client_id,
85
+ });
86
+ if (!response.ok) return null;
87
+ const refreshed = await response.json();
88
+ const next = { ...tokens, ...refreshed };
89
+ await writeTokens(config.apiUrl, next);
90
+ return next;
91
+ }
92
+
93
+ export async function accessToken(config) {
94
+ let tokens = await readTokens(config.apiUrl);
95
+ if (!tokens?.access_token) throw new Error('Not logged in. Run constraint login.');
96
+ if (tokenExpiresSoon(tokens.access_token)) tokens = (await refresh(config, tokens)) || tokens;
97
+ if (tokenExpiresSoon(tokens.access_token)) throw new Error('Login expired. Run constraint login.');
98
+ return tokens.access_token;
99
+ }
100
+
101
+ export async function logout(config) {
102
+ await deleteTokens(config.apiUrl);
103
+ }