@voltade/volty 0.1.19 → 0.1.20
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/dist/volty.js +141 -66
- package/package.json +1 -1
- package/skill/volty-cli-ops/SKILL.md +1 -0
package/dist/volty.js
CHANGED
|
@@ -6,6 +6,7 @@ import { homedir as homedir3 } from "node:os";
|
|
|
6
6
|
import { join as join4 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// src/api.ts
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { tmpdir } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
@@ -41,51 +42,36 @@ async function apiFetch(ctx, path, init) {
|
|
|
41
42
|
} catch {}
|
|
42
43
|
return { ok: res.ok, status: res.status, body };
|
|
43
44
|
}
|
|
44
|
-
var
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
var keyTag = (ctx) => createHash("sha256").update(ctx.apiKey ?? "").digest("hex").slice(0, 12);
|
|
46
|
+
var cachePath = (env, ctx) => join(tmpdir(), `volty-catalog-${env}-${targetOrg ?? "default"}-${keyTag(ctx)}.json`);
|
|
47
|
+
function refusalSuffix(res, env) {
|
|
48
|
+
if (res.body.error?.message)
|
|
49
|
+
return `: ${res.body.error.message}`;
|
|
50
|
+
return res.status === 401 ? `: this key is not valid any more, run \`volty auth login --env ${env}\`` : "";
|
|
47
51
|
}
|
|
48
52
|
async function fetchCatalog(ctx, env, useCache = true) {
|
|
49
|
-
const path = cachePath(env);
|
|
53
|
+
const path = cachePath(env, ctx);
|
|
50
54
|
if (useCache && existsSync(path)) {
|
|
51
55
|
try {
|
|
52
56
|
const cached = JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
-
if (Date.now() - cached.at < CATALOG_TTL_MS && cached.renamed && cached.inApp)
|
|
57
|
+
if (Date.now() - cached.at < CATALOG_TTL_MS && cached.renamed && cached.inApp && cached.outOfScope)
|
|
54
58
|
return cached;
|
|
55
59
|
} catch {}
|
|
56
60
|
}
|
|
57
61
|
const res = await apiFetch(ctx, "/api/v/_catalog");
|
|
58
62
|
if (!res.ok || !res.body.catalog) {
|
|
59
|
-
throw new Error(`could not fetch the operation catalog (HTTP ${res.status})${res
|
|
63
|
+
throw new Error(`could not fetch the operation catalog (HTTP ${res.status})${refusalSuffix(res, env)}`);
|
|
60
64
|
}
|
|
61
65
|
const ops = res.body.catalog;
|
|
62
66
|
const retired = res.body.retired ?? {};
|
|
63
67
|
const renamed = res.body.renamed ?? {};
|
|
64
68
|
const inApp = res.body.inApp ?? {};
|
|
69
|
+
const outOfScope = res.body.outOfScope ?? {};
|
|
65
70
|
try {
|
|
66
71
|
mkdirSync(tmpdir(), { recursive: true });
|
|
67
|
-
writeFileSync(path, JSON.stringify({ at: Date.now(), ops, retired, renamed, inApp }));
|
|
72
|
+
writeFileSync(path, JSON.stringify({ at: Date.now(), ops, retired, renamed, inApp, outOfScope }));
|
|
68
73
|
} catch {}
|
|
69
|
-
return { ops, retired, renamed, inApp };
|
|
70
|
-
}
|
|
71
|
-
async function fetchOrgs(ctx, env, useCache = true) {
|
|
72
|
-
const path = join(tmpdir(), `volty-orgs-${env}.json`);
|
|
73
|
-
if (useCache && existsSync(path)) {
|
|
74
|
-
try {
|
|
75
|
-
const cached = JSON.parse(readFileSync(path, "utf8"));
|
|
76
|
-
if (Date.now() - cached.at < CATALOG_TTL_MS)
|
|
77
|
-
return cached.orgs;
|
|
78
|
-
} catch {}
|
|
79
|
-
}
|
|
80
|
-
const res = await apiFetch(ctx, "/api/cli/orgs");
|
|
81
|
-
if (!res.ok || !res.body.orgs) {
|
|
82
|
-
throw new Error(`could not fetch your org memberships (HTTP ${res.status})${res.status === 401 ? staleKeyHint(env) : ""}`);
|
|
83
|
-
}
|
|
84
|
-
const orgs = res.body.orgs;
|
|
85
|
-
try {
|
|
86
|
-
writeFileSync(path, JSON.stringify({ at: Date.now(), orgs }));
|
|
87
|
-
} catch {}
|
|
88
|
-
return orgs;
|
|
74
|
+
return { ops, retired, renamed, inApp, outOfScope };
|
|
89
75
|
}
|
|
90
76
|
function pathForOp(name) {
|
|
91
77
|
return `/api/v/${name.trim().split(/\s+/).join("/")}`;
|
|
@@ -166,16 +152,59 @@ function ensureContext(config, env) {
|
|
|
166
152
|
return ctx;
|
|
167
153
|
}
|
|
168
154
|
|
|
155
|
+
// src/org-keys.ts
|
|
156
|
+
function storedKeyFor(ctx, org, now = Date.now()) {
|
|
157
|
+
if (!ctx.apiKey)
|
|
158
|
+
return null;
|
|
159
|
+
if (!org || org === ctx.organizationId || org === ctx.organizationSlug) {
|
|
160
|
+
return { apiKey: ctx.apiKey, organizationId: ctx.organizationId };
|
|
161
|
+
}
|
|
162
|
+
for (const [organizationId, k] of Object.entries(ctx.orgKeys ?? {})) {
|
|
163
|
+
if (organizationId !== org && k.slug !== org)
|
|
164
|
+
continue;
|
|
165
|
+
if (k.expiresAt && Date.parse(k.expiresAt) <= now)
|
|
166
|
+
return null;
|
|
167
|
+
return { apiKey: k.apiKey, organizationId };
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
async function ensureOrgKey(ctx, org, onMint) {
|
|
172
|
+
const stored = storedKeyFor(ctx, org);
|
|
173
|
+
if (stored)
|
|
174
|
+
return { ...stored, minted: false };
|
|
175
|
+
setOrgOverride(undefined);
|
|
176
|
+
const res = await apiFetch(ctx, "/api/cli/keys", { method: "POST", body: { org } });
|
|
177
|
+
if (!res.ok || !res.body.apiKey || !res.body.organizationId) {
|
|
178
|
+
throw new Error(res.body.error?.message ?? `could not get a key for '${org}' (HTTP ${res.status})`);
|
|
179
|
+
}
|
|
180
|
+
const entry = { apiKey: res.body.apiKey, slug: res.body.slug ?? org, expiresAt: res.body.expiresAt };
|
|
181
|
+
ctx.orgKeys = { ...ctx.orgKeys, [res.body.organizationId]: entry };
|
|
182
|
+
onMint(`Minted a key for ${entry.slug} (expires ${entry.expiresAt?.slice(0, 10) ?? "never"})`);
|
|
183
|
+
return { apiKey: entry.apiKey, organizationId: res.body.organizationId, minted: true };
|
|
184
|
+
}
|
|
185
|
+
async function dropOrgKeys(ctx) {
|
|
186
|
+
await Promise.all(Object.values(ctx.orgKeys ?? {}).map(async (k) => {
|
|
187
|
+
const res = await apiFetch({ ...ctx, apiKey: k.apiKey }, "/api/cli/logout", { method: "POST", body: {} }).catch(() => null);
|
|
188
|
+
if (!res || !res.ok && res.status !== 401) {
|
|
189
|
+
process.stderr.write(`Could not revoke the key for ${k.slug}; \`volty keys revoke cli --org ${k.slug}\` removes it.
|
|
190
|
+
`);
|
|
191
|
+
}
|
|
192
|
+
}));
|
|
193
|
+
ctx.orgKeys = undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
// src/auth.ts
|
|
170
197
|
async function whoami(ctx) {
|
|
171
198
|
if (!ctx.apiKey)
|
|
172
|
-
return
|
|
199
|
+
return { error: "not logged in" };
|
|
173
200
|
const res = await apiFetch(ctx, "/api/cli/whoami");
|
|
174
|
-
if (!res.ok || !res.body.organizationId)
|
|
175
|
-
return
|
|
201
|
+
if (!res.ok || !res.body.organizationId) {
|
|
202
|
+
return { error: res.body?.error?.message ?? "the stored key did not validate, run `volty auth login --rotate`" };
|
|
203
|
+
}
|
|
176
204
|
ctx.organizationId = res.body.organizationId;
|
|
205
|
+
ctx.organizationSlug = res.body.organizationSlug ?? undefined;
|
|
177
206
|
ctx.principal = res.body.email ?? res.body.name ?? res.body.userId;
|
|
178
|
-
return res.body;
|
|
207
|
+
return { who: res.body };
|
|
179
208
|
}
|
|
180
209
|
function openBrowser(url) {
|
|
181
210
|
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", url] : ["xdg-open", url];
|
|
@@ -233,13 +262,13 @@ async function login(config, env, opts) {
|
|
|
233
262
|
ctx.baseUrl = opts.baseUrl;
|
|
234
263
|
if (!ctx.baseUrl)
|
|
235
264
|
throw new Error(`no base URL for '${env}' — pass --base-url <url>`);
|
|
236
|
-
if (!opts.rotate && await whoami(ctx)) {
|
|
265
|
+
if (!opts.rotate && "who" in await whoami(ctx)) {
|
|
237
266
|
return `Already authenticated to '${env}' as ${ctx.principal ?? "(unknown)"} — reusing the stored key.`;
|
|
238
267
|
}
|
|
268
|
+
await dropOrgKeys(ctx);
|
|
239
269
|
if (opts.token) {
|
|
240
270
|
ctx.apiKey = opts.token;
|
|
241
|
-
|
|
242
|
-
if (!who)
|
|
271
|
+
if ("error" in await whoami(ctx))
|
|
243
272
|
throw new Error("the provided --token did not validate");
|
|
244
273
|
return `Authenticated to '${env}' as ${ctx.principal ?? "(unknown)"} (org ${ctx.organizationId}).`;
|
|
245
274
|
}
|
|
@@ -247,21 +276,22 @@ async function login(config, env, opts) {
|
|
|
247
276
|
const key = await devGrant(ctx, opts.devPersona, opts.org);
|
|
248
277
|
if (key) {
|
|
249
278
|
ctx.apiKey = key;
|
|
250
|
-
|
|
251
|
-
return `Authenticated to '${env}' as ${
|
|
279
|
+
await whoami(ctx);
|
|
280
|
+
return `Authenticated to '${env}' as ${ctx.principal ?? ctx.organizationId ?? "(unknown)"} (dev persona ${opts.devPersona}).`;
|
|
252
281
|
}
|
|
253
282
|
process.stdout.write(`The '${env}' server is not in development mode, so --as has no dev grant to use. Falling back to the browser grant.
|
|
254
283
|
`);
|
|
255
284
|
}
|
|
256
285
|
ctx.apiKey = await deviceGrant(ctx);
|
|
257
|
-
|
|
258
|
-
return `Authenticated to '${env}' as ${
|
|
286
|
+
await whoami(ctx);
|
|
287
|
+
return `Authenticated to '${env}' as ${ctx.principal ?? ctx.organizationId ?? "(unknown)"}.`;
|
|
259
288
|
}
|
|
260
289
|
async function logout(config, env) {
|
|
261
290
|
const ctx = config.contexts[env];
|
|
262
291
|
if (!ctx?.apiKey)
|
|
263
292
|
return `No stored key for '${env}'.`;
|
|
264
293
|
await apiFetch(ctx, "/api/cli/logout", { method: "POST", body: {} });
|
|
294
|
+
await dropOrgKeys(ctx);
|
|
265
295
|
ctx.apiKey = undefined;
|
|
266
296
|
ctx.principal = undefined;
|
|
267
297
|
return `Logged out of '${env}' (key revoked).`;
|
|
@@ -386,6 +416,9 @@ function missMessage(catalog, argv) {
|
|
|
386
416
|
const inApp = retiredPrefix(catalog.inApp, argv);
|
|
387
417
|
if (inApp)
|
|
388
418
|
return `\`${inApp.name}\` is not run from here. ${inApp.replacement}`;
|
|
419
|
+
const outOfScope = retiredPrefix(catalog.outOfScope, argv);
|
|
420
|
+
if (outOfScope)
|
|
421
|
+
return outOfScope.replacement;
|
|
389
422
|
const retired = retiredPrefix(catalog.retired, argv);
|
|
390
423
|
if (retired)
|
|
391
424
|
return `\`${retired.name}\` is retired: ${retired.replacement}`;
|
|
@@ -539,6 +572,35 @@ function renderResult(body, json) {
|
|
|
539
572
|
return text || "(no output)";
|
|
540
573
|
}
|
|
541
574
|
|
|
575
|
+
// src/keys.ts
|
|
576
|
+
var VALUE_FLAGS = new Set(["name", "for", "for-user-id", "expires", "expires-in-days", "families"]);
|
|
577
|
+
function keysArgv(argv) {
|
|
578
|
+
const out = argv.slice(0, 2);
|
|
579
|
+
const rest = argv.slice(2);
|
|
580
|
+
for (let i = 0;i < rest.length; i++) {
|
|
581
|
+
const token = rest[i];
|
|
582
|
+
if (!token.startsWith("--")) {
|
|
583
|
+
out.push("--name", token);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const eq = token.indexOf("=");
|
|
587
|
+
const flag = eq > 0 ? token.slice(2, eq) : token.slice(2);
|
|
588
|
+
const value = eq > 0 ? token.slice(eq + 1) : VALUE_FLAGS.has(flag) ? rest[++i] : undefined;
|
|
589
|
+
if (value === undefined)
|
|
590
|
+
out.push(token);
|
|
591
|
+
else if (flag === "for")
|
|
592
|
+
out.push("--for-user-id", value);
|
|
593
|
+
else if (flag === "expires")
|
|
594
|
+
out.push("--expires-in-days", value.replace(/d$/, ""));
|
|
595
|
+
else if (flag === "families")
|
|
596
|
+
for (const f of value.split(","))
|
|
597
|
+
out.push("--families", f.trim());
|
|
598
|
+
else
|
|
599
|
+
out.push(`--${flag}`, value);
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
|
|
542
604
|
// src/org-guard.ts
|
|
543
605
|
var ORG_NATIVE = new Set(["use", "current", "list"]);
|
|
544
606
|
function isOrgFreeCommand(command) {
|
|
@@ -575,12 +637,6 @@ function resolveTargetOrg({ env, baseUrl, override, sticky, command }) {
|
|
|
575
637
|
return { org: undefined };
|
|
576
638
|
return { refusal: PROD_ORG_REFUSAL };
|
|
577
639
|
}
|
|
578
|
-
function describeOrgTarget(orgs, org, env) {
|
|
579
|
-
const match = orgs.find((o) => o.organizationId === org);
|
|
580
|
-
if (!match)
|
|
581
|
-
return { refusal: `org ${org} is not one of your memberships on ${env}` };
|
|
582
|
-
return { line: `→ ${env} · ${match.name} (${match.organizationId}) · ${match.role}` };
|
|
583
|
-
}
|
|
584
640
|
|
|
585
641
|
// src/proposals.ts
|
|
586
642
|
function decideProposal(ctx, proposalId, decision, reason) {
|
|
@@ -712,11 +768,11 @@ async function handleAuth(rest, config, env, orgOverride) {
|
|
|
712
768
|
const ctx = config.contexts[env];
|
|
713
769
|
if (!ctx?.apiKey)
|
|
714
770
|
fail(`not logged in to '${env}' — run \`volty auth login --env ${env}\``);
|
|
715
|
-
const
|
|
716
|
-
if (
|
|
717
|
-
fail(
|
|
771
|
+
const res = await whoami(ctx);
|
|
772
|
+
if ("error" in res)
|
|
773
|
+
fail(res.error);
|
|
718
774
|
saveConfig(config);
|
|
719
|
-
process.stdout.write(`${who.email ?? who.userId} · org ${who.organizationId} · role ${who.role}
|
|
775
|
+
process.stdout.write(`${res.who.email ?? res.who.userId} · org ${res.who.organizationId} · role ${res.who.role}
|
|
720
776
|
`);
|
|
721
777
|
return;
|
|
722
778
|
}
|
|
@@ -800,23 +856,27 @@ async function handleOrg(rest, config, env, target) {
|
|
|
800
856
|
const json = takeBool(rest.slice(1), "--json");
|
|
801
857
|
if (!ctx.apiKey)
|
|
802
858
|
fail(`not logged in to '${env}' — run \`volty auth login --env ${env}\``);
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
859
|
+
const orgs = [
|
|
860
|
+
{ organizationId: ctx.organizationId ?? "", slug: ctx.organizationSlug ?? "", expiresAt: null },
|
|
861
|
+
...Object.entries(ctx.orgKeys ?? {}).map(([organizationId, k]) => ({
|
|
862
|
+
organizationId,
|
|
863
|
+
slug: k.slug,
|
|
864
|
+
expiresAt: k.expiresAt ?? null
|
|
865
|
+
}))
|
|
866
|
+
];
|
|
806
867
|
if (json.present) {
|
|
807
|
-
process.stdout.write(`${JSON.stringify(
|
|
868
|
+
process.stdout.write(`${JSON.stringify({ orgs }, null, 2)}
|
|
808
869
|
`);
|
|
809
870
|
return;
|
|
810
871
|
}
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
for (const o of res.body.orgs) {
|
|
816
|
-
const marker = o.organizationId === active ? "*" : " ";
|
|
817
|
-
process.stdout.write(`${marker} ${o.organizationId} ${o.role} ${o.name}
|
|
872
|
+
for (const [i, o] of orgs.entries()) {
|
|
873
|
+
const active = target.org ? target.org === o.organizationId || target.org === o.slug : i === 0;
|
|
874
|
+
const note = i === 0 ? "signed in" : `expires ${o.expiresAt?.slice(0, 10) ?? "never"}`;
|
|
875
|
+
process.stdout.write(`${active ? "*" : " "} ${o.organizationId} ${o.slug} ${note}
|
|
818
876
|
`);
|
|
819
877
|
}
|
|
878
|
+
process.stdout.write(`Another workspace you belong to: pass --org <slug> and the CLI gets a key for it.
|
|
879
|
+
`);
|
|
820
880
|
return;
|
|
821
881
|
}
|
|
822
882
|
fail("usage: volty org <use|current|list>");
|
|
@@ -889,7 +949,6 @@ async function main() {
|
|
|
889
949
|
});
|
|
890
950
|
if ("refusal" in target)
|
|
891
951
|
fail(target.refusal);
|
|
892
|
-
setOrgOverride(target.org);
|
|
893
952
|
const [cmd, ...rest] = argv;
|
|
894
953
|
if (cmd === "auth")
|
|
895
954
|
return handleAuth(rest, config, env, orgExtract.value ?? (process.env.VOLTY_ORG || undefined));
|
|
@@ -901,18 +960,34 @@ async function main() {
|
|
|
901
960
|
return handleDocs(config, env);
|
|
902
961
|
if (cmd === "skill")
|
|
903
962
|
return handleSkill(rest);
|
|
904
|
-
const
|
|
905
|
-
if (!
|
|
963
|
+
const login = config.contexts[env];
|
|
964
|
+
if (!login?.apiKey)
|
|
906
965
|
fail(`not logged in to '${env}' — run \`volty auth login --env ${env}\``);
|
|
966
|
+
let ctx = login;
|
|
967
|
+
if (target.org) {
|
|
968
|
+
let selected;
|
|
969
|
+
try {
|
|
970
|
+
selected = await ensureOrgKey(login, target.org, (line) => process.stderr.write(`${line}
|
|
971
|
+
`));
|
|
972
|
+
} catch (err) {
|
|
973
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
974
|
+
}
|
|
975
|
+
if (selected.minted)
|
|
976
|
+
saveConfig(config);
|
|
977
|
+
ctx = { ...login, apiKey: selected.apiKey, organizationId: selected.organizationId };
|
|
978
|
+
setOrgOverride(selected.organizationId);
|
|
979
|
+
}
|
|
907
980
|
if (prod && !isOrgFreeCommand(argv) && target.org) {
|
|
908
|
-
const
|
|
909
|
-
if ("
|
|
910
|
-
fail(
|
|
911
|
-
process.stderr.write(
|
|
981
|
+
const res = await whoami(ctx);
|
|
982
|
+
if ("error" in res)
|
|
983
|
+
fail(res.error);
|
|
984
|
+
process.stderr.write(`→ ${env} · ${res.who.organizationName} (${res.who.organizationId}) · ${res.who.role}
|
|
912
985
|
`);
|
|
913
986
|
}
|
|
914
987
|
if (cmd === "changes" && rest[0] === "decide")
|
|
915
988
|
return handleChanges(rest, ctx);
|
|
989
|
+
if (cmd === "keys")
|
|
990
|
+
argv = keysArgv(argv);
|
|
916
991
|
const catalog = await fetchCatalog(ctx, env, !cacheFlag.present);
|
|
917
992
|
if (!cmd || cmd === "help" || cmd === "--help") {
|
|
918
993
|
process.stdout.write(`${renderCatalog(catalog.ops, rest.length ? rest.join(" ") : undefined)}
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ Use the lane the behaviour was reported on. On the contact lane Volty declines t
|
|
|
25
25
|
|
|
26
26
|
## On prod
|
|
27
27
|
|
|
28
|
+
- A key reaches one org and calls operations only; `--org` gets each org you belong to its own key the first time. A call a key cannot make is refused with a sentence naming why; report that sentence instead of working around it. `volty keys create <name>` mints a narrower named key for a script (`volty docs` has the flags).
|
|
28
29
|
- Name the org on every command (`--env prod --org <id>`, or `VOLTY_ENV` and `VOLTY_ORG` in the process), and read the stderr line the CLI prints before each command to check it names the org you meant. Act only in the org the user named.
|
|
29
30
|
- Test through the Sandbox, where a reply to the synthetic contact is recorded as sent and never leaves. A real conversation is never a test target.
|
|
30
31
|
- Run `volty debug sandbox-cleanup --all --env prod --org <id>` when you finish.
|