rakomi 0.2.0 → 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 CHANGED
@@ -15,9 +15,11 @@ session to get a short code to enter on another device instead. If you don't alr
15
15
  Rakomi administrator for the right value.
16
16
 
17
17
  `rakomi whoami` shows the tenant you signed in against ("Home tenant") and the tenant your other
18
- commands should treat as active ("Active tenant"). `rakomi use <tenant-id>` remembers a tenant id
19
- locally for that; `--tenant <tenant-id>` overrides it for one command. Neither is a verified
20
- grant — the tenant `whoami` shows as your Home tenant is the only one the server has confirmed.
18
+ commands should treat as active ("Active tenant"). `rakomi tenants memberships` lists every tenant
19
+ your account is a verified member of, with your role in each. `rakomi use <slug>` looks one up from
20
+ that list and remembers it — a slug you don't belong to fails with a plain "not found" rather than
21
+ confirming the tenant exists; `rakomi use <tenant-id>` (a full id) remembers it directly without a
22
+ lookup, and `--tenant <tenant-id>` overrides it for one command.
21
23
 
22
24
  ## Connect an AI agent
23
25
 
@@ -54,10 +56,11 @@ token), sends a real invitation e-mail, and is refused under `--ci` — see
54
56
  | `login` | sign in (browser by default; `--no-browser` for a device code) |
55
57
  | `logout` | clear the local session |
56
58
  | `whoami` | show the signed-in account, home tenant, and active tenant |
57
- | `use <tenant-id>` | remember a tenant id locally for `whoami`/future commands |
59
+ | `use <tenant-id-or-slug>` | remember a tenant locally for `whoami`/future commands; a slug is server-verified |
58
60
  | `connect` | connect Claude Code / Claude Desktop to your tenant (read access) |
59
61
  | `tenants create <name>` | create a tenant (`--owner me\|<email>`, `--slug <slug>`) |
60
62
  | `tenants list` | list tenants you provisioned |
63
+ | `tenants memberships` | list tenants you're a verified member of |
61
64
 
62
65
  ## Global options
63
66
 
@@ -1,6 +1,7 @@
1
1
  import { InteractiveRequiredError, NotLoggedInError, UsageError } from '../errors.js';
2
2
  import { resolveDpopKey } from '../install-key.js';
3
- import { createTenant, listTenants } from '../tenants-client.js';
3
+ import { claimEphemeralTenant, EPHEMERAL_LEASE_LABEL_MAX_LENGTH, EPHEMERAL_TENANT_TTL_SECONDS_MAX, EPHEMERAL_TENANT_TTL_SECONDS_MIN, releaseEphemeralTenant, } from '../lease-client.js';
4
+ import { createTenant, listTenantMemberships, listTenants } from '../tenants-client.js';
4
5
  export async function runTenantsCreate(deps, args) {
5
6
  const session = deps.session.read();
6
7
  if (!session)
@@ -59,3 +60,76 @@ export async function runTenantsList(deps) {
59
60
  }
60
61
  deps.stdout.write(result.data.map((t) => `${t.slug}\t${t.id}\t${t.status}`).join('\n') + '\n');
61
62
  }
