@yolo-labs/yolo-cli 0.23.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 +57 -0
- package/dist/artifact-get.js +131 -0
- package/dist/artifact-list.js +133 -0
- package/dist/auth-context.js +60 -0
- package/dist/canonicalizer.js +208 -0
- package/dist/cli.js +1403 -0
- package/dist/context.js +58 -0
- package/dist/deploy-bundle.js +394 -0
- package/dist/deploy-cli.js +1341 -0
- package/dist/deploy-client.js +460 -0
- package/dist/deploy-config.js +301 -0
- package/dist/deploy-detect.js +498 -0
- package/dist/deploy-ship.js +389 -0
- package/dist/lockfile.js +203 -0
- package/dist/plan-diff.js +162 -0
- package/dist/plan-export.js +261 -0
- package/dist/plan-frontmatter.schema.json +184 -0
- package/dist/plan-get.js +244 -0
- package/dist/plan-import.js +420 -0
- package/dist/plan-list.js +153 -0
- package/dist/plan-open.js +100 -0
- package/dist/plan-state.js +228 -0
- package/dist/plan-validate.js +270 -0
- package/dist/revision.js +43 -0
- package/dist/run-get.js +130 -0
- package/dist/run-lifecycle.js +133 -0
- package/dist/run-list.js +148 -0
- package/dist/run-start.js +110 -0
- package/dist/run-transfer.js +128 -0
- package/dist/serve.js +227 -0
- package/dist/tileapp-developer.js +222 -0
- package/dist/tileapp-oci-assembler.js +591 -0
- package/dist/tileapp-personal.js +461 -0
- package/dist/tileapp-publisher.js +134 -0
- package/dist/tileapp-validator.js +239 -0
- package/dist/vendored/flexdb.mjs +1087 -0
- package/dist/webapp-url.js +61 -0
- package/dist/work-client.js +195 -0
- package/package.json +39 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webapp URL derivation for CLI link-print.
|
|
3
|
+
*
|
|
4
|
+
* The substrate CLI runs in a container that has `YOLO_COMMON_API_URL`
|
|
5
|
+
* but no first-class webapp URL var. To keep `yolo plan import` / `yolo
|
|
6
|
+
* plan get` able to print a clickable Plan-DAG link without requiring
|
|
7
|
+
* operators to set a new env var, derive the webapp host from the
|
|
8
|
+
* common-api host: prod is `api.yolo.studio` → `yolo.studio`, staging
|
|
9
|
+
* is `api-staging.yolo.studio` → `staging.yolo.studio`. Operators can
|
|
10
|
+
* override via `YOLO_WEBAPP_URL` for local dev / custom deployments.
|
|
11
|
+
*
|
|
12
|
+
* When derivation fails (an unrecognized API URL shape, e.g. a raw
|
|
13
|
+
* localhost port), `deriveWebappUrl` returns null and the caller is
|
|
14
|
+
* expected to skip the URL line — printing a bogus URL would be worse
|
|
15
|
+
* than printing nothing.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveWebappUrl(input) {
|
|
18
|
+
if (input.webappUrlOverride && input.webappUrlOverride.length > 0) {
|
|
19
|
+
return stripTrailingSlash(input.webappUrlOverride);
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(input.commonApiUrl);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const host = parsed.hostname;
|
|
29
|
+
// Recognized prod shape: api.<rest> → <rest>
|
|
30
|
+
if (host.startsWith('api.')) {
|
|
31
|
+
return `${parsed.protocol}//${host.slice('api.'.length)}`;
|
|
32
|
+
}
|
|
33
|
+
// Recognized env-suffixed shape: api-<env>.<rest> → <env>.<rest>
|
|
34
|
+
const envMatch = /^api-([^.]+)\.(.+)$/.exec(host);
|
|
35
|
+
if (envMatch) {
|
|
36
|
+
return `${parsed.protocol}//${envMatch[1]}.${envMatch[2]}`;
|
|
37
|
+
}
|
|
38
|
+
// Unknown shape (raw localhost, IPs, custom hosts). Don't guess.
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convenience for the CLI flows: derive the webapp URL from
|
|
43
|
+
* `process.env`-style inputs and format the Plan-DAG path. Returns
|
|
44
|
+
* null when derivation fails so the caller can omit the URL line
|
|
45
|
+
* entirely.
|
|
46
|
+
*/
|
|
47
|
+
export function planDagUrl(env, workspaceId, planId) {
|
|
48
|
+
const commonApiUrl = env.YOLO_COMMON_API_URL || env.YOLO_API_URL;
|
|
49
|
+
if (!commonApiUrl)
|
|
50
|
+
return null;
|
|
51
|
+
const base = deriveWebappUrl({
|
|
52
|
+
commonApiUrl,
|
|
53
|
+
webappUrlOverride: env.YOLO_WEBAPP_URL,
|
|
54
|
+
});
|
|
55
|
+
if (!base)
|
|
56
|
+
return null;
|
|
57
|
+
return `${base}/workspaces/${workspaceId}/plans/${planId}`;
|
|
58
|
+
}
|
|
59
|
+
function stripTrailingSlash(s) {
|
|
60
|
+
return s.endsWith('/') ? s.slice(0, -1) : s;
|
|
61
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrate-CLI work client (Phase 8c.2).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the 8a Group 9 scaffold stub with the real auth flow:
|
|
5
|
+
*
|
|
6
|
+
* 1. `mintSubstrateToken` POSTs to `/internal/mcp/tokens` with
|
|
7
|
+
* `Authorization: Bearer <user JWT>` and the session-bound payload
|
|
8
|
+
* `{ sessionId, agentId: 'substrate-cli', scopes: [...] }`. The endpoint
|
|
9
|
+
* (`internalOrUserAuth`) verifies the JWT's userId owns the session,
|
|
10
|
+
* derives `workspaceId`, and returns a delegated JWT in `result.token`
|
|
11
|
+
* plus `result.claims.workspaceId`. The substrate CLI's lockfile is
|
|
12
|
+
* keyed by that workspaceId — `--workspace` is optional in containers
|
|
13
|
+
* because the session record IS the source of truth.
|
|
14
|
+
*
|
|
15
|
+
* 2. `authenticatedRequest` is the helper used by `yolo plan
|
|
16
|
+
* import/export` (8c.3+) to call `/internal/work/*`. It sends
|
|
17
|
+
* `Authorization: Bearer <delegated>` — the delegated MCP token is the
|
|
18
|
+
* capability (the per-agent allowedScopes check at the work routes is
|
|
19
|
+
* satisfied by the token's claims). The CLI is user-JWT-only; the
|
|
20
|
+
* INTERNAL_API_KEY / X-Internal-Auth path was removed.
|
|
21
|
+
*
|
|
22
|
+
* Both calls take an injectable `fetchImpl` so unit tests can stub
|
|
23
|
+
* the transport without touching real `globalThis.fetch`. In v1 the
|
|
24
|
+
* substrate CLI requires Node 20+, which guarantees `fetch` exists
|
|
25
|
+
* globally — `engines: ">=20"` is pinned in package.json.
|
|
26
|
+
*
|
|
27
|
+
* Substrate-CLI v1 scopes (capped per agents.json `substrate-cli`):
|
|
28
|
+
* - Plan authoring: work.create_plan, work.update_plan,
|
|
29
|
+
* work.get_plan, work.list_plans
|
|
30
|
+
* - Run lifecycle: work.start_run, work.get_run,
|
|
31
|
+
* work.list_runs, work.pause_run, work.resume_run,
|
|
32
|
+
* work.cancel_run, work.transfer_run_operator
|
|
33
|
+
* - Artifact reads: work.get_artifact, work.list_artifacts, work.transfer_run_operator
|
|
34
|
+
*
|
|
35
|
+
* Run-lifecycle implication: `work.start_run` binds the calling agent
|
|
36
|
+
* as the Run's Operator (route handler, Phase 4/6 R4). So a Run started
|
|
37
|
+
* via `yolo run start` has `operatorAgentId === 'substrate-cli'`, and
|
|
38
|
+
* only the substrate CLI can pause/resume/cancel it via MCP. The
|
|
39
|
+
* natural handoff loop is `yolo run transfer <runId> --to claude` —
|
|
40
|
+
* substrate-cli is the current Operator, claude/codex is an
|
|
41
|
+
* Operator-tier target, route's R4 self-transfer path applies.
|
|
42
|
+
* Substrate-cli is NOT itself Operator-tier (per
|
|
43
|
+
* `OPERATOR_TIER_AGENT_IDS` in operator-binding.ts), so `--to
|
|
44
|
+
* substrate-cli` is rejected by the route's `isValidOperatorTarget`
|
|
45
|
+
* check.
|
|
46
|
+
*/
|
|
47
|
+
export const SUBSTRATE_CLI_AGENT_ID = 'substrate-cli';
|
|
48
|
+
export const SUBSTRATE_CLI_PLAN_SCOPES = [
|
|
49
|
+
'work.create_plan',
|
|
50
|
+
'work.update_plan',
|
|
51
|
+
'work.get_plan',
|
|
52
|
+
'work.list_plans',
|
|
53
|
+
];
|
|
54
|
+
export const SUBSTRATE_CLI_RUN_SCOPES = [
|
|
55
|
+
'work.start_run',
|
|
56
|
+
'work.get_run',
|
|
57
|
+
'work.list_runs',
|
|
58
|
+
'work.pause_run',
|
|
59
|
+
'work.resume_run',
|
|
60
|
+
'work.cancel_run',
|
|
61
|
+
'work.transfer_run_operator',
|
|
62
|
+
];
|
|
63
|
+
export const SUBSTRATE_CLI_ARTIFACT_SCOPES = [
|
|
64
|
+
'work.get_artifact',
|
|
65
|
+
'work.list_artifacts',
|
|
66
|
+
];
|
|
67
|
+
export class WorkClientError extends Error {
|
|
68
|
+
status;
|
|
69
|
+
code;
|
|
70
|
+
constructor(message, status, code) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = 'WorkClientError';
|
|
73
|
+
this.status = status;
|
|
74
|
+
this.code = code;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Mint a session-bound delegated token for the substrate CLI.
|
|
79
|
+
* Server-derived workspaceId comes back in `claims.workspaceId`.
|
|
80
|
+
*
|
|
81
|
+
* Throws `WorkClientError` with `status` and `code` from the
|
|
82
|
+
* common-api error envelope (`{ error, code }`); networks failures
|
|
83
|
+
* surface as plain `Error` from the underlying fetch.
|
|
84
|
+
*/
|
|
85
|
+
export async function mintSubstrateToken(options) {
|
|
86
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
87
|
+
if (!fetchImpl) {
|
|
88
|
+
throw new WorkClientError('fetch is not available; substrate CLI requires Node 20+', 0, 'INTERNAL');
|
|
89
|
+
}
|
|
90
|
+
if (!options.userToken) {
|
|
91
|
+
throw new WorkClientError('no user token available to mint a substrate token', 0, 'INTERNAL');
|
|
92
|
+
}
|
|
93
|
+
const url = `${stripTrailingSlash(options.commonApiUrl)}/internal/mcp/tokens`;
|
|
94
|
+
const response = await fetchImpl(url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
Authorization: `Bearer ${options.userToken}`,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
sessionId: options.sessionId,
|
|
102
|
+
agentId: SUBSTRATE_CLI_AGENT_ID,
|
|
103
|
+
scopes: options.scopes,
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
let parsed = {};
|
|
108
|
+
try {
|
|
109
|
+
parsed = (await response.json());
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// body wasn't JSON — fall through to a generic message
|
|
113
|
+
}
|
|
114
|
+
throw new WorkClientError(parsed.error ?? `mint failed: HTTP ${response.status}`, response.status, parsed.code ?? 'INTERNAL');
|
|
115
|
+
}
|
|
116
|
+
const json = (await response.json());
|
|
117
|
+
if (typeof json.token !== 'string' || json.token.length === 0) {
|
|
118
|
+
throw new WorkClientError('mint response missing string `token`', 502, 'INTERNAL');
|
|
119
|
+
}
|
|
120
|
+
if (typeof json.expiresAt !== 'string' || json.expiresAt.length === 0) {
|
|
121
|
+
throw new WorkClientError('mint response missing string `expiresAt`', 502, 'INTERNAL');
|
|
122
|
+
}
|
|
123
|
+
if (typeof json.jti !== 'string' || json.jti.length === 0) {
|
|
124
|
+
throw new WorkClientError('mint response missing string `jti`', 502, 'INTERNAL');
|
|
125
|
+
}
|
|
126
|
+
const workspaceId = json.claims?.workspaceId;
|
|
127
|
+
if (typeof workspaceId !== 'string' || workspaceId.length === 0) {
|
|
128
|
+
throw new WorkClientError('mint response missing string `claims.workspaceId`', 502, 'INTERNAL');
|
|
129
|
+
}
|
|
130
|
+
const userId = json.claims?.userId;
|
|
131
|
+
if (typeof userId !== 'string' || userId.length === 0) {
|
|
132
|
+
throw new WorkClientError('mint response missing string `claims.userId`', 502, 'INTERNAL');
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
token: json.token,
|
|
136
|
+
expiresAt: json.expiresAt,
|
|
137
|
+
workspaceId,
|
|
138
|
+
userId,
|
|
139
|
+
jti: json.jti,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Make an authenticated request to `/internal/work/*` with
|
|
144
|
+
* `Authorization: Bearer <delegated token>` (the delegated MCP token is the
|
|
145
|
+
* capability; requireMcpAuth accepts it without any service header). `workPath`
|
|
146
|
+
* is appended verbatim to `${commonApiUrl}/internal/work` — start it with a
|
|
147
|
+
* slash, e.g., `/workspaces/${workspaceId}/plans/${planId}`.
|
|
148
|
+
*/
|
|
149
|
+
export async function authenticatedRequest(options, workPath, init = {}) {
|
|
150
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
151
|
+
if (!fetchImpl) {
|
|
152
|
+
throw new WorkClientError('fetch is not available; substrate CLI requires Node 20+', 0, 'INTERNAL');
|
|
153
|
+
}
|
|
154
|
+
const url = `${stripTrailingSlash(options.commonApiUrl)}/internal/work${workPath}`;
|
|
155
|
+
const headers = {
|
|
156
|
+
...(init.headers ?? {}),
|
|
157
|
+
Authorization: `Bearer ${options.delegatedToken}`,
|
|
158
|
+
};
|
|
159
|
+
const fetchInit = {
|
|
160
|
+
method: init.method ?? 'GET',
|
|
161
|
+
headers,
|
|
162
|
+
};
|
|
163
|
+
if (init.jsonBody !== undefined) {
|
|
164
|
+
headers['Content-Type'] = 'application/json';
|
|
165
|
+
fetchInit.body = JSON.stringify(init.jsonBody);
|
|
166
|
+
}
|
|
167
|
+
return fetchImpl(url, fetchInit);
|
|
168
|
+
}
|
|
169
|
+
export async function userRouteRequest(options, routePath, init = {}) {
|
|
170
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
171
|
+
if (!fetchImpl) {
|
|
172
|
+
throw new WorkClientError('fetch is not available; substrate CLI requires Node 20+', 0, 'INTERNAL');
|
|
173
|
+
}
|
|
174
|
+
if (!options.userToken) {
|
|
175
|
+
throw new WorkClientError('no user token available for user-route request', 0, 'INTERNAL');
|
|
176
|
+
}
|
|
177
|
+
const authHeaders = { Authorization: `Bearer ${options.userToken}` };
|
|
178
|
+
const url = `${stripTrailingSlash(options.commonApiUrl)}/v1${routePath}`;
|
|
179
|
+
const headers = {
|
|
180
|
+
...(init.headers ?? {}),
|
|
181
|
+
...authHeaders,
|
|
182
|
+
};
|
|
183
|
+
const fetchInit = {
|
|
184
|
+
method: init.method ?? 'GET',
|
|
185
|
+
headers,
|
|
186
|
+
};
|
|
187
|
+
if (init.jsonBody !== undefined) {
|
|
188
|
+
headers['Content-Type'] = 'application/json';
|
|
189
|
+
fetchInit.body = JSON.stringify(init.jsonBody);
|
|
190
|
+
}
|
|
191
|
+
return fetchImpl(url, fetchInit);
|
|
192
|
+
}
|
|
193
|
+
function stripTrailingSlash(url) {
|
|
194
|
+
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
195
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yolo-labs/yolo-cli",
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "YOLO Studio substrate CLI — owns yolo plan/workspace/artifact substrate-tooling commands. Distinct from yolo-code (the agent CLI) and yolo-router (the LLM gateway client).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"yolo": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**/*.js",
|
|
15
|
+
"!dist/**/*.test.js",
|
|
16
|
+
"dist/**/*.mjs",
|
|
17
|
+
"dist/**/*.json",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json && cp src/plan-frontmatter.schema.json dist/ && node scripts/build-vendored-flexdb.mjs",
|
|
22
|
+
"test": "npm run build && node --test --test-reporter=spec dist/*.test.js",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"prepublishOnly": "npm test"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"ajv": "^6.12.6",
|
|
31
|
+
"esbuild": "^0.24.0",
|
|
32
|
+
"js-yaml": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.4.0",
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"@types/js-yaml": "^4.0.9"
|
|
38
|
+
}
|
|
39
|
+
}
|