@mnemom/mnemom 0.13.0 → 0.14.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/dist/commands/agents.d.ts +37 -1
- package/dist/commands/agents.js +234 -9
- package/dist/commands/card.js +5 -3
- package/dist/commands/protection.js +5 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +40 -6
- package/dist/lib/api.d.ts +88 -2
- package/dist/lib/api.js +145 -7
- package/package.json +3 -1
|
@@ -1 +1,37 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface AgentsListOptions {
|
|
2
|
+
/** Scope to a single org. When omitted, aggregates across every org you belong to. */
|
|
3
|
+
org?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function agentsListCommand(options?: AgentsListOptions): Promise<void>;
|
|
6
|
+
export interface AgentsClaimOptions {
|
|
7
|
+
/** Org slug OR org_id to claim into. Omit → land in your personal org (the API default). */
|
|
8
|
+
org?: string;
|
|
9
|
+
/** The agent's provider API key — the CLI derives the hash proof from it. */
|
|
10
|
+
key?: string;
|
|
11
|
+
/** A pre-computed 64-hex hash proof (advanced/CI; alternative to --key). */
|
|
12
|
+
hashProof?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The name the agent was provisioned with, used in the proof derivation
|
|
15
|
+
* `sha256(`${key}|${name}`)`. Defaults to a name-shaped positional arg, else
|
|
16
|
+
* the agent is treated as an unnamed singleton (`sha256(key)`).
|
|
17
|
+
*/
|
|
18
|
+
name?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Derive the full 64-hex hash proof the claim endpoint expects:
|
|
23
|
+
* `sha256(`${apiKey}|${agentName}`)` for a named agent, or `sha256(apiKey)`
|
|
24
|
+
* for an unnamed singleton. This mirrors the gateway's provisioning
|
|
25
|
+
* derivation (gateway/src/index.ts) and agent-proof.ts on the API side.
|
|
26
|
+
*/
|
|
27
|
+
export declare function deriveHashProof(apiKey: string, agentName?: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* `mnemom agents claim <id-or-name> [--org <slug>] (--key <k> | --hash-proof <h>)`
|
|
30
|
+
*
|
|
31
|
+
* Claims an agent into an org (ADR-062). Possession of the agent's key (via
|
|
32
|
+
* `--key`, from which the CLI derives the proof) or a pre-computed
|
|
33
|
+
* `--hash-proof` authenticates the claim; `--org` selects the landing org
|
|
34
|
+
* (omit → your personal org). On a not-a-member 403 the command teaches by
|
|
35
|
+
* listing your claimable orgs.
|
|
36
|
+
*/
|
|
37
|
+
export declare function agentsClaimCommand(idOrName: string, options?: AgentsClaimOptions): Promise<void>;
|
package/dist/commands/agents.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { listOrgAgents, listMyOrgs, getAgentByName, claimAgent, MnemomApiError, } from "../lib/api.js";
|
|
2
3
|
import { requireAuth } from "../lib/auth.js";
|
|
3
4
|
import { fmt } from "../lib/format.js";
|
|
4
|
-
export async function agentsListCommand() {
|
|
5
|
+
export async function agentsListCommand(options = {}) {
|
|
5
6
|
await requireAuth();
|
|
6
7
|
console.log(fmt.header("Agents"));
|
|
7
8
|
console.log();
|
|
8
9
|
let agents;
|
|
9
10
|
try {
|
|
10
|
-
agents = await
|
|
11
|
+
agents = await listOrgAgents(options.org);
|
|
11
12
|
}
|
|
12
13
|
catch (err) {
|
|
13
14
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -15,24 +16,248 @@ export async function agentsListCommand() {
|
|
|
15
16
|
process.exit(1);
|
|
16
17
|
}
|
|
17
18
|
if (agents.length === 0) {
|
|
18
|
-
console.log(" No agents found.\n");
|
|
19
|
+
console.log(options.org ? ` No agents found in org '${options.org}'.\n` : " No agents found.\n");
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
|
-
//
|
|
22
|
+
// Show the Org column only when aggregating across orgs; with --org it's redundant.
|
|
23
|
+
const showOrg = !options.org;
|
|
22
24
|
const nameW = 24;
|
|
23
25
|
const idW = 40;
|
|
24
26
|
const seenW = 14;
|
|
25
27
|
const statusW = 14;
|
|
26
|
-
const
|
|
28
|
+
const orgW = 22;
|
|
29
|
+
let header = "Name".padEnd(nameW) +
|
|
30
|
+
"ID".padEnd(idW) +
|
|
31
|
+
"Last Seen".padEnd(seenW) +
|
|
32
|
+
"Containment".padEnd(statusW);
|
|
33
|
+
if (showOrg)
|
|
34
|
+
header += "Org";
|
|
27
35
|
console.log(` ${header}`);
|
|
28
|
-
console.log(` ${"─".repeat(nameW + idW + seenW + statusW)}`);
|
|
36
|
+
console.log(` ${"─".repeat(nameW + idW + seenW + statusW + (showOrg ? orgW : 0))}`);
|
|
29
37
|
for (const agent of agents) {
|
|
30
38
|
const name = (agent.name ?? "-").slice(0, nameW - 2).padEnd(nameW);
|
|
31
39
|
const id = agent.id.padEnd(idW);
|
|
32
40
|
const lastSeen = agent.last_seen ? new Date(agent.last_seen).toLocaleDateString() : "-";
|
|
33
41
|
const seen = lastSeen.padEnd(seenW);
|
|
34
|
-
const containment = agent.containment_status ?? "-";
|
|
35
|
-
|
|
42
|
+
const containment = (agent.containment_status ?? "-").padEnd(statusW);
|
|
43
|
+
const org = showOrg ? agent.org_name.slice(0, orgW - 2) : "";
|
|
44
|
+
console.log(` ${name}${id}${seen}${containment}${org}`);
|
|
36
45
|
}
|
|
37
46
|
console.log(`\n Total: ${agents.length} agent(s)\n`);
|
|
38
47
|
}
|
|
48
|
+
// ─── agents claim (ADR-062 claim-to-org) ──────────────────────────────────
|
|
49
|
+
/** Matches the canonical agent-id shapes (mirrors resolveAgentId in lib/api). */
|
|
50
|
+
const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
|
|
51
|
+
/** A presented hash proof is the full 64-hex SHA-256 digest. */
|
|
52
|
+
const FULL_PROOF_RE = /^[0-9a-f]{64}$/;
|
|
53
|
+
// SHA-256 → lowercase hex. NOTE the apiKey-derived input below: this is NOT
|
|
54
|
+
// password-at-rest hashing — it's the platform's deterministic proof-of-
|
|
55
|
+
// possession (`hash_proof`). It MUST equal the server's stored value, which the
|
|
56
|
+
// gateway + mnemom-api/src/auth/agent-proof.ts compute as plain SHA-256 of
|
|
57
|
+
// `${apiKey}|${name}`. A slow KDF (bcrypt/scrypt) would produce a different
|
|
58
|
+
// digest and break the claim handshake entirely, so SHA-256 is mandated by the
|
|
59
|
+
// protocol — the "insufficient computational effort" alert is a false positive.
|
|
60
|
+
function sha256Hex(input) {
|
|
61
|
+
// lgtm[js/insufficient-password-hash] — protocol proof, not password storage (see note above)
|
|
62
|
+
return createHash("sha256").update(input, "utf8").digest("hex"); // codeql[js/insufficient-password-hash]
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Derive the full 64-hex hash proof the claim endpoint expects:
|
|
66
|
+
* `sha256(`${apiKey}|${agentName}`)` for a named agent, or `sha256(apiKey)`
|
|
67
|
+
* for an unnamed singleton. This mirrors the gateway's provisioning
|
|
68
|
+
* derivation (gateway/src/index.ts) and agent-proof.ts on the API side.
|
|
69
|
+
*/
|
|
70
|
+
export function deriveHashProof(apiKey, agentName) {
|
|
71
|
+
const key = apiKey.trim();
|
|
72
|
+
const name = agentName?.trim();
|
|
73
|
+
return name ? sha256Hex(`${key}|${name}`) : sha256Hex(key);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve an `--org <slug>` value to an org_id. Accepts either a slug or a raw
|
|
77
|
+
* org_id (both are matched against the caller's memberships). Returns the
|
|
78
|
+
* resolved org_id, or throws a teaching error listing the caller's orgs when
|
|
79
|
+
* the value matches none of them.
|
|
80
|
+
*/
|
|
81
|
+
function resolveOrgId(slugOrId, orgs) {
|
|
82
|
+
const needle = slugOrId.trim();
|
|
83
|
+
const match = orgs.find((o) => o.slug === needle) ?? orgs.find((o) => o.org_id === needle);
|
|
84
|
+
if (match)
|
|
85
|
+
return match.org_id;
|
|
86
|
+
const choices = orgs.map((o) => ` ${o.slug.padEnd(24)} ${o.name}`).join("\n");
|
|
87
|
+
throw new Error(`No org of yours matches '${needle}'. You can claim into one of:\n${choices}\n` +
|
|
88
|
+
` Pass --org <slug>, or omit --org to land in your personal org.`);
|
|
89
|
+
}
|
|
90
|
+
/** The not-a-member 403 code emitted by the merged endpoint. */
|
|
91
|
+
const NOT_A_MEMBER_CODE = "agent_org_not_member";
|
|
92
|
+
/**
|
|
93
|
+
* Distinguish a not-a-member 403 (the requested org isn't one of yours) from
|
|
94
|
+
* the other 403s this endpoint emits — `invalid_hash_proof` and
|
|
95
|
+
* `agent_cross_tenant` (already claimed by a different owner) — which should
|
|
96
|
+
* fall through to the generic error. Source-verified against #713: the
|
|
97
|
+
* not-a-member 403 carries `code: "agent_org_not_member"` and
|
|
98
|
+
* `details.claimable_orgs`. A loose match guards against minor drift.
|
|
99
|
+
*/
|
|
100
|
+
function isNotAMemberError(err) {
|
|
101
|
+
if (err.code === NOT_A_MEMBER_CODE)
|
|
102
|
+
return true;
|
|
103
|
+
if (extractClaimableOrgs(err.details).length > 0)
|
|
104
|
+
return true;
|
|
105
|
+
return /org_not_member|not_a_member|not_org_member/.test(err.code ?? "");
|
|
106
|
+
}
|
|
107
|
+
/** Pull the server-provided claimable-org list off a 403 body, if present. */
|
|
108
|
+
function extractClaimableOrgs(details) {
|
|
109
|
+
const list = details?.claimable_orgs;
|
|
110
|
+
if (!Array.isArray(list))
|
|
111
|
+
return [];
|
|
112
|
+
return list.filter((o) => !!o && typeof o === "object" && typeof o.org_id === "string");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `mnemom agents claim <id-or-name> [--org <slug>] (--key <k> | --hash-proof <h>)`
|
|
116
|
+
*
|
|
117
|
+
* Claims an agent into an org (ADR-062). Possession of the agent's key (via
|
|
118
|
+
* `--key`, from which the CLI derives the proof) or a pre-computed
|
|
119
|
+
* `--hash-proof` authenticates the claim; `--org` selects the landing org
|
|
120
|
+
* (omit → your personal org). On a not-a-member 403 the command teaches by
|
|
121
|
+
* listing your claimable orgs.
|
|
122
|
+
*/
|
|
123
|
+
export async function agentsClaimCommand(idOrName, options = {}) {
|
|
124
|
+
await requireAuth();
|
|
125
|
+
// 1. Resolve the agent id to claim. An UNCLAIMED agent is not in your org
|
|
126
|
+
// fleet yet, so a name can only be resolved for agents you can already
|
|
127
|
+
// see; the reliable path for a fresh claim is to pass the agent id.
|
|
128
|
+
const looksLikeId = AGENT_ID_RE.test(idOrName);
|
|
129
|
+
let agentId = idOrName;
|
|
130
|
+
let derivationName = options.name;
|
|
131
|
+
if (!looksLikeId) {
|
|
132
|
+
// A name-shaped arg: try the fleet, else guide the user to the id.
|
|
133
|
+
const found = await getAgentByName(idOrName).catch(() => null);
|
|
134
|
+
if (found) {
|
|
135
|
+
agentId = found.id;
|
|
136
|
+
derivationName = derivationName ?? found.name ?? idOrName;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// The arg names an agent we can't see yet (unclaimed agents aren't in
|
|
140
|
+
// your fleet), so we have no id to address the claim — guide to the id.
|
|
141
|
+
console.log(fmt.error(`Could not resolve '${idOrName}' to an agent id you can see yet — unclaimed agents are not in your fleet.`));
|
|
142
|
+
console.log(fmt.dim(` Pass the agent id directly: mnemom agents claim mnm-... --key <key> [--name ${idOrName}]`) + "\n");
|
|
143
|
+
process.exit(1);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 2. Establish the hash proof: --hash-proof (raw) takes precedence, else
|
|
148
|
+
// derive from --key. Exactly one source is needed.
|
|
149
|
+
let hashProof;
|
|
150
|
+
if (options.hashProof) {
|
|
151
|
+
const proof = options.hashProof.trim().toLowerCase();
|
|
152
|
+
if (!FULL_PROOF_RE.test(proof)) {
|
|
153
|
+
console.log(fmt.error("--hash-proof should be the full 64-character hex SHA-256 proof.") + "\n");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
hashProof = proof;
|
|
158
|
+
}
|
|
159
|
+
else if (options.key) {
|
|
160
|
+
hashProof = deriveHashProof(options.key, derivationName);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(fmt.error("Claiming needs proof you hold the agent's key.") + "\n");
|
|
164
|
+
console.log(fmt.dim(" Pass --key <agent-api-key> (the CLI derives the proof), or --hash-proof <64-hex> directly.") + "\n");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// 3. Resolve --org slug → org_id (omit → server default / personal org).
|
|
169
|
+
let orgId;
|
|
170
|
+
let orgs = [];
|
|
171
|
+
if (options.org) {
|
|
172
|
+
try {
|
|
173
|
+
orgs = await listMyOrgs();
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
console.log(fmt.error(`Failed to read your orgs: ${msg}`) + "\n");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
orgId = resolveOrgId(options.org, orgs);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
186
|
+
console.log(fmt.error(msg) + "\n");
|
|
187
|
+
process.exit(1);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 4. Claim.
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = await claimAgent(agentId, { hashProof, orgId });
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err instanceof MnemomApiError && err.effectiveStatus === 403 && isNotAMemberError(err)) {
|
|
198
|
+
// Teaching error: list the orgs the caller can actually claim into.
|
|
199
|
+
// Prefer `mnemom org list` data (listMyOrgs) — it carries slugs, so we
|
|
200
|
+
// can print the actionable `--org <slug>`. The server's
|
|
201
|
+
// `details.claimable_orgs` (org_id + name, NO slug) is the fallback when
|
|
202
|
+
// the membership list is unavailable.
|
|
203
|
+
const memberOrgs = orgs.length ? orgs : await listMyOrgs().catch(() => []);
|
|
204
|
+
console.log(fmt.error(`You are not a member of '${options.org}', so the agent can't land there.`));
|
|
205
|
+
if (memberOrgs.length) {
|
|
206
|
+
console.log("\n You can claim into one of (use the slug):");
|
|
207
|
+
for (const o of memberOrgs) {
|
|
208
|
+
const tag = o.is_personal ? " (personal)" : "";
|
|
209
|
+
console.log(` ${o.slug.padEnd(24)} ${o.name}${tag}`);
|
|
210
|
+
}
|
|
211
|
+
console.log(`\n Re-run with --org <slug>, or omit --org to land in your personal org.\n`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const fromServer = extractClaimableOrgs(err.details);
|
|
215
|
+
if (fromServer.length) {
|
|
216
|
+
console.log("\n You can claim into one of (use the org id):");
|
|
217
|
+
for (const o of fromServer) {
|
|
218
|
+
const tag = o.is_personal ? " (personal)" : "";
|
|
219
|
+
console.log(` ${o.org_id.padEnd(40)} ${o.name}${tag}`);
|
|
220
|
+
}
|
|
221
|
+
console.log(`\n Re-run with --org <id>, or omit --org to land in your personal org.\n`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(fmt.dim(" Run `mnemom org list` to see the orgs you belong to.") + "\n");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
process.exit(1);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (err instanceof MnemomApiError && err.effectiveStatus === 503) {
|
|
231
|
+
// The platform is still provisioning the caller's personal org. Transient.
|
|
232
|
+
console.log(fmt.warn("Your personal org is still being set up. Give it a moment and run claim again."));
|
|
233
|
+
console.log("");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
console.log(fmt.error(`Failed to claim agent: ${msg}`) + "\n");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// 5. Report where it landed.
|
|
243
|
+
if (options.json) {
|
|
244
|
+
console.log(JSON.stringify(result, null, 2));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Map the resolved org_id to a friendly name when we can.
|
|
248
|
+
const landed = result.org_id;
|
|
249
|
+
let where;
|
|
250
|
+
if (!landed) {
|
|
251
|
+
where = "your personal org (default scope)";
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const named = (orgs.length ? orgs : await listMyOrgs().catch(() => [])).find((o) => o.org_id === landed);
|
|
255
|
+
where = named ? `${named.name} (${named.slug})` : landed;
|
|
256
|
+
}
|
|
257
|
+
console.log(fmt.success(`Claimed ${result.agent_id}.`));
|
|
258
|
+
console.log(fmt.label(" Landed in:", where));
|
|
259
|
+
if (result.claimed_at) {
|
|
260
|
+
console.log(fmt.label(" Claimed at:", new Date(result.claimed_at).toLocaleString()));
|
|
261
|
+
}
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
package/dist/commands/card.js
CHANGED
|
@@ -807,9 +807,11 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
807
807
|
},
|
|
808
808
|
audit: { retention_days: 30, queryable: false, trace_format: "otel" },
|
|
809
809
|
}, { lineWidth: 120, noRefs: true });
|
|
810
|
-
// Write to temp
|
|
811
|
-
|
|
812
|
-
|
|
810
|
+
// Write to a per-invocation temp dir created with mkdtemp (mode 0700,
|
|
811
|
+
// unpredictable suffix) so the editor file can't be pre-created or
|
|
812
|
+
// symlink-raced by another user in the shared os.tmpdir().
|
|
813
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnemom-card-"));
|
|
814
|
+
const tmpFile = path.join(tmpDir, `${agentId}.yaml`);
|
|
813
815
|
fs.writeFileSync(tmpFile, cardYaml);
|
|
814
816
|
// Open in editor
|
|
815
817
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
@@ -578,8 +578,11 @@ export async function protectionEditCommand(agentName, options = {}) {
|
|
|
578
578
|
},
|
|
579
579
|
trusted_sources: { domains: [], agent_ids: [], ip_ranges: [] },
|
|
580
580
|
}, { lineWidth: 120, noRefs: true });
|
|
581
|
-
|
|
582
|
-
|
|
581
|
+
// Per-invocation temp dir via mkdtemp (mode 0700, unpredictable suffix) so
|
|
582
|
+
// the editor file can't be pre-created or symlink-raced by another user in
|
|
583
|
+
// the shared os.tmpdir().
|
|
584
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnemom-protection-"));
|
|
585
|
+
const tmpFile = path.join(tmpDir, `${agentId}.yaml`);
|
|
583
586
|
fs.writeFileSync(tmpFile, cardYaml);
|
|
584
587
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
585
588
|
console.log(`Opening ${editor}...`);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
2
3
|
import { program } from "commander";
|
|
3
4
|
import { CLI_VERSION } from "./version.js";
|
|
4
5
|
import { statusCommand } from "./commands/status.js";
|
|
@@ -8,7 +9,7 @@ import { licenseActivateCommand, licenseStatusCommand, licenseDeactivateCommand,
|
|
|
8
9
|
import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditCommand, cardEvaluateCommand, } from "./commands/card.js";
|
|
9
10
|
import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyListCommand, policyTestCommand, policyEvaluateCommand, } from "./commands/policy.js";
|
|
10
11
|
import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, } from "./commands/protection.js";
|
|
11
|
-
import { agentsListCommand } from "./commands/agents.js";
|
|
12
|
+
import { agentsListCommand, agentsClaimCommand } from "./commands/agents.js";
|
|
12
13
|
import { orgListCommand, orgShowCommand } from "./commands/org.js";
|
|
13
14
|
import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
|
|
14
15
|
import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
|
|
@@ -295,12 +296,36 @@ policyCmd
|
|
|
295
296
|
// ============================================================================
|
|
296
297
|
// Agent management
|
|
297
298
|
// ============================================================================
|
|
298
|
-
program
|
|
299
|
+
const agentsCmd = program
|
|
299
300
|
.command("agents")
|
|
300
|
-
.description("List agents
|
|
301
|
-
.
|
|
301
|
+
.description("List agents across the orgs you belong to (ADR-062 org-scoped)")
|
|
302
|
+
.option("--org <id>", "Scope to a single org (default: all orgs you belong to)")
|
|
303
|
+
.action(async (opts) => {
|
|
302
304
|
try {
|
|
303
|
-
await agentsListCommand();
|
|
305
|
+
await agentsListCommand({ org: opts.org });
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
agentsCmd
|
|
313
|
+
.command("claim <id-or-name>")
|
|
314
|
+
.description("Claim an agent into an org (ADR-062 claim-to-org)")
|
|
315
|
+
.option("--org <slug>", "Org slug or id to claim into (default: your personal org)")
|
|
316
|
+
.option("--key <key>", "The agent's provider API key — the CLI derives the hash proof from it")
|
|
317
|
+
.option("--hash-proof <hex>", "A pre-computed 64-hex SHA-256 proof (advanced/CI; alternative to --key)")
|
|
318
|
+
.option("--name <name>", "Provisioned agent name used in proof derivation (defaults to a name-shaped positional arg)")
|
|
319
|
+
.option("--json", "Output JSON instead of human-readable text")
|
|
320
|
+
.action(async (idOrName, opts) => {
|
|
321
|
+
try {
|
|
322
|
+
await agentsClaimCommand(idOrName, {
|
|
323
|
+
org: opts.org,
|
|
324
|
+
key: opts.key,
|
|
325
|
+
hashProof: opts.hashProof,
|
|
326
|
+
name: opts.name,
|
|
327
|
+
json: opts.json,
|
|
328
|
+
});
|
|
304
329
|
}
|
|
305
330
|
catch (error) {
|
|
306
331
|
console.error("Error:", error instanceof Error ? error.message : error);
|
|
@@ -1296,4 +1321,13 @@ program
|
|
|
1296
1321
|
process.exit(1);
|
|
1297
1322
|
}
|
|
1298
1323
|
});
|
|
1299
|
-
program.
|
|
1324
|
+
// Export the fully-assembled commander program so tooling (e.g. the
|
|
1325
|
+
// command-tree snapshot generator in scripts/gen-command-tree.mjs) can
|
|
1326
|
+
// statically introspect the command surface WITHOUT executing the CLI.
|
|
1327
|
+
export { program };
|
|
1328
|
+
// Parse argv only when invoked as the CLI entrypoint — not when imported.
|
|
1329
|
+
// (ESM has no `require.main`; compare this module's URL to argv[1].)
|
|
1330
|
+
const invokedDirectly = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
1331
|
+
if (invokedDirectly) {
|
|
1332
|
+
program.parse();
|
|
1333
|
+
}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -207,6 +207,13 @@ export interface AgentListItem {
|
|
|
207
207
|
containment_status: string | null;
|
|
208
208
|
key_prefix?: string | null;
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* @deprecated Legacy per-user listing — `GET /v1/agents` is scoped by the
|
|
212
|
+
* caller's `claimed_by` rows, which ADR-062 retired as an authorization /
|
|
213
|
+
* listing boundary (org_id is now the sole boundary). New code must list via
|
|
214
|
+
* {@link listOrgAgents} (org-scoped). Retained only for back-compat callers;
|
|
215
|
+
* the `mnemom agents` command and name resolution no longer use it.
|
|
216
|
+
*/
|
|
210
217
|
export declare function listAgents(): Promise<AgentListItem[]>;
|
|
211
218
|
export interface OrgListItem {
|
|
212
219
|
org_id: string;
|
|
@@ -231,6 +238,85 @@ export interface PersonalOrgRef {
|
|
|
231
238
|
* first; multi-user orgs follow.
|
|
232
239
|
*/
|
|
233
240
|
export declare function listMyOrgs(): Promise<OrgListItem[]>;
|
|
241
|
+
/**
|
|
242
|
+
* One agent row from the org-scoped fleet (`get_org_agent_fleet`). Mirrors the
|
|
243
|
+
* website's `OrgFleetAgent` shape so the CLI and dashboard read the same
|
|
244
|
+
* surface. This is the ADR-062-canonical listing: every agent has exactly one
|
|
245
|
+
* governing org, and the fleet is keyed on `org_id` (not `claimed_by`).
|
|
246
|
+
*/
|
|
247
|
+
export interface OrgFleetAgent {
|
|
248
|
+
agent_id: string;
|
|
249
|
+
agent_name: string;
|
|
250
|
+
owner_email: string | null;
|
|
251
|
+
last_seen: string | null;
|
|
252
|
+
created_at: string;
|
|
253
|
+
integrity_score: number;
|
|
254
|
+
coverage_ratio: number;
|
|
255
|
+
latest_verdict: string | null;
|
|
256
|
+
active_drift_alerts: number;
|
|
257
|
+
worst_drift_severity: string | null;
|
|
258
|
+
check_count: number;
|
|
259
|
+
containment_status?: string | null;
|
|
260
|
+
avatar_url?: string | null;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* GET /v1/orgs/:org_id/agents — the org-scoped agent fleet (ADR-062). Any
|
|
264
|
+
* member of the org may read it; non-members get 403.
|
|
265
|
+
*/
|
|
266
|
+
export declare function fetchOrgFleet(orgId: string): Promise<OrgFleetAgent[]>;
|
|
267
|
+
/** An {@link AgentListItem} tagged with the org it belongs to. */
|
|
268
|
+
export interface OrgAgentRow extends AgentListItem {
|
|
269
|
+
org_id: string;
|
|
270
|
+
org_name: string;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* List agents the ADR-062 way: scoped by org membership, never by `claimed_by`.
|
|
274
|
+
*
|
|
275
|
+
* - With `orgId`: lists that single org's fleet.
|
|
276
|
+
* - Without `orgId`: aggregates the fleet across every org the caller belongs
|
|
277
|
+
* to (the same pattern as {@link listAllTeams}), tagging each row with its
|
|
278
|
+
* org. Orgs the caller can't read (403/404) are skipped silently so one
|
|
279
|
+
* inaccessible org doesn't fail the whole listing.
|
|
280
|
+
*/
|
|
281
|
+
export declare function listOrgAgents(orgId?: string): Promise<OrgAgentRow[]>;
|
|
282
|
+
/**
|
|
283
|
+
* Result of a successful agent claim (ADR-062). Mirrors the locked
|
|
284
|
+
* `POST /v1/agents/:id/claim` 200 body verbatim — only the fields the
|
|
285
|
+
* endpoint actually returns. `org_id` is the org the agent ACTUALLY landed
|
|
286
|
+
* in (resolved server-side: the requested org, or the caller's personal org
|
|
287
|
+
* when none was supplied), so the CLI can confirm placement without a
|
|
288
|
+
* follow-up read. Optional/defaulted on the type so a mid-deploy CLI pointed
|
|
289
|
+
* at an older API that omits a field degrades to a sane value rather than
|
|
290
|
+
* crashing.
|
|
291
|
+
*/
|
|
292
|
+
export interface ClaimAgentResult {
|
|
293
|
+
claimed: boolean;
|
|
294
|
+
agent_id: string;
|
|
295
|
+
/** Resolved landing org (may differ from the request); null if unscoped. */
|
|
296
|
+
org_id: string | null;
|
|
297
|
+
/** ISO-8601 claim timestamp; null if the server omits it. */
|
|
298
|
+
claimed_at: string | null;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* POST /v1/agents/:id/claim — claim an agent into an org (ADR-062).
|
|
302
|
+
*
|
|
303
|
+
* Body is `{ hash_proof, org_id? }`. `hash_proof` (the agent's full SHA-256
|
|
304
|
+
* proof) authenticates the caller as the agent's owner; the user's own auth
|
|
305
|
+
* header rides along too so the platform can validate org membership and link
|
|
306
|
+
* the principal. Omit `orgId` to land in the caller's personal org (the
|
|
307
|
+
* platform default); a supplied `orgId` is validated against the caller's
|
|
308
|
+
* memberships server-side (a non-member gets a 403 the command layer turns
|
|
309
|
+
* into a teaching error listing claimable orgs).
|
|
310
|
+
*
|
|
311
|
+
* An Idempotency-Key is minted (and held across the 401-refresh retry) so a
|
|
312
|
+
* retried claim replays the same logical operation server-side.
|
|
313
|
+
*/
|
|
314
|
+
export declare function claimAgent(agentId: string, body: {
|
|
315
|
+
hashProof: string;
|
|
316
|
+
orgId?: string;
|
|
317
|
+
}, opts?: {
|
|
318
|
+
idempotencyKey?: string;
|
|
319
|
+
}): Promise<ClaimAgentResult>;
|
|
234
320
|
/**
|
|
235
321
|
* GET /v1/auth/me/personal-org — accessor for the user's personal org.
|
|
236
322
|
* Idempotent: lazily provisions for legacy accounts that pre-date the
|
|
@@ -369,11 +455,11 @@ export declare function revokeTeamAdmin(teamId: string, userId: string): Promise
|
|
|
369
455
|
*/
|
|
370
456
|
export declare function listTeamAdmins(teamId: string): Promise<TeamAdminListResponse>;
|
|
371
457
|
/**
|
|
372
|
-
* Look up an agent
|
|
458
|
+
* Look up an agent by name across the caller's org fleets (ADR-062-scoped via
|
|
459
|
+
* {@link listOrgAgents}, not the legacy `claimed_by` list).
|
|
373
460
|
* Tries exact match first, then single partial match.
|
|
374
461
|
* Throws if multiple agents partially match (ambiguous).
|
|
375
462
|
* Returns null if no match found.
|
|
376
|
-
* Note: capped at 100 agents by listAgents().
|
|
377
463
|
*/
|
|
378
464
|
export declare function getAgentByName(name: string): Promise<AgentListItem | null>;
|
|
379
465
|
/**
|
package/dist/lib/api.js
CHANGED
|
@@ -205,6 +205,13 @@ async function fetchWithAuthRetry(url, buildInit) {
|
|
|
205
205
|
export async function getAgent(id) {
|
|
206
206
|
return fetchApi(`/v1/agents/${id}`);
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* @deprecated Legacy per-user listing — `GET /v1/agents` is scoped by the
|
|
210
|
+
* caller's `claimed_by` rows, which ADR-062 retired as an authorization /
|
|
211
|
+
* listing boundary (org_id is now the sole boundary). New code must list via
|
|
212
|
+
* {@link listOrgAgents} (org-scoped). Retained only for back-compat callers;
|
|
213
|
+
* the `mnemom agents` command and name resolution no longer use it.
|
|
214
|
+
*/
|
|
208
215
|
export async function listAgents() {
|
|
209
216
|
const url = validateUrl(`${API_BASE}/v1/agents?limit=100`);
|
|
210
217
|
const response = await fetchWithAuthRetry(url, async () => ({
|
|
@@ -238,6 +245,127 @@ export async function listMyOrgs() {
|
|
|
238
245
|
const data = (await response.json());
|
|
239
246
|
return data.orgs ?? [];
|
|
240
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* GET /v1/orgs/:org_id/agents — the org-scoped agent fleet (ADR-062). Any
|
|
250
|
+
* member of the org may read it; non-members get 403.
|
|
251
|
+
*/
|
|
252
|
+
export async function fetchOrgFleet(orgId) {
|
|
253
|
+
const url = validateUrl(`${API_BASE}/v1/orgs/${encodeURIComponent(orgId)}/agents`);
|
|
254
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
255
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
256
|
+
}));
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
if (response.status === 401) {
|
|
259
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
260
|
+
}
|
|
261
|
+
throw await readApiError(response, "Failed to list org agents");
|
|
262
|
+
}
|
|
263
|
+
const data = (await response.json());
|
|
264
|
+
return data.agents ?? [];
|
|
265
|
+
}
|
|
266
|
+
function fleetToRow(a, org) {
|
|
267
|
+
return {
|
|
268
|
+
id: a.agent_id,
|
|
269
|
+
name: a.agent_name ?? null,
|
|
270
|
+
email: a.owner_email ?? null,
|
|
271
|
+
created_at: a.created_at,
|
|
272
|
+
last_seen: a.last_seen ?? null,
|
|
273
|
+
containment_status: a.containment_status ?? null,
|
|
274
|
+
org_id: org.org_id,
|
|
275
|
+
org_name: org.name,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* List agents the ADR-062 way: scoped by org membership, never by `claimed_by`.
|
|
280
|
+
*
|
|
281
|
+
* - With `orgId`: lists that single org's fleet.
|
|
282
|
+
* - Without `orgId`: aggregates the fleet across every org the caller belongs
|
|
283
|
+
* to (the same pattern as {@link listAllTeams}), tagging each row with its
|
|
284
|
+
* org. Orgs the caller can't read (403/404) are skipped silently so one
|
|
285
|
+
* inaccessible org doesn't fail the whole listing.
|
|
286
|
+
*/
|
|
287
|
+
export async function listOrgAgents(orgId) {
|
|
288
|
+
let orgs;
|
|
289
|
+
if (orgId) {
|
|
290
|
+
// Resolve a friendly name if the caller is a member; fall back to the id.
|
|
291
|
+
const mine = await listMyOrgs().catch(() => []);
|
|
292
|
+
const match = mine.find((o) => o.org_id === orgId);
|
|
293
|
+
orgs = [{ org_id: orgId, name: match?.name ?? orgId }];
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
orgs = (await listMyOrgs()).map((o) => ({ org_id: o.org_id, name: o.name }));
|
|
297
|
+
}
|
|
298
|
+
const rows = [];
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
for (const org of orgs) {
|
|
301
|
+
let fleet;
|
|
302
|
+
try {
|
|
303
|
+
fleet = await fetchOrgFleet(org.org_id);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
// A specific --org that we can't read is a real error worth surfacing;
|
|
307
|
+
// a skipped org during aggregation is not.
|
|
308
|
+
if (orgId)
|
|
309
|
+
throw err;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
for (const a of fleet) {
|
|
313
|
+
if (seen.has(a.agent_id))
|
|
314
|
+
continue;
|
|
315
|
+
seen.add(a.agent_id);
|
|
316
|
+
rows.push(fleetToRow(a, org));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return rows;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* POST /v1/agents/:id/claim — claim an agent into an org (ADR-062).
|
|
323
|
+
*
|
|
324
|
+
* Body is `{ hash_proof, org_id? }`. `hash_proof` (the agent's full SHA-256
|
|
325
|
+
* proof) authenticates the caller as the agent's owner; the user's own auth
|
|
326
|
+
* header rides along too so the platform can validate org membership and link
|
|
327
|
+
* the principal. Omit `orgId` to land in the caller's personal org (the
|
|
328
|
+
* platform default); a supplied `orgId` is validated against the caller's
|
|
329
|
+
* memberships server-side (a non-member gets a 403 the command layer turns
|
|
330
|
+
* into a teaching error listing claimable orgs).
|
|
331
|
+
*
|
|
332
|
+
* An Idempotency-Key is minted (and held across the 401-refresh retry) so a
|
|
333
|
+
* retried claim replays the same logical operation server-side.
|
|
334
|
+
*/
|
|
335
|
+
export async function claimAgent(agentId, body, opts = {}) {
|
|
336
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/claim`);
|
|
337
|
+
const payload = {
|
|
338
|
+
hash_proof: sanitizeForHttp(body.hashProof),
|
|
339
|
+
};
|
|
340
|
+
if (body.orgId)
|
|
341
|
+
payload.org_id = sanitizeForHttp(body.orgId);
|
|
342
|
+
const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
|
|
343
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
...(await authHeaders()),
|
|
347
|
+
"Content-Type": "application/json",
|
|
348
|
+
Accept: "application/json",
|
|
349
|
+
"Idempotency-Key": idempotencyKey,
|
|
350
|
+
},
|
|
351
|
+
body: JSON.stringify(payload),
|
|
352
|
+
}));
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
if (response.status === 401) {
|
|
355
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
356
|
+
}
|
|
357
|
+
throw await readApiError(response, "Failed to claim agent");
|
|
358
|
+
}
|
|
359
|
+
const data = (await response.json());
|
|
360
|
+
return {
|
|
361
|
+
claimed: data.claimed ?? true,
|
|
362
|
+
agent_id: data.agent_id ?? agentId,
|
|
363
|
+
// Reflect what the server resolved — null (not "") when absent/unscoped,
|
|
364
|
+
// matching the locked SDK contract so the two surfaces don't drift.
|
|
365
|
+
org_id: data.org_id ?? null,
|
|
366
|
+
claimed_at: data.claimed_at ?? null,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
241
369
|
/**
|
|
242
370
|
* GET /v1/auth/me/personal-org — accessor for the user's personal org.
|
|
243
371
|
* Idempotent: lazily provisions for legacy accounts that pre-date the
|
|
@@ -511,14 +639,14 @@ export async function listTeamAdmins(teamId) {
|
|
|
511
639
|
return (await response.json());
|
|
512
640
|
}
|
|
513
641
|
/**
|
|
514
|
-
* Look up an agent
|
|
642
|
+
* Look up an agent by name across the caller's org fleets (ADR-062-scoped via
|
|
643
|
+
* {@link listOrgAgents}, not the legacy `claimed_by` list).
|
|
515
644
|
* Tries exact match first, then single partial match.
|
|
516
645
|
* Throws if multiple agents partially match (ambiguous).
|
|
517
646
|
* Returns null if no match found.
|
|
518
|
-
* Note: capped at 100 agents by listAgents().
|
|
519
647
|
*/
|
|
520
648
|
export async function getAgentByName(name) {
|
|
521
|
-
const agents = await
|
|
649
|
+
const agents = await listOrgAgents();
|
|
522
650
|
const lower = name.toLowerCase();
|
|
523
651
|
const exact = agents.find((a) => a.name?.toLowerCase() === lower);
|
|
524
652
|
if (exact)
|
|
@@ -607,7 +735,10 @@ export async function getTraces(id, limit = 10) {
|
|
|
607
735
|
*/
|
|
608
736
|
export async function getAlignmentCard(agentId, format = "yaml") {
|
|
609
737
|
const accept = format === "yaml" ? "text/yaml" : "application/json";
|
|
610
|
-
|
|
738
|
+
// Canonical Resources × Scope × Verb surface (ADR-062 / cards-as-primitive
|
|
739
|
+
// W1.2b). The legacy `/v1/agents/:id/alignment-card` only 308-redirects to
|
|
740
|
+
// this — first-party callers target canonical directly (no legacy shapes).
|
|
741
|
+
const url = validateUrl(`${API_BASE}/v1/alignment/agent/${encodeURIComponent(agentId)}`);
|
|
611
742
|
const response = await fetch(url, {
|
|
612
743
|
headers: { Accept: accept, ...(await authHeaders()) },
|
|
613
744
|
});
|
|
@@ -628,7 +759,10 @@ export const ALIGNMENT_CARD_MAX_BYTES = 128 * 1024;
|
|
|
628
759
|
* Accepts YAML or JSON body; set contentType accordingly.
|
|
629
760
|
*/
|
|
630
761
|
export async function putAlignmentCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
631
|
-
|
|
762
|
+
// Canonical Resources × Scope × Verb surface (ADR-062 / cards-as-primitive
|
|
763
|
+
// W1.2b). The legacy `/v1/agents/:id/alignment-card` only 308-redirects to
|
|
764
|
+
// this — first-party callers target canonical directly (no legacy shapes).
|
|
765
|
+
const url = validateUrl(`${API_BASE}/v1/alignment/agent/${encodeURIComponent(agentId)}`);
|
|
632
766
|
// Lock in a single Idempotency-Key for this logical mutation. If the auth
|
|
633
767
|
// token is stale and the first attempt returns 401, fetchWithAuthRetry
|
|
634
768
|
// refreshes the token and retries — but the Idempotency-Key must be the
|
|
@@ -662,7 +796,9 @@ export async function putAlignmentCard(agentId, body, contentType = "text/yaml",
|
|
|
662
796
|
*/
|
|
663
797
|
export async function getProtectionCard(agentId, format = "yaml") {
|
|
664
798
|
const accept = format === "yaml" ? "text/yaml" : "application/json";
|
|
665
|
-
|
|
799
|
+
// Canonical surface (ADR-062 / W1.2b); legacy `/v1/agents/:id/protection-card`
|
|
800
|
+
// only 308-redirects here. First-party callers use canonical directly.
|
|
801
|
+
const url = validateUrl(`${API_BASE}/v1/protection/agent/${encodeURIComponent(agentId)}`);
|
|
666
802
|
const response = await fetch(url, {
|
|
667
803
|
headers: { Accept: accept, ...(await authHeaders()) },
|
|
668
804
|
});
|
|
@@ -682,7 +818,9 @@ export const PROTECTION_CARD_MAX_BYTES = 64 * 1024;
|
|
|
682
818
|
* Publish (create or update) a protection card.
|
|
683
819
|
*/
|
|
684
820
|
export async function putProtectionCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
685
|
-
|
|
821
|
+
// Canonical surface (ADR-062 / W1.2b); legacy `/v1/agents/:id/protection-card`
|
|
822
|
+
// only 308-redirects here. First-party callers use canonical directly.
|
|
823
|
+
const url = validateUrl(`${API_BASE}/v1/protection/agent/${encodeURIComponent(agentId)}`);
|
|
686
824
|
// See putAlignmentCard for why the Idempotency-Key is computed once outside
|
|
687
825
|
// the retry closure.
|
|
688
826
|
const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemom/mnemom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Transparent AI agent tracing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "npm install --prefix ../shared/policy-engine && npm run build --prefix ../shared/policy-engine && tsc",
|
|
12
12
|
"dev": "tsx src/index.ts",
|
|
13
|
+
"gen:command-tree": "tsx scripts/gen-command-tree.mjs",
|
|
14
|
+
"check:command-tree": "tsx scripts/gen-command-tree.mjs --check",
|
|
13
15
|
"test": "vitest"
|
|
14
16
|
},
|
|
15
17
|
"dependencies": {
|