63
+ export async function runTenantsMemberships(deps) {
64
+ const session = deps.session.read();
65
+ if (!session)
66
+ throw new NotLoggedInError();
67
+ const result = await listTenantMemberships(deps, {
68
+ apiBaseUrl: session.api_base_url,
69
+ accessToken: session.access_token,
70
+ dpop: resolveDpopKey(deps.keys, session),
71
+ });
72
+ if (deps.json) {
73
+ deps.stdout.write(JSON.stringify(result) + '\n');
74
+ return;
75
+ }
76
+ if (result.data.length === 0) {
77
+ deps.stdout.write('No tenant memberships found for your account.\n');
78
+ return;
79
+ }
80
+ deps.stdout.write(result.data.map((m) => `${m.slug}\t${m.id}\t${m.role}`).join('\n') + '\n');
81
+ }
82
+ function resolveParentTenantId(explicit, tenantConfig) {
83
+ return explicit ?? tenantConfig.read() ?? undefined;
84
+ }
85
+ const NO_PARENT_TENANT_MESSAGE = 'No tenant to operate on — pass --tenant <tenant-id>, or run `rakomi use <tenant-id>` first.';
86
+ export async function runTenantsClaim(deps, args) {
87
+ const session = deps.session.read();
88
+ if (!session)
89
+ throw new NotLoggedInError('Not logged in. Run `rakomi login --ci` first.');
90
+ const parentTenantId = resolveParentTenantId(args.parentTenantId, deps.tenantConfig);
91
+ if (!parentTenantId)
92
+ throw new UsageError(NO_PARENT_TENANT_MESSAGE);
93
+ if (args.ttlSeconds !== undefined &&
94
+ (args.ttlSeconds < EPHEMERAL_TENANT_TTL_SECONDS_MIN || args.ttlSeconds > EPHEMERAL_TENANT_TTL_SECONDS_MAX)) {
95
+ throw new UsageError(`--ttl-seconds must be between ${EPHEMERAL_TENANT_TTL_SECONDS_MIN} and ${EPHEMERAL_TENANT_TTL_SECONDS_MAX}, got ${args.ttlSeconds}`);
96
+ }
97
+ const label = args.label?.trim();
98
+ if (args.label !== undefined && (label === '' || (label?.length ?? 0) > EPHEMERAL_LEASE_LABEL_MAX_LENGTH)) {
99
+ throw new UsageError(`--label must be 1-${EPHEMERAL_LEASE_LABEL_MAX_LENGTH} characters after trimming, got "${args.label}"`);
100
+ }
101
+ const result = await claimEphemeralTenant(deps, {
102
+ apiBaseUrl: session.api_base_url,
103
+ accessToken: session.access_token,
104
+ parentTenantId,
105
+ ttlSeconds: args.ttlSeconds,
106
+ label,
107
+ dpop: resolveDpopKey(deps.keys, session),
108
+ });
109
+ if (deps.json) {
110
+ deps.stdout.write(JSON.stringify(result) + '\n');
111
+ return;
112
+ }
113
+ deps.stdout.write(`Claimed ephemeral tenant "${result.slug}" (${result.tenant_id}) — expires ${result.expires_at} (source: ${result.source})\n` +
114
+ `Lease id: ${result.lease_id} — pass this to \`rakomi tenants release --ci\` when you're done with it.\n`);
115
+ }
116
+ export async function runTenantsRelease(deps, args) {
117
+ const session = deps.session.read();
118
+ if (!session)
119
+ throw new NotLoggedInError('Not logged in. Run `rakomi login --ci` first.');
120
+ const parentTenantId = resolveParentTenantId(args.parentTenantId, deps.tenantConfig);
121
+ if (!parentTenantId)
122
+ throw new UsageError(NO_PARENT_TENANT_MESSAGE);
123
+ await releaseEphemeralTenant(deps, {
124
+ apiBaseUrl: session.api_base_url,
125
+ accessToken: session.access_token,
126
+ parentTenantId,
127
+ tenantId: args.tenantId,
128
+ dpop: resolveDpopKey(deps.keys, session),
129
+ });
130
+ if (deps.json) {
131
+ deps.stdout.write(JSON.stringify({ released: true, tenant_id: args.tenantId }) + '\n');
132
+ return;
133
+ }
134
+ deps.stdout.write(`Released ephemeral tenant ${args.tenantId} — deletion scheduled.\n`);
135
+ }
@@ -1,10 +1,26 @@
1
1
  import { isValidTenantId } from '../env.js';
