@ritualai/cli 0.3.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/README.md +120 -0
- package/dist/commands/doctor.js +87 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +158 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.js +63 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.js +25 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/refresh.js +42 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/agents/configure-mcp.js +127 -0
- package/dist/lib/agents/configure-mcp.js.map +1 -0
- package/dist/lib/agents/detector.js +32 -0
- package/dist/lib/agents/detector.js.map +1 -0
- package/dist/lib/agents/providers.js +118 -0
- package/dist/lib/agents/providers.js.map +1 -0
- package/dist/lib/api-client.js +120 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/config.js +154 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/oidc.js +213 -0
- package/dist/lib/oidc.js.map +1 -0
- package/dist/lib/pat-store.js +44 -0
- package/dist/lib/pat-store.js.map +1 -0
- package/dist/lib/skill-copy.js +57 -0
- package/dist/lib/skill-copy.js.map +1 -0
- package/package.json +59 -0
- package/skills/claude-code/ritual/SKILL.md +271 -0
- package/skills/codex/ritual/SKILL.md +271 -0
- package/skills/cursor/ritual/SKILL.md +271 -0
- package/skills/gemini/ritual/SKILL.md +271 -0
- package/skills/kiro/ritual/SKILL.md +271 -0
- package/skills/vscode/ritual/SKILL.md +271 -0
package/dist/lib/oidc.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RefreshTokenError = void 0;
|
|
4
|
+
exports.performLoginFlow = performLoginFlow;
|
|
5
|
+
exports.refreshTokens = refreshTokens;
|
|
6
|
+
exports.decodeJwtPayloadUnsafe = decodeJwtPayloadUnsafe;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_http_1 = require("node:http");
|
|
9
|
+
const node_url_1 = require("node:url");
|
|
10
|
+
const base64url = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
11
|
+
function generatePkce() {
|
|
12
|
+
const verifier = base64url((0, node_crypto_1.randomBytes)(32));
|
|
13
|
+
const challenge = base64url((0, node_crypto_1.createHash)('sha256').update(verifier).digest());
|
|
14
|
+
return { verifier, challenge };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Spin up a one-shot loopback server on a random port. Resolves with
|
|
18
|
+
* the captured `?code=...&state=...` query params from the OAuth
|
|
19
|
+
* redirect. Rejects on timeout (default 5 min) or error response.
|
|
20
|
+
*/
|
|
21
|
+
function awaitAuthorizationRedirect(timeoutMs = 5 * 60 * 1000) {
|
|
22
|
+
let port = 0;
|
|
23
|
+
let serverRef;
|
|
24
|
+
const promise = new Promise((resolve, reject) => {
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
serverRef.close();
|
|
27
|
+
reject(new Error('Login timed out — no callback received within 5 minutes'));
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
const server = (0, node_http_1.createServer)((req, res) => {
|
|
30
|
+
if (!req.url) {
|
|
31
|
+
res.writeHead(400).end();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const url = new node_url_1.URL(req.url, `http://127.0.0.1:${port}`);
|
|
35
|
+
if (url.pathname !== '/' && url.pathname !== '/callback') {
|
|
36
|
+
res.writeHead(404).end();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const code = url.searchParams.get('code');
|
|
40
|
+
const state = url.searchParams.get('state');
|
|
41
|
+
const errorParam = url.searchParams.get('error');
|
|
42
|
+
if (errorParam) {
|
|
43
|
+
const description = url.searchParams.get('error_description') ?? '';
|
|
44
|
+
res.writeHead(400, { 'Content-Type': 'text/html' }).end(htmlPage('Sign-in failed', `<p style="color:#dc2626">${escapeHtml(errorParam)}: ${escapeHtml(description)}</p>`));
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
serverRef.close();
|
|
47
|
+
reject(new Error(`OAuth error: ${errorParam}: ${description}`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!code || !state) {
|
|
51
|
+
res.writeHead(400).end('Missing code or state');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
res.writeHead(200, { 'Content-Type': 'text/html' }).end(htmlPage('Sign-in complete', `<p>You can close this tab and return to your terminal.</p>`));
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
serverRef.close();
|
|
57
|
+
resolve({ code, state });
|
|
58
|
+
});
|
|
59
|
+
server.listen(0, '127.0.0.1', () => {
|
|
60
|
+
const addr = server.address();
|
|
61
|
+
if (typeof addr === 'object' && addr) {
|
|
62
|
+
port = addr.port;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
server.close();
|
|
67
|
+
reject(new Error('Failed to bind loopback server'));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
serverRef = server;
|
|
71
|
+
});
|
|
72
|
+
// Caller needs port for the redirect URI; resolve via wrapper that
|
|
73
|
+
// settles synchronously after listen completes.
|
|
74
|
+
return { server: serverRef, port, promise };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run the full Auth Code + PKCE flow. Opens the user's browser to
|
|
78
|
+
* Keycloak; resolves with a TokenSet. Caller is responsible for
|
|
79
|
+
* persisting it (see lib/config.ts).
|
|
80
|
+
*/
|
|
81
|
+
async function performLoginFlow(cfg, openBrowser, log) {
|
|
82
|
+
const { verifier, challenge } = generatePkce();
|
|
83
|
+
const state = base64url((0, node_crypto_1.randomBytes)(16));
|
|
84
|
+
// Bind loopback server FIRST so we know which port to put in the
|
|
85
|
+
// redirect URI before sending the user off to Keycloak.
|
|
86
|
+
const { server, promise } = awaitAuthorizationRedirect();
|
|
87
|
+
// Wait one tick for `listen()` to populate the port.
|
|
88
|
+
await new Promise((r) => setImmediate(r));
|
|
89
|
+
const addr = server.address();
|
|
90
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
91
|
+
if (!port)
|
|
92
|
+
throw new Error('Failed to bind loopback server');
|
|
93
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
94
|
+
// Build the authorize URL.
|
|
95
|
+
const authorizeUrl = new node_url_1.URL(`${cfg.issuer}/protocol/openid-connect/auth`);
|
|
96
|
+
authorizeUrl.searchParams.set('response_type', 'code');
|
|
97
|
+
authorizeUrl.searchParams.set('client_id', cfg.clientId);
|
|
98
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
99
|
+
authorizeUrl.searchParams.set('scope', cfg.scope);
|
|
100
|
+
authorizeUrl.searchParams.set('state', state);
|
|
101
|
+
authorizeUrl.searchParams.set('code_challenge', challenge);
|
|
102
|
+
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
|
103
|
+
log(`Opening browser to ${cfg.issuer.replace(/^https?:\/\//, '')}…`);
|
|
104
|
+
log(`If the browser doesn't open, visit:\n ${authorizeUrl.toString()}\n`);
|
|
105
|
+
await openBrowser(authorizeUrl.toString()).catch(() => {
|
|
106
|
+
/* if `open` fails the user can copy-paste */
|
|
107
|
+
});
|
|
108
|
+
// Wait for the redirect back to localhost.
|
|
109
|
+
const result = await promise;
|
|
110
|
+
if (result.state !== state) {
|
|
111
|
+
throw new Error('OAuth state mismatch — possible CSRF attack. Aborting login. Please try again.');
|
|
112
|
+
}
|
|
113
|
+
// Exchange code for token.
|
|
114
|
+
const tokenUrl = `${cfg.issuer}/protocol/openid-connect/token`;
|
|
115
|
+
const body = new URLSearchParams({
|
|
116
|
+
grant_type: 'authorization_code',
|
|
117
|
+
client_id: cfg.clientId,
|
|
118
|
+
code: result.code,
|
|
119
|
+
redirect_uri: redirectUri,
|
|
120
|
+
code_verifier: verifier,
|
|
121
|
+
});
|
|
122
|
+
const res = await fetch(tokenUrl, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
125
|
+
body,
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const text = await res.text();
|
|
129
|
+
throw new Error(`Token exchange failed: HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
130
|
+
}
|
|
131
|
+
const tokenSet = (await res.json());
|
|
132
|
+
return tokenSet;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Exchange a refresh token for a fresh access token (and usually a
|
|
136
|
+
* fresh refresh token too — Keycloak rotates them by default).
|
|
137
|
+
*
|
|
138
|
+
* Spec: RFC 6749 §6 (Refreshing an Access Token).
|
|
139
|
+
*
|
|
140
|
+
* Throws if the refresh token is expired/revoked — caller should
|
|
141
|
+
* fall back to `performLoginFlow()` to get the user re-auth'd in
|
|
142
|
+
* the browser.
|
|
143
|
+
*/
|
|
144
|
+
async function refreshTokens(cfg, refreshToken) {
|
|
145
|
+
const tokenUrl = `${cfg.issuer}/protocol/openid-connect/token`;
|
|
146
|
+
const body = new URLSearchParams({
|
|
147
|
+
grant_type: 'refresh_token',
|
|
148
|
+
client_id: cfg.clientId,
|
|
149
|
+
refresh_token: refreshToken,
|
|
150
|
+
});
|
|
151
|
+
const res = await fetch(tokenUrl, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
154
|
+
body,
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const text = await res.text();
|
|
158
|
+
// 400 invalid_grant is the canonical "refresh token expired or
|
|
159
|
+
// revoked" response — surface a typed error so the caller can
|
|
160
|
+
// trigger a fresh `performLoginFlow()` cleanly.
|
|
161
|
+
throw new RefreshTokenError(`Refresh failed: HTTP ${res.status}: ${text.slice(0, 200)}`, res.status);
|
|
162
|
+
}
|
|
163
|
+
return (await res.json());
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Thrown by `refreshTokens()` when Keycloak rejects the refresh
|
|
167
|
+
* token. Carries the HTTP status so callers can distinguish a
|
|
168
|
+
* permanent failure (400 invalid_grant → re-login required) from
|
|
169
|
+
* a transient one (5xx → retry maybe).
|
|
170
|
+
*/
|
|
171
|
+
class RefreshTokenError extends Error {
|
|
172
|
+
status;
|
|
173
|
+
constructor(message, status) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.status = status;
|
|
176
|
+
this.name = 'RefreshTokenError';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
exports.RefreshTokenError = RefreshTokenError;
|
|
180
|
+
/**
|
|
181
|
+
* Decode a JWT's payload WITHOUT verifying its signature. The CLI
|
|
182
|
+
* validates by way of acquiring the token from Keycloak directly over
|
|
183
|
+
* TLS, then trusts the payload for display only ("logged in as
|
|
184
|
+
* alice@example.com"). For any actual authorization decision, call
|
|
185
|
+
* the API which verifies the signature against JWKS.
|
|
186
|
+
*/
|
|
187
|
+
function decodeJwtPayloadUnsafe(token) {
|
|
188
|
+
const parts = token.split('.');
|
|
189
|
+
if (parts.length !== 3)
|
|
190
|
+
return null;
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ─── small html helpers for the loopback browser response ──────────────────
|
|
199
|
+
function htmlPage(title, body) {
|
|
200
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title>
|
|
201
|
+
<style>body{font-family:system-ui,sans-serif;max-width:480px;margin:64px auto;padding:0 24px;color:#111}
|
|
202
|
+
h1{font-size:18px;font-weight:600;margin:0 0 8px}p{margin:8px 0;color:#555}</style></head>
|
|
203
|
+
<body><h1>${escapeHtml(title)}</h1>${body}</body></html>`;
|
|
204
|
+
}
|
|
205
|
+
function escapeHtml(s) {
|
|
206
|
+
return s
|
|
207
|
+
.replace(/&/g, '&')
|
|
208
|
+
.replace(/</g, '<')
|
|
209
|
+
.replace(/>/g, '>')
|
|
210
|
+
.replace(/"/g, '"')
|
|
211
|
+
.replace(/'/g, ''');
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=oidc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc.js","sourceRoot":"","sources":["../../src/lib/oidc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mintAgentPat = mintAgentPat;
|
|
4
|
+
exports.defaultAgentTokenName = defaultAgentTokenName;
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const api_client_1 = require("./api-client");
|
|
7
|
+
async function mintAgentPat(opts) {
|
|
8
|
+
const name = opts.nameOverride ?? defaultAgentTokenName();
|
|
9
|
+
let response;
|
|
10
|
+
try {
|
|
11
|
+
response = await opts.api.post('/auth/tokens', {
|
|
12
|
+
name,
|
|
13
|
+
scopes: opts.scopes ?? ['admin'],
|
|
14
|
+
// undefined → service-layer default; null → no expiry.
|
|
15
|
+
expiresInDays: opts.expiresInDays ?? 365,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err instanceof api_client_1.ApiError && err.status === 409) {
|
|
20
|
+
// Active-token cap hit. Re-surface with a hint about the
|
|
21
|
+
// management UI so the user can revoke an old one.
|
|
22
|
+
throw new Error('Personal access token limit reached. Revoke an old token at ' +
|
|
23
|
+
'https://app.ritualapp.cloud/settings/tokens and re-run `ritual init`.');
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
plaintextToken: response.plaintextToken,
|
|
29
|
+
id: response.token.id,
|
|
30
|
+
name: response.token.name,
|
|
31
|
+
expiresAt: response.token.expiresAt,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `Ritual CLI — <hostname>`, truncated to fit the API's 80-char limit
|
|
36
|
+
* on token names. Real hostnames are typically 10-30 chars so this is
|
|
37
|
+
* defensive.
|
|
38
|
+
*/
|
|
39
|
+
function defaultAgentTokenName() {
|
|
40
|
+
const host = (0, node_os_1.hostname)() || 'unknown-host';
|
|
41
|
+
const full = `Ritual CLI — ${host}`;
|
|
42
|
+
return full.length > 80 ? full.slice(0, 77) + '...' : full;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=pat-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pat-store.js","sourceRoot":"","sources":["../../src/lib/pat-store.ts"],"names":[],"mappings":";;AAoEA,oCA6BC;AAOD,sDAIC;AA5GD,qCAAmC;AACnC,6CAAmD;AAmE5C,KAAK,UAAU,YAAY,CAAC,IAAoB;IACtD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,IAAI,qBAAqB,EAAE,CAAC;IAE1D,IAAI,QAA2B,CAAC;IAChC,IAAI,CAAC;QACJ,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAAoB,cAAc,EAAE;YACjE,IAAI;YACJ,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC;YAChC,uDAAuD;YACvD,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,GAAG;SACxC,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,GAAG,YAAY,qBAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACnD,yDAAyD;YACzD,mDAAmD;YACnD,MAAM,IAAI,KAAK,CACd,8DAA8D;gBAC7D,uEAAuE,CACxE,CAAC;QACH,CAAC;QACD,MAAM,GAAG,CAAC;IACX,CAAC;IAED,OAAO;QACN,cAAc,EAAE,QAAQ,CAAC,cAAc;QACvC,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE;QACrB,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI;QACzB,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS;KACnC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAgB,qBAAqB;IACpC,MAAM,IAAI,GAAG,IAAA,kBAAQ,GAAE,IAAI,cAAc,CAAC;IAC1C,MAAM,IAAI,GAAG,gBAAgB,IAAI,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPackageRoot = getPackageRoot;
|
|
4
|
+
exports.copySkillsForProvider = copySkillsForProvider;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the npm package root (where the `skills/` bundle lives).
|
|
9
|
+
*
|
|
10
|
+
* At runtime, this file compiles to `dist/lib/skill-copy.js`. Package
|
|
11
|
+
* root is two `..` up from there.
|
|
12
|
+
*
|
|
13
|
+
* For dev (running via tsx from source), `__dirname` is `src/lib`, so
|
|
14
|
+
* the same `../..` walk lands at the package root. Same answer either
|
|
15
|
+
* way — no special-casing needed.
|
|
16
|
+
*/
|
|
17
|
+
function getPackageRoot() {
|
|
18
|
+
return (0, node_path_1.resolve)(__dirname, '..', '..');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Copy every skill bundled for `provider` into the project.
|
|
22
|
+
*
|
|
23
|
+
* `projectDir` is usually `process.cwd()` but parameterized so tests
|
|
24
|
+
* can write to a temp directory.
|
|
25
|
+
*/
|
|
26
|
+
function copySkillsForProvider(provider, projectDir, packageRoot = getPackageRoot()) {
|
|
27
|
+
const sourceBase = (0, node_path_1.join)(packageRoot, provider.packageSkillDir);
|
|
28
|
+
if (!(0, node_fs_1.existsSync)(sourceBase)) {
|
|
29
|
+
return {
|
|
30
|
+
provider,
|
|
31
|
+
copied: 0,
|
|
32
|
+
reason: `package missing ${provider.packageSkillDir}/ — try reinstalling @ritualai/cli`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const skillDirs = (0, node_fs_1.readdirSync)(sourceBase, { withFileTypes: true })
|
|
36
|
+
.filter((d) => d.isDirectory())
|
|
37
|
+
.map((d) => d.name);
|
|
38
|
+
if (skillDirs.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
provider,
|
|
41
|
+
copied: 0,
|
|
42
|
+
reason: 'no skills found in package bundle',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const targetBase = (0, node_path_1.join)(projectDir, provider.projectSkillDir);
|
|
46
|
+
for (const name of skillDirs) {
|
|
47
|
+
const src = (0, node_path_1.join)(sourceBase, name);
|
|
48
|
+
const dest = (0, node_path_1.join)(targetBase, name);
|
|
49
|
+
(0, node_fs_1.mkdirSync)(dest, { recursive: true });
|
|
50
|
+
// `force: true` overwrites; `recursive: true` walks the whole
|
|
51
|
+
// skill directory (covers SKILL.md + any sibling assets like
|
|
52
|
+
// nested examples/ or templates/).
|
|
53
|
+
(0, node_fs_1.cpSync)(src, dest, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
return { provider, copied: skillDirs.length };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=skill-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-copy.js","sourceRoot":"","sources":["../../src/lib/skill-copy.ts"],"names":[],"mappings":";;AA0CA,wCAEC;AAQD,sDAwCC;AA5FD,qCAAqE;AACrE,yCAA0C;AA+B1C;;;;;;;;;GASG;AACH,SAAgB,cAAc;IAC7B,OAAO,IAAA,mBAAO,EAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED;;;;;GAKG;AACH,SAAgB,qBAAqB,CACpC,QAAuB,EACvB,UAAkB,EAClB,cAAsB,cAAc,EAAE;IAEtC,MAAM,UAAU,GAAG,IAAA,gBAAI,EAAC,WAAW,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IAE/D,IAAI,CAAC,IAAA,oBAAU,EAAC,UAAU,CAAC,EAAE,CAAC;QAC7B,OAAO;YACN,QAAQ;YACR,MAAM,EAAE,CAAC;YACT,MAAM,EAAE,mBAAmB,QAAQ,CAAC,eAAe,oCAAoC;SACvF,CAAC;IACH,CAAC;IAED,MAAM,SAAS,GAAG,IAAA,qBAAW,EAAC,UAAU,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;SAChE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;SAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAErB,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO;YACN,QAAQ;YACR,MAAM,EAAE,CAAC;YACT,MAAM,EAAE,mCAAmC;SAC3C,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAA,gBAAI,EAAC,UAAU,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IAE9D,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,IAAA,gBAAI,EAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,IAAA,gBAAI,EAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACpC,IAAA,mBAAS,EAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,8DAA8D;QAC9D,6DAA6D;QAC7D,mCAAmC;QACnC,IAAA,gBAAM,EAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;AAC/C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ritualai/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Ritual CLI — scaffold AI coding agent skills + register MCP servers. Connects Claude Code, Cursor, Windsurf, Kiro, Gemini CLI, VS Code/Copilot, and Codex to Ritual Cloud.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ritual": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"skills",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build:skills": "node scripts/build-skills.js",
|
|
18
|
+
"build:ts": "tsc -p tsconfig.json",
|
|
19
|
+
"build": "pnpm run build:skills && pnpm run build:ts",
|
|
20
|
+
"dev": "tsx src/index.ts",
|
|
21
|
+
"clean": "rm -rf dist skills",
|
|
22
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
23
|
+
"test": "jest --passWithNoTests",
|
|
24
|
+
"prepublishOnly": "pnpm run clean && pnpm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^14.0.3",
|
|
28
|
+
"open": "^10.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/jest": "^29.5.0",
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"ts-jest": "^29.1.0",
|
|
35
|
+
"tsx": "^4.19.0",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20.0.0"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"ritual",
|
|
43
|
+
"mcp",
|
|
44
|
+
"ai-coding-agent",
|
|
45
|
+
"claude-code",
|
|
46
|
+
"cursor",
|
|
47
|
+
"windsurf",
|
|
48
|
+
"kiro",
|
|
49
|
+
"gemini-cli",
|
|
50
|
+
"copilot",
|
|
51
|
+
"codex"
|
|
52
|
+
],
|
|
53
|
+
"jest": {
|
|
54
|
+
"preset": "ts-jest",
|
|
55
|
+
"testEnvironment": "node",
|
|
56
|
+
"roots": ["<rootDir>/src", "<rootDir>/test"],
|
|
57
|
+
"testMatch": ["**/?(*.)+(spec|test).ts"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ritual
|
|
3
|
+
description: "Top-level Ritual workflow dispatcher. Use when the engineer wants to start, run, check, or accept a Ritual exploration. Subcommands today: `build` (free-form problem → accepted recommendations, end-to-end). More land as they're built."
|
|
4
|
+
argument-hint: "[subcommand] <args> (e.g. 'build Reduce T2 churn in Q3')"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /ritual
|
|
9
|
+
|
|
10
|
+
Top-level dispatcher for vNext Ritual workflows. The first whitespace-delimited token of the argument selects a subcommand; everything after is passed to that subcommand.
|
|
11
|
+
|
|
12
|
+
## Routing
|
|
13
|
+
|
|
14
|
+
Parse the first token of the argument:
|
|
15
|
+
|
|
16
|
+
| First token | Route to |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `build` | the [build flow](#ritual-build) below |
|
|
19
|
+
| (anything else, OR no subcommand) | default to `build` and treat the entire argument as the problem statement |
|
|
20
|
+
|
|
21
|
+
Future subcommands (`run`, `status`, `recs`) will be added as their workflows are built. When you add a new subcommand, extend the routing table above and add a `## /ritual <subcommand>` section below.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## /ritual build
|
|
26
|
+
|
|
27
|
+
Walks the engineer from a free-form problem statement to vetted, accepted Ritual recommendations using the vNext MCP tool surface.
|
|
28
|
+
|
|
29
|
+
Output: **an exploration in COMPLETE state with accepted recommendations**, suitable as input to downstream implementation skills.
|
|
30
|
+
|
|
31
|
+
### Vocabulary mapping (engineer-tone)
|
|
32
|
+
|
|
33
|
+
The Ritual data model uses product-research terminology. This skill translates to engineer-natural terms when speaking to the user. The underlying API still uses original names — keep this table mental when you read tool inputs / outputs:
|
|
34
|
+
|
|
35
|
+
| User-facing term (the skill says) | Tool field name (the API uses) |
|
|
36
|
+
|---|---|
|
|
37
|
+
| **scope** | `problemStatement` |
|
|
38
|
+
| **sub-problem** | `consideration` |
|
|
39
|
+
| **matter** | `matter` (no rename — already the right word) |
|
|
40
|
+
| **discovery question** | `question` |
|
|
41
|
+
| **recommendation** | `recommendation` |
|
|
42
|
+
|
|
43
|
+
When the user says "tighten the scope," call `generate_problem_statement(...)` with their refinement. When you tell the user "I picked these sub-problems," you mean the items you put in `considerations[]`. The tools don't change; only the language to the user does.
|
|
44
|
+
|
|
45
|
+
### When to use
|
|
46
|
+
|
|
47
|
+
- The user describes a problem they want explored ("we need to figure out X", "let's scope Y", "I want recommendations on Z")
|
|
48
|
+
- The user wants the full pipeline — sub-problems → scope → discovery → recommendations — not just one step
|
|
49
|
+
|
|
50
|
+
When **not** to use:
|
|
51
|
+
- An exploration already exists with recommendations — fetch directly via `get_exploration` + `get_recommendations`
|
|
52
|
+
- The user wants to *implement* a feature from existing recommendations — use `/ritual-builder-spec` (from `@ritual-ai/cli`)
|
|
53
|
+
|
|
54
|
+
### Workflow — 7 phases
|
|
55
|
+
|
|
56
|
+
Each phase has explicit **[USER PAUSE]** points — never skip them.
|
|
57
|
+
|
|
58
|
+
#### Phase 1 — Pick a workspace
|
|
59
|
+
|
|
60
|
+
Call `mcp__ritual__list_workspaces`. Present as a numbered list (id, name).
|
|
61
|
+
|
|
62
|
+
If the user only has one project workspace, confirm it directly: "I'll create this in *Acme Engineering*. Sound right?" — but still wait for **[USER PAUSE]**.
|
|
63
|
+
|
|
64
|
+
Store `workspace_id`.
|
|
65
|
+
|
|
66
|
+
#### Phase 2 — Generate sub-problems
|
|
67
|
+
|
|
68
|
+
Call `mcp__ritual__generate_considerations` with:
|
|
69
|
+
- `workspace_id`
|
|
70
|
+
- `raw_input` (the problem from the slash-command argument or chat)
|
|
71
|
+
- `template_id` (omit unless the user specified one)
|
|
72
|
+
|
|
73
|
+
LLM call, ~5-10s. Returns 5-6 sub-problems — different framing axes the agent should investigate.
|
|
74
|
+
|
|
75
|
+
**[USER PAUSE]** Present as a numbered list and ask which to include:
|
|
76
|
+
|
|
77
|
+
> Sub-problems the system identified:
|
|
78
|
+
>
|
|
79
|
+
> 1. {sub-problem 1}
|
|
80
|
+
> 2. {sub-problem 2}
|
|
81
|
+
> ...
|
|
82
|
+
>
|
|
83
|
+
> Which should we factor into the scope? Pick any subset, or "all".
|
|
84
|
+
|
|
85
|
+
Store the picked sub-problems (text strings — they'll go into `considerations[]` next call).
|
|
86
|
+
|
|
87
|
+
#### Phase 3 — Generate scope
|
|
88
|
+
|
|
89
|
+
Call `mcp__ritual__generate_problem_statement` with:
|
|
90
|
+
- `workspace_id`
|
|
91
|
+
- `raw_input`
|
|
92
|
+
- `considerations` (the picks from Phase 2 — these are the sub-problems we just discussed)
|
|
93
|
+
- `template_id` (same as Phase 2 if used)
|
|
94
|
+
|
|
95
|
+
Returns a polished "How might we ..." style scope (typically <800 chars) plus optional follow-up questions and quality scores.
|
|
96
|
+
|
|
97
|
+
**[USER PAUSE]** Present and ask:
|
|
98
|
+
|
|
99
|
+
> Here's the scope:
|
|
100
|
+
>
|
|
101
|
+
> > **{generated scope}**
|
|
102
|
+
>
|
|
103
|
+
> Looks good? Want to tighten / broaden / change the audience? Or "ship it" to lock it in.
|
|
104
|
+
|
|
105
|
+
If refinement requested: regenerate with the refinement appended to `raw_input`. Iterate until accepted.
|
|
106
|
+
|
|
107
|
+
Store the final scope as `problem_statement` for the next call.
|
|
108
|
+
|
|
109
|
+
#### Phase 4 — Create the exploration
|
|
110
|
+
|
|
111
|
+
Generate a short name (≤60 chars) from the scope — typically the noun phrase, not the full HMW. E.g. "Reduce T2 customer churn in Q3" → name `T2 churn reduction (Q3)`.
|
|
112
|
+
|
|
113
|
+
**[USER PAUSE]** "I'll create the exploration as **T2 churn reduction (Q3)**. Proceed?"
|
|
114
|
+
|
|
115
|
+
Call `mcp__ritual__create_exploration` with:
|
|
116
|
+
- `workspace_id`
|
|
117
|
+
- `name`
|
|
118
|
+
- `problem_statement` (the scope from Phase 3)
|
|
119
|
+
- `agentic: false` — **do NOT** pass `agentic: true`. We want explicit per-step control so the user gets to pick discovery questions in Phase 5. Auto-agentic skips that.
|
|
120
|
+
|
|
121
|
+
Store `exploration_id`. Show the URL:
|
|
122
|
+
|
|
123
|
+
> Exploration created. Track progress at https://dev.ritualapp.cloud/e/{exploration_id}
|
|
124
|
+
|
|
125
|
+
#### Phase 5 — Discovery questions
|
|
126
|
+
|
|
127
|
+
Longest phase because generation is async + the user picks per-matter.
|
|
128
|
+
|
|
129
|
+
##### 5.1 — Kick off
|
|
130
|
+
|
|
131
|
+
Call `mcp__ritual__suggest_discovery_questions(exploration_id)`. Returns immediately with `task_id`. Tell the user:
|
|
132
|
+
|
|
133
|
+
> Generating discovery questions for each matter… 30-120 seconds.
|
|
134
|
+
|
|
135
|
+
##### 5.2 — Poll until ready
|
|
136
|
+
|
|
137
|
+
Loop:
|
|
138
|
+
- Call `mcp__ritual__get_discovery_state(exploration_id)`
|
|
139
|
+
- If `ready: false`, wait 5 seconds, poll again
|
|
140
|
+
- If `ready: true`, exit loop
|
|
141
|
+
|
|
142
|
+
Don't poll faster than every 5 seconds. Update the user every ~30 seconds so they know you haven't stalled.
|
|
143
|
+
|
|
144
|
+
##### 5.3 — Present matters and collect picks
|
|
145
|
+
|
|
146
|
+
The state contains `matters[]`, each with `id`, `name`, and `questions[]`. Walk through them **one matter at a time**:
|
|
147
|
+
|
|
148
|
+
> **{matter.name}** — {matter.questions.length} discovery questions:
|
|
149
|
+
>
|
|
150
|
+
> 1. {question 1}
|
|
151
|
+
> 2. {question 2}
|
|
152
|
+
> ...
|
|
153
|
+
>
|
|
154
|
+
> Which should the agent investigate? Pick any subset, "all", or "skip" to skip this matter.
|
|
155
|
+
|
|
156
|
+
**[USER PAUSE]** Wait per matter.
|
|
157
|
+
|
|
158
|
+
##### 5.4 — Commit picks per-matter
|
|
159
|
+
|
|
160
|
+
For each matter where the user picked at least one question, call `mcp__ritual__accept_discovery_questions` with:
|
|
161
|
+
- `state_id` (from the discovery state)
|
|
162
|
+
- `matter_id`
|
|
163
|
+
- `question_ids[]` (the picks for THIS matter)
|
|
164
|
+
|
|
165
|
+
This MUST be one call per matter — the API enforces per-matter atomicity. Don't bundle across matters.
|
|
166
|
+
|
|
167
|
+
##### 5.5 — Optional: capture out-of-scope items
|
|
168
|
+
|
|
169
|
+
If the user mentioned things they DON'T want investigated ("don't touch enterprise SSO", "skip pricing"), capture them as anti-goals.
|
|
170
|
+
|
|
171
|
+
Call `mcp__ritual__set_anti_goals(exploration_id, [{ text, reason? }, ...])`.
|
|
172
|
+
|
|
173
|
+
Skip silently if no anti-goals were mentioned.
|
|
174
|
+
|
|
175
|
+
#### Phase 6 — Run the agentic pipeline
|
|
176
|
+
|
|
177
|
+
Call `mcp__ritual__start_agentic_run` with:
|
|
178
|
+
- `scope_type: 'exploration'`
|
|
179
|
+
- `exploration_id`
|
|
180
|
+
|
|
181
|
+
Returns `run_id`. Starts the full pipeline (sourcing → answers → recommendations) in the background. Typical runtime: 3-8 minutes.
|
|
182
|
+
|
|
183
|
+
Poll `mcp__ritual__get_agentic_run(run_id)` every 15 seconds. Show progress when it changes:
|
|
184
|
+
|
|
185
|
+
> Agentic run: {progress_pct}% — {current_step}
|
|
186
|
+
|
|
187
|
+
When `status` is `COMPLETED`: continue to Phase 7.
|
|
188
|
+
When `status` is `COMPLETED_WITH_ERRORS`: tell the user, but proceed — partial recommendations may still be useful.
|
|
189
|
+
When `status` is `FAILED`: surface the error message, ask if they want to retry (`start_agentic_run` again with same exploration_id) or stop.
|
|
190
|
+
|
|
191
|
+
If user wants to abort mid-flight: `mcp__ritual__cancel_agentic_run(run_id)`.
|
|
192
|
+
|
|
193
|
+
#### Phase 7 — Review and accept recommendations
|
|
194
|
+
|
|
195
|
+
Call `mcp__ritual__get_recommendations(exploration_id)`. Response is an array of recommendations with id, title, status, content, reasoning.
|
|
196
|
+
|
|
197
|
+
**[USER PAUSE]** Present as a numbered list with title + 1-line summary:
|
|
198
|
+
|
|
199
|
+
> {N} recommendations:
|
|
200
|
+
>
|
|
201
|
+
> 1. **{title}** — {one-line from content}
|
|
202
|
+
> 2. **{title}** — {one-line from content}
|
|
203
|
+
> ...
|
|
204
|
+
>
|
|
205
|
+
> Want detail on any of them? Or ready to accept the set?
|
|
206
|
+
|
|
207
|
+
If detail requested: show full `content` and `reasoning_chain` (if present). Loop until done.
|
|
208
|
+
|
|
209
|
+
When the user is ready:
|
|
210
|
+
|
|
211
|
+
**[USER PAUSE]** "Accept all and mark the exploration complete?"
|
|
212
|
+
|
|
213
|
+
If yes, call `mcp__ritual__accept_recommendations(exploration_id)`. Response includes counts (`promoted`, `alreadyApproved`, `skipped`, `transitionedToComplete`).
|
|
214
|
+
|
|
215
|
+
Show the final state:
|
|
216
|
+
|
|
217
|
+
> Done. {N} recommendations accepted. Exploration is now COMPLETE.
|
|
218
|
+
>
|
|
219
|
+
> View at: https://dev.ritualapp.cloud/e/{exploration_id}
|
|
220
|
+
|
|
221
|
+
### Failure modes & recovery
|
|
222
|
+
|
|
223
|
+
**Discovery generation hangs (>5 min polling without `ready: true`)**: ask the user — wait longer? retry (`suggest_discovery_questions` again, new task)? or skip discovery entirely (proceed to Phase 6 without picked questions)?
|
|
224
|
+
|
|
225
|
+
**Agentic run fails or stalls**: surface the error, offer retry or stop.
|
|
226
|
+
|
|
227
|
+
**Discovery state ready=true with zero matters**: rare but possible if the LLM produced a malformed state. Retry by calling `suggest_discovery_questions` again.
|
|
228
|
+
|
|
229
|
+
**LLM-cost / quota errors (HTTP 429)**: tell the user explicitly. Do NOT auto-retry — quota issues need a human decision (different model tier, wait, top up).
|
|
230
|
+
|
|
231
|
+
### Tools used
|
|
232
|
+
|
|
233
|
+
This subcommand exclusively uses vNext MCP tools, in the order they appear:
|
|
234
|
+
|
|
235
|
+
1. `mcp__ritual__list_workspaces` (Phase 1)
|
|
236
|
+
2. `mcp__ritual__generate_considerations` (Phase 2)
|
|
237
|
+
3. `mcp__ritual__generate_problem_statement` (Phase 3)
|
|
238
|
+
4. `mcp__ritual__create_exploration` (Phase 4)
|
|
239
|
+
5. `mcp__ritual__suggest_discovery_questions` (Phase 5.1)
|
|
240
|
+
6. `mcp__ritual__get_discovery_state` (Phase 5.2)
|
|
241
|
+
7. `mcp__ritual__accept_discovery_questions` (Phase 5.4)
|
|
242
|
+
8. `mcp__ritual__set_anti_goals` (Phase 5.5, optional)
|
|
243
|
+
9. `mcp__ritual__start_agentic_run` (Phase 6)
|
|
244
|
+
10. `mcp__ritual__get_agentic_run` (Phase 6 polling)
|
|
245
|
+
11. `mcp__ritual__cancel_agentic_run` (Phase 6, only on user abort)
|
|
246
|
+
12. `mcp__ritual__get_recommendations` (Phase 7)
|
|
247
|
+
13. `mcp__ritual__accept_recommendations` (Phase 7)
|
|
248
|
+
|
|
249
|
+
13 of the 19 vNext tools. The other 6 (`ping`, `get_exploration`, `list_templates`, `list_agentic_runs`, `add_collaborator`, `check_anti_goals`) are situational, not part of the linear build flow.
|
|
250
|
+
|
|
251
|
+
### After this subcommand
|
|
252
|
+
|
|
253
|
+
When `/ritual build` completes, the exploration is in COMPLETE state with accepted recommendations. Downstream:
|
|
254
|
+
|
|
255
|
+
1. `/ritual-builder-spec <feature description>` — generates a build brief from the recommendations + a codebase-grounded plan
|
|
256
|
+
2. `/ritual-build <feature description>` (legacy variant from `@ritual-ai/cli`) — implements the plan, commits per logical unit, drafts a PR description
|
|
257
|
+
|
|
258
|
+
Naming overlap note: the legacy `/ritual-build` (in the public CLI package) is the *implementation* skill — feature description → committed code. This new `/ritual build` is the *exploration* skill — problem statement → accepted recommendations. Different scopes, similar names. Future cleanup may align them.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## /ritual run, /ritual status, /ritual recs
|
|
263
|
+
|
|
264
|
+
Not yet built. When they are, add a `## /ritual <name>` section to this file and update the routing table at the top.
|
|
265
|
+
|
|
266
|
+
Conceptual scope (when built):
|
|
267
|
+
- `/ritual run <exploration_id>` — kick off agentic run on an existing exploration, poll, present recommendations. Subset of `build` starting at Phase 6.
|
|
268
|
+
- `/ritual status <id>` — quick status check on an exploration or run. No interaction.
|
|
269
|
+
- `/ritual recs <exploration_id>` — fetch + present recommendations. No interaction.
|
|
270
|
+
|
|
271
|
+
For now, if the user asks for these, tell them politely: "That subcommand isn't built yet — for status, use `mcp__ritual__get_exploration` directly; for recs, use `mcp__ritual__get_recommendations` directly."
|