2
- import { UsageError } from '../errors.js';
3
- export function runUse(deps, args) {
4
- if (!isValidTenantId(args.tenantId)) {
5
- throw new UsageError(`"${args.tenantId}" is not a valid tenant id — expected a UUID (e.g. the value \`rakomi login\` ` +
6
- 'printed as "Home tenant", or the id shown in the dashboard).');
2
+ import { NotLoggedInError, UsageError } from '../errors.js';
3
+ import { resolveDpopKey } from '../install-key.js';
4
+ import { getTenantMembershipBySlug } from '../tenants-client.js';
5
+ export async function runUse(deps, args) {
6
+ if (isValidTenantId(args.tenantId)) {
7
+ deps.tenantConfig.write(args.tenantId);
8
+ deps.stdout.write(`Active tenant set to ${args.tenantId}.\n`);
9
+ return;
7
10
  }
8
- deps.tenantConfig.write(args.tenantId);
9
- deps.stdout.write(`Active tenant set to ${args.tenantId}.\n`);
11
+ const session = deps.session.read();
12
+ if (!session)
13
+ throw new NotLoggedInError();
14
+ const membership = await getTenantMembershipBySlug(deps, {
15
+ apiBaseUrl: session.api_base_url,
16
+ accessToken: session.access_token,
17
+ slug: args.tenantId,
18
+ dpop: resolveDpopKey(deps.keys, session),
19
+ });
20
+ if (!membership) {
21
+ throw new UsageError(`No tenant membership found for "${args.tenantId}" — check the spelling, or ask a tenant ` +
22
+ 'admin to invite you. (Run `rakomi tenants memberships` to see the tenants you already belong to.)');
23
+ }
24
+ deps.tenantConfig.write(membership.id);
25
+ deps.stdout.write(`Active tenant set to ${membership.slug} (${membership.id}) — role: ${membership.role}.\n`);
10
26
  }
package/dist/env.js CHANGED
@@ -3,6 +3,7 @@ export const DEFAULT_API_BASE_URL = 'https://api.rakomi.com';
3
3
  export const DEFAULT_ACCOUNTS_BASE_URL = 'https://accounts.rakomi.com';
4
4
  export const DEFAULT_MCP_URL = 'https://mcp.rakomi.com/mcp';
5
5
  export const DEFAULT_CIMD_CLIENT_ID_URL = 'https://api.rakomi.com/.well-known/rakomi-cli/client-metadata.json';
6
+ export const DEFAULT_PLATFORM_TENANT_ID = '01ef847e-4c40-4960-af05-11112c4764b2';
6
7
  export const CI_FEDERATION_SESSION_CLIENT_ID = 'oidc-federation';
7
8
  export function isCimdClientId(id) {
8
9
  let url;
@@ -36,7 +37,10 @@ export function isValidTenantId(id) {
36
37
  }
37
38
  export function platformTenantId(env) {
38
39
  const raw = env.RAKOMI_PLATFORM_TENANT_ID;
39
- return raw && isValidTenantId(raw) ? raw : undefined;
40
+ if (raw)
41
+ return isValidTenantId(raw) ? raw : undefined;
42
+ const isUnoverriddenProdHost = apiBaseUrl(env) === DEFAULT_API_BASE_URL && accountsBaseUrl(env) === DEFAULT_ACCOUNTS_BASE_URL;
43
+ return isUnoverriddenProdHost ? DEFAULT_PLATFORM_TENANT_ID : undefined;
40
44
  }
41
45
  const CLIENT_ID_METADATA_DOCUMENT_SUPPORTED_FIELD = 'client_id_metadata_document_supported';
42
46
  export async function isCimdPlatformSupported(deps, apiUrl) {
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { runConnect } from './commands/connect.js';
9
9
  import { runLogin, runLoginCi } from './commands/login.js';
10
10
  import { runLogout } from './commands/logout.js';
11
11
  import { runMcpToolsCi } from './commands/mcp.js';
12
- import { runTenantsCreate, runTenantsList } from './commands/tenants.js';
12
+ import { runTenantsClaim, runTenantsCreate, runTenantsList, runTenantsMemberships, runTenantsRelease } from './commands/tenants.js';
13
13
  import { runUse } from './commands/use.js';
14
14
  import { runWhoami } from './commands/whoami.js';
15
15
  import { apiBaseUrl, DEFAULT_MCP_URL } from './env.js';
@@ -33,6 +33,8 @@ const GLOBAL_OPTIONS = {
33
33
  slug: { type: 'string' },
34
34
  'tenant-id': { type: 'string' },
35
35
  tenant: { type: 'string' },
36
+ 'ttl-seconds': { type: 'string' },
37
+ label: { type: 'string' },
36
38
  'cimd-url': { type: 'string' },
37
39
  status: { type: 'boolean' },
38
40
  help: { type: 'boolean', short: 'h' },
@@ -55,6 +57,15 @@ export async function run(args, deps) {
55
57
  return EXIT.FAIL;
56
58
  }
57
59
  }
60
+ function parseTtlSeconds(raw) {
61
+ if (typeof raw !== 'string')
62
+ return undefined;
63
+ const n = Number(raw);
64
+ if (!Number.isInteger(n) || n <= 0) {
65
+ throw new UsageError(`--ttl-seconds must be a positive integer, got "${raw}"`);
66
+ }
67
+ return n;
68
+ }
58
69
  async function dispatch(args, deps) {
59
70
  let values;
60
71
  let positionals;
@@ -127,8 +138,8 @@ async function dispatch(args, deps) {
127
138
  case 'use': {
128
139
  const tenantId = rest[0];
129
140
  if (!tenantId)
130
- throw new UsageError('Usage: rakomi use <tenant-id>');
131
- runUse({ tenantConfig: deps.tenantConfig, stdout: deps.stdout }, { tenantId });
141
+ throw new UsageError('Usage: rakomi use <tenant-id-or-slug>');
142
+ await runUse({ ...httpDeps, session: deps.session, keys: deps.keys, tenantConfig: deps.tenantConfig, stdout: deps.stdout }, { tenantId });
132
143
  return EXIT.OK;
133
144
  }
134
145
  case 'connect':
@@ -183,7 +194,32 @@ async function dispatch(args, deps) {
183
194
  await runTenantsList({ ...httpDeps, session: deps.session, keys: deps.keys, json, stdout: deps.stdout });
184
195
  return EXIT.OK;
185
196
  }
186
- throw new UsageError('Usage: rakomi tenants <create|list> ...');
197
+ if (sub === 'memberships') {
198
+ await runTenantsMemberships({ ...httpDeps, session: deps.session, keys: deps.keys, json, stdout: deps.stdout });
199
+ return EXIT.OK;
200
+ }
201
+ if (sub === 'claim') {
202
+ if (values.ci !== true) {
203
+ throw new UsageError('rakomi tenants claim requires --ci (no interactive form exists yet).');
204
+ }
205
+ await runTenantsClaim({ ...httpDeps, session: deps.session, keys: deps.keys, tenantConfig: deps.tenantConfig, json, stdout: deps.stdout }, {
206
+ parentTenantId: typeof values.tenant === 'string' ? values.tenant : undefined,
207
+ ttlSeconds: parseTtlSeconds(values['ttl-seconds']),
208
+ label: typeof values.label === 'string' ? values.label : undefined,
209
+ });
210
+ return EXIT.OK;
211
+ }
212
+ if (sub === 'release') {
213
+ if (values.ci !== true) {
214
+ throw new UsageError('rakomi tenants release requires --ci (no interactive form exists yet).');
215
+ }
216
+ const tenantId = tenantArgs[0];
217
+ if (!tenantId)
218
+ throw new UsageError('Usage: rakomi tenants release --ci <tenant-id> [--tenant <parent-tenant-id>]');
219
+ await runTenantsRelease({ ...httpDeps, session: deps.session, keys: deps.keys, tenantConfig: deps.tenantConfig, json, stdout: deps.stdout }, { tenantId, parentTenantId: typeof values.tenant === 'string' ? values.tenant : undefined });
220
+ return EXIT.OK;
221
+ }
222
+ throw new UsageError('Usage: rakomi tenants <create|list|claim|release> ...');
187
223
  }
188
224
  default:
189
225
  throw new UsageError(`Unknown command "${command ?? ''}".`);
@@ -0,0 +1,64 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { CliError, EXIT } from './errors.js';
3
+ import { describeError, errorCode, request } from './http.js';
4
+ export const EPHEMERAL_TENANT_TTL_SECONDS_MIN = 600;
5
+ export const EPHEMERAL_TENANT_TTL_SECONDS_MAX = 1_209_600;
6
+ export const EPHEMERAL_LEASE_LABEL_MAX_LENGTH = 200;
7
+ function generateIdempotencyKey() {
8
+ return randomBytes(16).toString('hex');
9
+ }
10
+ const CHILD_TENANT_LIMIT_REACHED_CODE = 'tenant/child_tenant_limit_reached';
11
+ export async function claimEphemeralTenant(deps, opts) {
12
+ const result = await request(deps, {
13
+ method: 'POST',
14
+ url: `${opts.apiBaseUrl}/v1/tenants/${encodeURIComponent(opts.parentTenantId)}/ephemeral/claims`,
15
+ headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
16
+ body: {
17
+ ...(opts.ttlSeconds !== undefined ? { ttl_seconds: opts.ttlSeconds } : {}),
18
+ ...(opts.label ? { label: opts.label } : {}),
19
+ },
20
+ idempotencyKey: opts.idempotencyKey ?? generateIdempotencyKey(),
21
+ dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
22
+ });
23
+ if (result.status === 401) {
24
+ throw new CliError('Your session has expired. Run `rakomi login --ci` again.', EXIT.NOT_LOGGED_IN);
25
+ }
26
+ if (result.status === 403) {
27
+ throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)} ` +
28
+ "(missing the `tenants:lease` scope — check this CI identity's trust policy).", EXIT.FAIL);
29
+ }
30
+ if (result.status === 404) {
31
+ throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)} ` +
32
+ "(the given tenant is not the caller's own tenant, or the caller's own tenant is itself " +
33
+ 'ephemeral — ephemeral tenants cannot nest).', EXIT.FAIL);
34
+ }
35
+ if (result.status === 409 && errorCode(result.body) === CHILD_TENANT_LIMIT_REACHED_CODE) {
36
+ throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)}.`, EXIT.FAIL);
37
+ }
38
+ if (result.status !== 201) {
39
+ throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)}`, EXIT.FAIL);
40
+ }
41
+ return result.body;
42
+ }
43
+ export async function releaseEphemeralTenant(deps, opts) {
44
+ const result = await request(deps, {
45
+ method: 'DELETE',
46
+ url: `${opts.apiBaseUrl}/v1/tenants/${encodeURIComponent(opts.parentTenantId)}/ephemeral/claims/${encodeURIComponent(opts.tenantId)}`,
47
+ headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
48
+ dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
49
+ });
50
+ if (result.status === 401) {
51
+ throw new CliError('Your session has expired. Run `rakomi login --ci` again.', EXIT.NOT_LOGGED_IN);
52
+ }
53
+ if (result.status === 403) {
54
+ throw new CliError(`Could not release the ephemeral tenant: ${describeError(result.body, result.status)} ` +
55
+ "(missing the `tenants:lease` scope — check this CI identity's trust policy).", EXIT.FAIL);
56
+ }
57
+ if (result.status === 404) {
58
+ throw new CliError(`Could not release the ephemeral tenant: ${describeError(result.body, result.status)} ` +
59
+ '(the given tenant id does not exist under the given parent, or is not an ephemeral tenant).', EXIT.FAIL);
60
+ }
61
+ if (result.status !== 204) {
62
+ throw new CliError(`Could not release the ephemeral tenant: ${describeError(result.body, result.status)}`, EXIT.FAIL);
63
+ }
64
+ }
@@ -64,3 +64,46 @@ export async function listTenants(deps, opts) {
64
64
  }
65
65
  return result.body;
66
66
  }
67
+ function describeCallerUnverified(action) {
68
+ return (`Could not ${action}: your account e-mail is not verified (or is disabled). Verify your e-mail ` +
69
+ 'and sign in again.');
70
+ }
71
+ export async function listTenantMemberships(deps, opts) {
72
+ const result = await request(deps, {
73
+ method: 'GET',
74
+ url: `${opts.apiBaseUrl}/v1/tenants/memberships`,
75
+ headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
76
+ dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
77
+ });
78
+ if (result.status === 401) {
79
+ throw new CliError('Your session has expired. Run `rakomi login` again.', EXIT.NOT_LOGGED_IN);
80
+ }
81
+ if (result.status === 403) {
82
+ throw new CliError(describeCallerUnverified('list your tenant memberships'), EXIT.FAIL);
83
+ }
84
+ if (result.status !== 200) {
85
+ throw new CliError(`Could not list your tenant memberships: ${describeError(result.body, result.status)}`, EXIT.FAIL);
86
+ }
87
+ return result.body;
88
+ }
89
+ export async function getTenantMembershipBySlug(deps, opts) {
90
+ const result = await request(deps, {
91
+ method: 'GET',
92
+ url: `${opts.apiBaseUrl}/v1/tenants/memberships/${encodeURIComponent(opts.slug)}`,
93
+ headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
94
+ dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
95
+ });
96
+ if (result.status === 401) {
97
+ throw new CliError('Your session has expired. Run `rakomi login` again.', EXIT.NOT_LOGGED_IN);
98
+ }
99
+ if (result.status === 404) {
100
+ return null;
101
+ }
102
+ if (result.status === 403) {
103
+ throw new CliError(describeCallerUnverified('look up this tenant membership'), EXIT.FAIL);
104
+ }
105
+ if (result.status !== 200) {
106
+ throw new CliError(`Could not look up tenant "${opts.slug}": ${describeError(result.body, result.status)}`, EXIT.FAIL);
107
+ }
108
+ return result.body;
109
+ }
package/dist/usage.js CHANGED
@@ -12,7 +12,7 @@ export function helpText() {
12
12
  ' --tenant-id <uuid> the tenant to authenticate against (or set RAKOMI_PLATFORM_TENANT_ID)',
13
13
  ' logout clear the local session',
14
14
  ' whoami show the signed-in account, home tenant, and active tenant',
15
- ' use <tenant-id> remember a tenant id locally for whoami/future commands (a value, not a verified membership)',
15
+ ' use <tenant-id-or-slug> remember a tenant locally (whoami/future commands); a UUID is stored as-is, a slug is server-verified',
16
16
  ' connect connect Claude Code / Claude Desktop to your tenant (read access)',
17
17
  ' --write also request write access for the connected client',
18
18
  ' --client <name> claude-code | claude-desktop (required if more than one applies)',
@@ -23,6 +23,13 @@ export function helpText() {
23
23
  ' --owner <me|email> who becomes the new tenant\'s owner (default: me)',
24
24
  ' --slug <slug> optional; auto-derived + suffixed if omitted',
25
25
  ' tenants list list tenants you provisioned',
26
+ ' tenants memberships list tenants your account is a verified member of',
27
+ ' tenants claim --ci claim a temporary, self-expiring tenant under your own tenant',
28
+ ' --tenant <id> your own tenant to claim under (or run `rakomi use <tenant-id>` first)',
29
+ ' --ttl-seconds <n> how long the claim should live, in seconds (only for a freshly-created tenant)',
30
+ ' --label <text> optional label, stored and echoed back, never interpreted',
31
+ ' tenants release --ci <tenant-id> release a tenant claimed by `tenants claim`',
32
+ ' --tenant <id> the tenant it was claimed under (or run `rakomi use <tenant-id>` first)',
26
33
  ' mcp tools --ci list MCP read tools your CI identity can invoke (no login needed first)',
27
34
  '',
28
35
  'Global options:',
@@ -31,7 +38,7 @@ export function helpText() {
31
38
  ' --dry-run print what would happen, make no writes or mutating calls',
32
39
  ' --no-browser use the RFC 8628 device-code flow instead of opening a browser',
33
40
  ' --no-keychain store the session in a 0600 file instead of the OS keychain',
34
- ' --tenant <id> override the active tenant for this invocation (whoami only today)',
41
+ ' --tenant <id> the tenant for this invocation (whoami, tenants claim/release)',
35
42
  ' -h, --help show this help and exit',
36
43
  ' -V, --version print the version and exit',
37
44
  '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rakomi",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The Rakomi CLI — sign in, connect an AI agent (Claude Code, Claude Desktop) to your tenant, and provision tenants. EU-native auth-as-a-service.",
5
5
  "keywords": [
6
6
  "rakomi",
package/sbom.cdx.json CHANGED
@@ -26,10 +26,10 @@
26
26
  },
27
27
  "component": {
28
28
  "type": "library",
29
- "bom-ref": "pkg:npm/rakomi@0.2.0",
29
+ "bom-ref": "pkg:npm/rakomi@0.3.0",
30
30
  "name": "rakomi",
31
- "version": "0.2.0",
32
- "purl": "pkg:npm/rakomi@0.2.0",
31
+ "version": "0.3.0",
32
+ "purl": "pkg:npm/rakomi@0.3.0",
33
33
  "licenses": [
34
34
  {
35
35
  "license": {