@usecontextlayer/cli 0.14.3 → 0.15.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.
Files changed (32) hide show
  1. package/README.md +36 -0
  2. package/dist/auth-CMrKVwsH.mjs +6847 -0
  3. package/dist/auth-CMrKVwsH.mjs.map +1 -0
  4. package/dist/auth.d.mts +23 -0
  5. package/dist/auth.d.mts.map +1 -0
  6. package/dist/auth.mjs +2 -0
  7. package/dist/cli.mjs +141 -38
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{resources-DEdwfmku.mjs → resources-5FTDu19i.mjs} +152 -5619
  10. package/dist/resources-5FTDu19i.mjs.map +1 -0
  11. package/dist/rolldown-runtime-EAsCxrDo.mjs +14 -0
  12. package/dist/template/AGENTS.md +13 -0
  13. package/dist/template/README.md +44 -0
  14. package/dist/template/app/root.tsx +28 -0
  15. package/dist/template/app/routes/home.tsx +16 -3
  16. package/dist/template/databases/updates/drizzle.config.ts +8 -0
  17. package/dist/template/databases/updates/migrations/0000_create_updates.sql +6 -0
  18. package/dist/template/databases/updates/migrations/meta/0000_snapshot.json +14 -0
  19. package/dist/template/databases/updates/migrations/meta/_journal.json +13 -0
  20. package/dist/template/databases/updates/migrations/migrations.js +10 -0
  21. package/dist/template/databases/updates/schema.ts +8 -0
  22. package/dist/template/workers/app.ts +2 -1
  23. package/dist/template/workers/local-resources/index.ts +1 -0
  24. package/dist/template/workers/local-resources/wrangler.jsonc +19 -0
  25. package/dist/vite.d.mts +4426 -2
  26. package/dist/vite.d.mts.map +1 -1
  27. package/dist/vite.mjs +92 -8
  28. package/dist/vite.mjs.map +1 -1
  29. package/package.json +19 -3
  30. package/dist/resources-DEdwfmku.mjs.map +0 -1
  31. package/dist/template/workers/local-tools/index.ts +0 -1
  32. package/dist/template/workers/local-tools/wrangler.jsonc +0 -5
@@ -0,0 +1,23 @@
1
+ //#region env.d.ts
2
+ declare const env: {
3
+ CTX_AUTH_CLIENT_ID: string;
4
+ CTX_AUTH_ISSUER: string;
5
+ CTX_PLATFORM_URL: string;
6
+ };
7
+ //#endregion
8
+ //#region auth.d.ts
9
+ type AuthEnv = Pick<typeof env, "CTX_AUTH_ISSUER" | "CTX_AUTH_CLIENT_ID">;
10
+ export declare class LoginRequiredError extends Error {
11
+ constructor(reason: string, options?: ErrorOptions);
12
+ }
13
+ export declare function getAccessToken(env: AuthEnv): Promise<string>;
14
+ export declare function getCurrentUser(env: AuthEnv, headers: HeadersInit): Promise<{
15
+ id: string;
16
+ }>;
17
+ export declare function listOrganizations(env: AuthEnv, headers: HeadersInit): Promise<{
18
+ id: string;
19
+ slug: string;
20
+ }[]>;
21
+ //#endregion
22
+ export type { AuthEnv };
23
+ //# sourceMappingURL=auth.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.mts","names":[],"sources":["../env.ts","../auth.ts"],"mappings":";cAMa;;;;;;;KCCD,UAAU,YAAY;qBAYrB,2BAA2B;EACvC,YAAY,gBAAgB,UAAU;;wBAiDjB,eAAe,KAAK,UAAO;wBAqC3B,eAAe,KAAK,SAAS,SAAS,cAAW;;;wBAOjD,kBAAkB,KAAK,SAAS,SAAS,cAAW"}
package/dist/auth.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { i as listOrganizations, n as getAccessToken, r as getCurrentUser, t as LoginRequiredError } from "./auth-CMrKVwsH.mjs";
2
+ export { LoginRequiredError, getAccessToken, getCurrentUser, listOrganizations };
package/dist/cli.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { C as unknown, E as execa, S as union, T as uuid, _ as literal, b as record, c as getCurrentUser, d as env, g as hostname, h as boolean, i as ownerSchema, l as login, m as array, n as saveLocalConnection, o as joinApp, p as secretNameSchema, r as appResourceConfigurationSchema, s as getAccessToken, t as prepareLocalResourceConfiguration, u as whoami, v as looseObject, w as url, x as string, y as object } from "./resources-DEdwfmku.mjs";
2
+ import { t as __commonJSMin } from "./rolldown-runtime-EAsCxrDo.mjs";
3
+ import { S as uuid, a as login, b as unknown, c as array, d as hostname, f as literal, g as record, h as object, i as listOrganizations, l as boolean, n as getAccessToken, p as looseObject, r as getCurrentUser, t as LoginRequiredError, v as string, x as url, y as union } from "./auth-CMrKVwsH.mjs";
4
+ import { c as resourcesForConsumer, f as execa, i as ownerSchema, l as secretNameSchema, n as saveLocalConnection, o as joinApp, r as appResourceConfigurationSchema, s as env, t as prepareLocalResourceConfiguration, u as databaseQuerySchema } from "./resources-5FTDu19i.mjs";
3
5
  import { cp, readFile, writeFile } from "node:fs/promises";
4
6
  import path, { join } from "node:path";
5
7
  import { EventEmitter } from "node:events";
@@ -9,9 +11,6 @@ import process$1, { stdin, stdout } from "node:process";
9
11
  import { stripVTControlCharacters, styleText } from "node:util";
10
12
  import { fileURLToPath } from "node:url";
11
13
  import l__default, { createInterface } from "node:readline";
12
- //#region \0rolldown/runtime.js
13
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
14
- //#endregion
15
14
  //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
16
15
  /**
17
16
  * CommanderError class
@@ -2968,13 +2967,40 @@ function useColor() {
2968
2967
  new Command();
2969
2968
  //#endregion
2970
2969
  //#region agents.ts
2971
- const platformInstructions = "You are a ContextLayer app agent. The user message is the task for this run. Work in the app directory and return the task's result.";
2970
+ const platformInstructions = "You are a ContextLayer app agent. The user message is the task for this run. Work in the app directory and return the task's result. Use database_query to read and save persistent data in declared databases. Files are scratch data; schema changes belong in explicitly authored SQL migrations.";
2972
2971
  async function runLocalAgent(name, prompt) {
2973
2972
  const instructions = await readFile(join("agents", name, "instructions.md"), "utf8");
2974
- const { query } = await import("@anthropic-ai/claude-agent-sdk");
2973
+ const { createServer } = await import("vite");
2974
+ const { getPreparedLocalBindings, openLocalDatabases } = await import("./vite.mjs");
2975
+ const server = await createServer({
2976
+ logLevel: "error",
2977
+ server: {
2978
+ host: "127.0.0.1",
2979
+ port: 0
2980
+ }
2981
+ });
2982
+ await server.listen();
2983
+ const bindings = getPreparedLocalBindings(server);
2984
+ const configuration = resourcesForConsumer(bindings.configuration, {
2985
+ name,
2986
+ type: "agent"
2987
+ });
2988
+ const storage = await openLocalDatabases(bindings.workerName, configuration.databases);
2989
+ const { query, createSdkMcpServer, tool } = await import("@anthropic-ai/claude-agent-sdk");
2975
2990
  const run = query({
2976
2991
  options: {
2992
+ allowedTools: ["mcp__databases__database_query"],
2977
2993
  cwd: process.cwd(),
2994
+ mcpServers: { databases: createSdkMcpServer({
2995
+ name: "databases",
2996
+ tools: [tool("database_query", "Query or update a declared persistent SQLite database using parameterized SQL.", databaseQuerySchema.shape, async ({ slug, sql, params }) => {
2997
+ const result = await storage.databases.query(slug, sql, params);
2998
+ return { content: [{
2999
+ text: JSON.stringify(result),
3000
+ type: "text"
3001
+ }] };
3002
+ })]
3003
+ }) },
2978
3004
  settingSources: [],
2979
3005
  systemPrompt: {
2980
3006
  append: `${platformInstructions}\n\n${instructions}`,
@@ -3001,6 +3027,8 @@ async function runLocalAgent(name, prompt) {
3001
3027
  throw new Error("Claude ended without a result");
3002
3028
  } finally {
3003
3029
  run.close();
3030
+ await storage.dispose();
3031
+ await server.close();
3004
3032
  }
3005
3033
  }
3006
3034
  //#endregion
@@ -3861,7 +3889,7 @@ const connectionSchema = object({
3861
3889
  alias: string().nullable(),
3862
3890
  display_name: string().nullable(),
3863
3891
  id: string(),
3864
- status: string(),
3892
+ status: literal("ACTIVE"),
3865
3893
  toolkit: string()
3866
3894
  });
3867
3895
  const connectionsSchema = object({ connections: array(connectionSchema) });
@@ -3870,16 +3898,10 @@ const connectionsSchema = object({ connections: array(connectionSchema) });
3870
3898
  const ownerOptionsSchema = object({ org: string().min(1).optional() });
3871
3899
  async function resolveOwner(org, headers) {
3872
3900
  if (!org) return {
3873
- id: (await getCurrentUser(headers)).id,
3901
+ id: (await getCurrentUser(env, headers)).id,
3874
3902
  type: "user"
3875
3903
  };
3876
- const response = await fetch(new URL("/api/platform/organizations", env.CTX_AUTH_ISSUER), { headers });
3877
- if (!response.ok) throw new Error(`Organization lookup failed: HTTP ${response.status}`);
3878
- const { organizations } = object({ organizations: array(object({
3879
- id: string().min(1),
3880
- slug: string()
3881
- })) }).parse(await response.json());
3882
- const organization = organizations.find((organization) => organization.slug === org);
3904
+ const organization = (await listOrganizations(env, headers)).find((organization) => organization.slug === org);
3883
3905
  if (!organization) throw new Error("Organization is not in your memberships");
3884
3906
  return {
3885
3907
  id: organization.id,
@@ -3910,10 +3932,10 @@ async function assignAppConnection(target, slug, connectionId) {
3910
3932
  if (connectionId === void 0 && !process.stdin.isTTY) throw new Error("Supply a connection ID when assigning without a terminal");
3911
3933
  const url = new URL("/api/connections", env.CTX_PLATFORM_URL);
3912
3934
  url.searchParams.set("toolkit", requirement.toolkit);
3913
- const response = await fetch(url, { headers: { authorization: `Bearer ${await getAccessToken()}` } });
3935
+ const response = await fetch(url, { headers: { authorization: `Bearer ${await getAccessToken(env)}` } });
3914
3936
  if (!response.ok) throw new Error(`Personal connection list failed: HTTP ${response.status}`);
3915
3937
  const { connections } = connectionsSchema.parse(await response.json());
3916
- if (connections.length === 0) throw new Error(`No personal ${requirement.toolkit} accounts. Run ctx connections add ${requirement.toolkit}.`);
3938
+ if (connections.length === 0) throw new Error(`No personal ${requirement.toolkit} accounts. Run npx @usecontextlayer/cli connections add ${requirement.toolkit}, approve access in your browser, then rerun this assignment.`);
3917
3939
  if (connectionId === void 0) {
3918
3940
  const selected = await select({
3919
3941
  message: `Your ${requirement.toolkit} account for ${slug}`,
@@ -3932,6 +3954,7 @@ async function assignAppConnection(target, slug, connectionId) {
3932
3954
  if (!connections.some(({ id }) => id === connectionId)) throw new Error(`Account ${connectionId} is not one of your ${requirement.toolkit} accounts`);
3933
3955
  await saveLocalConnection(slug, connectionId);
3934
3956
  console.log(`${slug} → ${connectionId}`);
3957
+ console.log("Start or restart pnpm dev to use this assignment.");
3935
3958
  return;
3936
3959
  }
3937
3960
  const response = await fetch(new URL(`/api/apps/${encodeURIComponent(target)}/connections/assign`, env.CTX_PLATFORM_URL), {
@@ -3940,7 +3963,7 @@ async function assignAppConnection(target, slug, connectionId) {
3940
3963
  slug
3941
3964
  }),
3942
3965
  headers: {
3943
- authorization: `Bearer ${await getAccessToken()}`,
3966
+ authorization: `Bearer ${await getAccessToken(env)}`,
3944
3967
  "content-type": "application/json"
3945
3968
  },
3946
3969
  method: "POST"
@@ -3971,7 +3994,7 @@ async function listAppConnections(target) {
3971
3994
  const response = await fetch(new URL(`/api/apps/${encodeURIComponent(target)}/connections/list`, env.CTX_PLATFORM_URL), {
3972
3995
  body: JSON.stringify({}),
3973
3996
  headers: {
3974
- authorization: `Bearer ${await getAccessToken()}`,
3997
+ authorization: `Bearer ${await getAccessToken(env)}`,
3975
3998
  "content-type": "application/json"
3976
3999
  },
3977
4000
  method: "POST"
@@ -3988,7 +4011,7 @@ async function listAppConnections(target) {
3988
4011
  })));
3989
4012
  }
3990
4013
  async function addConnection(toolkit, org) {
3991
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4014
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
3992
4015
  const owner = await resolveOwner(org, headers);
3993
4016
  const response = await fetch(new URL(`/api/connections/${ownerPath(owner)}`, env.CTX_PLATFORM_URL), {
3994
4017
  body: JSON.stringify({ toolkit }),
@@ -4001,10 +4024,12 @@ async function addConnection(toolkit, org) {
4001
4024
  if (!response.ok) throw new Error(`Connection authorization failed: HTTP ${response.status}`);
4002
4025
  const { redirect_url } = object({ redirect_url: url() }).parse(await response.json());
4003
4026
  console.log(`Open this link to authorize the Owner's account:\n${redirect_url}`);
4004
- console.log(`After authorization, run ctx connections${org ? ` --org ${org}` : ""} list to see the Owner's account.`);
4027
+ console.log("After approving access in your browser, assign the account to an app's declared connection slug.");
4028
+ if (!org) console.log("From your app directory, choose your account for a slug in public/slate.json:\n npx @usecontextlayer/cli connection local assign <slug>");
4029
+ console.log(`For a hosted app, get the account ID and assign it:\n npx @usecontextlayer/cli connections${org ? ` --org '${org.replaceAll("'", "'\\''")}'` : ""} list\n npx @usecontextlayer/cli connection <AppID-or-hostname> assign <slug> <connection_id>`);
4005
4030
  }
4006
4031
  async function listConnections(org) {
4007
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4032
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
4008
4033
  const owner = await resolveOwner(org, headers);
4009
4034
  const response = await fetch(new URL(`/api/connections/${ownerPath(owner)}`, env.CTX_PLATFORM_URL), { headers });
4010
4035
  if (!response.ok) throw new Error(`Connection list failed: HTTP ${response.status}`);
@@ -4053,11 +4078,11 @@ async function gitCredential(operation) {
4053
4078
  attributes.append(line.slice(0, separator), line.slice(separator + 1));
4054
4079
  }
4055
4080
  literal("authtype").parse(attributes.getAll("capability[]").find((c) => c === "authtype"));
4056
- process.stdout.write(`capability[]=authtype\nauthtype=Bearer\ncredential=${await getAccessToken()}\n\n`);
4081
+ process.stdout.write(`capability[]=authtype\nauthtype=Bearer\ncredential=${await getAccessToken(env)}\n\n`);
4057
4082
  }
4058
4083
  //#endregion
4059
4084
  //#region package.json
4060
- var version = "0.14.3";
4085
+ var version = "0.15.0";
4061
4086
  //#endregion
4062
4087
  //#region secrets.ts
4063
4088
  const secretOptionsSchema = ownerOptionsSchema.extend({ stdin: boolean().default(false) });
@@ -4071,7 +4096,7 @@ async function resolveSecretUrl(name, org, headers) {
4071
4096
  return new URL(`/api/secrets/${ownerPath(owner)}/${encodeURIComponent(name)}`, env.CTX_PLATFORM_URL);
4072
4097
  }
4073
4098
  async function setSecret(name, options) {
4074
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4099
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
4075
4100
  const url = await resolveSecretUrl(name, options.org, headers);
4076
4101
  let value;
4077
4102
  if (options.stdin) {
@@ -4103,7 +4128,7 @@ async function setSecret(name, options) {
4103
4128
  console.log(JSON.stringify(confirmationSchema.parse(await response.json())));
4104
4129
  }
4105
4130
  async function dropSecret(name, org) {
4106
- const headers = { authorization: `Bearer ${await getAccessToken()}` };
4131
+ const headers = { authorization: `Bearer ${await getAccessToken(env)}` };
4107
4132
  const response = await fetch(await resolveSecretUrl(name, org, headers), {
4108
4133
  headers,
4109
4134
  method: "DELETE"
@@ -4114,9 +4139,31 @@ async function dropSecret(name, org) {
4114
4139
  //#endregion
4115
4140
  //#region cli.ts
4116
4141
  const program = new Command().name("ctx").description("ContextLayer CLI").version(version);
4117
- program.command("init <directory>").description("Create a Cloudflare React Router project with Tailwind and shadcn Base UI, register and join a private app owned by you or an organization (ctx login), and set its immutable Git remote").option("--org <slug>", "Create the app under an organization you belong to").action(async (directory, options) => {
4142
+ program.addHelpText("afterAll", "\nRun without a global install: npx @usecontextlayer/cli <command>");
4143
+ async function authenticate(command) {
4144
+ try {
4145
+ return await getCurrentUser(env, { authorization: `Bearer ${await getAccessToken(env)}` });
4146
+ } catch (error) {
4147
+ if (error instanceof LoginRequiredError) command.error(error.message);
4148
+ throw error;
4149
+ }
4150
+ }
4151
+ async function requireLogin(command) {
4152
+ await authenticate(command);
4153
+ }
4154
+ program.command("init <directory>").description("Create a private ContextLayer app on main with React Router, Tailwind, and shadcn; requires login, Node.js 24+, pnpm, and Git author identity; sets the Git remote and prints how to start and publish the app").option("--org <slug>", "Create the app under an organization you belong to").hook("preAction", requireLogin).action(async (directory, options) => {
4118
4155
  const input = object({ organization_slug: string().min(1).optional() }).parse({ organization_slug: options.org });
4119
- const accessToken = await getAccessToken();
4156
+ const accessToken = await getAccessToken(env);
4157
+ const gitIdentity = await execa("git", ["var", "GIT_AUTHOR_IDENT"], {
4158
+ reject: false,
4159
+ stdio: [
4160
+ "inherit",
4161
+ "ignore",
4162
+ "inherit"
4163
+ ]
4164
+ });
4165
+ process.exitCode = gitIdentity.exitCode;
4166
+ if (gitIdentity.exitCode !== 0) return;
4120
4167
  const result = await execa("pnpm", [
4121
4168
  "create",
4122
4169
  "cloudflare@latest",
@@ -4128,6 +4175,11 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4128
4175
  "--",
4129
4176
  "--yes"
4130
4177
  ], {
4178
+ env: {
4179
+ GIT_CONFIG_COUNT: "1",
4180
+ GIT_CONFIG_KEY_0: "init.defaultBranch",
4181
+ GIT_CONFIG_VALUE_0: "main"
4182
+ },
4131
4183
  reject: false,
4132
4184
  stdio: "inherit"
4133
4185
  });
@@ -4135,7 +4187,18 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4135
4187
  if (result.exitCode !== 0) return;
4136
4188
  const tsconfigPath = join(directory, "tsconfig.json");
4137
4189
  const tsconfig = looseObject({ compilerOptions: record(string(), unknown()) }).parse(JSON.parse(await readFile(tsconfigPath, "utf8")));
4138
- tsconfig.compilerOptions.paths = { "~/*": ["./app/*"] };
4190
+ tsconfig.compilerOptions.paths = {
4191
+ "@/*": ["./*"],
4192
+ "~/*": ["./app/*"]
4193
+ };
4194
+ const cloudflareTsconfigPath = join(directory, "tsconfig.cloudflare.json");
4195
+ const cloudflareTsconfig = looseObject({
4196
+ compilerOptions: record(string(), unknown()),
4197
+ include: array(string())
4198
+ }).parse(JSON.parse(await readFile(cloudflareTsconfigPath, "utf8")));
4199
+ cloudflareTsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths;
4200
+ cloudflareTsconfig.include.push("databases/**/schema.ts");
4201
+ await writeFile(cloudflareTsconfigPath, `${JSON.stringify(cloudflareTsconfig, null, 2)}\n`);
4139
4202
  await writeFile(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
4140
4203
  const ui = await execa("pnpm", [
4141
4204
  "dlx",
@@ -4172,7 +4235,8 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4172
4235
  const install = await execa("pnpm", [
4173
4236
  "add",
4174
4237
  `@usecontextlayer/sdk@${version}`,
4175
- "zod@^4.4.3"
4238
+ "zod@^4.4.3",
4239
+ "drizzle-orm@0.45.3"
4176
4240
  ], {
4177
4241
  cwd: directory,
4178
4242
  reject: false,
@@ -4180,6 +4244,21 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4180
4244
  });
4181
4245
  process.exitCode = install.exitCode;
4182
4246
  if (install.exitCode !== 0) return;
4247
+ const migrationTool = await execa("pnpm", [
4248
+ "add",
4249
+ "-D",
4250
+ "drizzle-kit@0.31.11"
4251
+ ], {
4252
+ cwd: directory,
4253
+ reject: false,
4254
+ stdio: "inherit"
4255
+ });
4256
+ process.exitCode = migrationTool.exitCode;
4257
+ if (migrationTool.exitCode !== 0) return;
4258
+ const packagePath = join(directory, "package.json");
4259
+ const projectPackage = looseObject({ scripts: record(string(), string()) }).parse(JSON.parse(await readFile(packagePath, "utf8")));
4260
+ projectPackage.scripts["db:migration"] = "drizzle-kit generate --custom";
4261
+ await writeFile(packagePath, `${JSON.stringify(projectPackage, null, 2)}\n`);
4183
4262
  const response = await fetch(new URL("/api/apps", env.CTX_PLATFORM_URL), {
4184
4263
  body: JSON.stringify(input),
4185
4264
  headers: {
@@ -4205,33 +4284,57 @@ program.command("init <directory>").description("Create a Cloudflare React Route
4205
4284
  stdio: "inherit"
4206
4285
  });
4207
4286
  process.exitCode = git.exitCode;
4287
+ if (git.exitCode !== 0) return;
4288
+ console.log(`
4289
+ ContextLayer app created: ${app.id}
4290
+ Dashboard: ${new URL(`/dashboard/apps/${app.id}`, env.CTX_PLATFORM_URL).href}
4291
+ Hosted URL: https://${app.public_hostname}/ (available after deployment)
4292
+
4293
+ Start locally:
4294
+ cd '${directory.replaceAll("'", "'\\''")}'
4295
+ pnpm dev
4296
+ Open the local URL printed by the development server.
4297
+
4298
+ Publish your changes:
4299
+ git add .
4300
+ git commit
4301
+ git push -u origin main
4302
+
4303
+ See README.md for the development and connection workflow.
4304
+ `);
4208
4305
  });
4209
4306
  program.command("login").description("Sign in by approving a browser code and configure Git credentials for ContextLayer repositories").action(async () => {
4210
- await login();
4307
+ await login(env);
4211
4308
  await setupGit(new URL(import.meta.url));
4212
4309
  console.log("Git credentials configured for ContextLayer.");
4213
4310
  });
4214
- program.command("git-credential <operation>").description("Git credential helper using the saved login; invoked automatically by Git").action(gitCredential);
4215
- program.command("whoami").description("Show your ContextLayer user ID using the saved login").action(whoami);
4216
- program.command("join").argument("<target>", "AppID, public hostname, or local (run from the app root)").description("Join a private app you can view using ctx login, enabling member-scoped unattended work; returns app and user IDs").action(async (target) => {
4311
+ program.command("git-credential <operation>").description("Git credential helper using the saved login; invoked automatically by Git").hook("preAction", async (command) => {
4312
+ if (command.args[0] === "get") await requireLogin(command);
4313
+ }).action(gitCredential);
4314
+ program.command("whoami").description("Show your ContextLayer user ID verified by the auth server").action(async (_options, command) => {
4315
+ console.log((await authenticate(command)).id);
4316
+ });
4317
+ program.command("join").argument("<target>", "AppID, public hostname, or local (run from the app root)").description("Join a private app you can view using ctx login, enabling member-scoped unattended work; returns app and user IDs").hook("preAction", requireLogin).action(async (target) => {
4217
4318
  await joinApp(union([
4218
4319
  literal("local"),
4219
4320
  uuid(),
4220
4321
  hostname()
4221
4322
  ]).parse(target));
4222
4323
  });
4223
- const connections = program.command("connections").description("Manage Owner-owned connected accounts or list app and agent connection requirements; local reads only the current folder and its saved development choices").option("--org <org_slug>", "Manage this organization's connections; defaults to the signed-in user").argument("[target]", "AppID or public hostname (pushed requirements), or local (current folder; no platform request)").addArgument(new Argument("[operation]", "App connection operation").choices(["list"])).action(async (target, operation) => {
4324
+ const connections = program.command("connections").description("Manage Owner-owned connected accounts or list app and agent connection requirements; local reads only the current folder and its saved development choices").option("--org <org_slug>", "Manage this organization's connections; defaults to the signed-in user").argument("[target]", "AppID or public hostname (pushed requirements), or local (current folder; no platform request)").addArgument(new Argument("[operation]", "App connection operation").choices(["list"])).hook("preAction", async (command, actionCommand) => {
4325
+ if (actionCommand !== command || command.args[0] !== "local") await requireLogin(actionCommand);
4326
+ }).action(async (target, operation) => {
4224
4327
  if (!target || !operation) connections.error("Expected: ctx connections <target> list");
4225
4328
  await listAppConnections(target);
4226
4329
  });
4227
4330
  connections.command("list").description("List the selected Owner's Composio accounts with their toolkit, identity, and status").action(async (_options, command) => {
4228
4331
  await listConnections(ownerOptionsSchema.parse(command.optsWithGlobals()).org);
4229
4332
  });
4230
- connections.command("add <toolkit>").description("Print a Composio authorization link for the selected Owner's account (for example, gmail)").action(async (toolkit, _options, command) => {
4333
+ connections.command("add <toolkit>").description("Authorize the selected Owner's account in your browser (for example, gmail), then assign it to an app's declared connection slug with ctx connection").action(async (toolkit, _options, command) => {
4231
4334
  const options = ownerOptionsSchema.parse(command.optsWithGlobals());
4232
4335
  await addConnection(string().min(1).parse(toolkit), options.org);
4233
4336
  });
4234
- program.command("connection").description("Bind a declared slug: local saves your personal account in .contextlayer/connections.json; AppID/hostname saves a hosted assignment under its declared Owner").argument("<target>", "AppID, public hostname, or local (run from the app root)").addArgument(new Argument("<operation>").choices(["assign"])).argument("<slug>", "Connection requirement slug").argument("[connection_id]", "Connected account ID; omit locally to choose your accounts for the declared toolkit; required for hosted targets").action(async (target, _operation, slug, connectionId) => {
4337
+ program.command("connection").description("Bind a declared slug: local saves your personal account in .contextlayer/connections.json; AppID/hostname saves a hosted assignment under its declared Owner").argument("<target>", "AppID, public hostname, or local (run from the app root)").addArgument(new Argument("<operation>").choices(["assign"])).argument("<slug>", "Connection slug declared in your Slate (for example, work-email)").argument("[connection_id]", "Connected account ID; omit locally to choose your accounts for the declared toolkit; required for hosted targets").hook("preAction", requireLogin).action(async (target, _operation, slug, connectionId) => {
4235
4338
  const input = connectionAssignmentSchema.parse({
4236
4339
  connection_id: connectionId,
4237
4340
  slug,
@@ -4249,7 +4352,7 @@ program.command("agent").addArgument(new Argument("<target>", "Run from the app
4249
4352
  });
4250
4353
  console.log(await runLocalAgent(input.name, input.prompt));
4251
4354
  });
4252
- const secrets = program.command("secrets").description("Save or drop Owner-owned shared secrets through the authenticated platform").option("--org <org_slug>", "Use this organization's Owner; defaults to the signed-in user");
4355
+ const secrets = program.command("secrets").description("Save or drop Owner-owned shared secrets through the authenticated platform").option("--org <org_slug>", "Use this organization's Owner; defaults to the signed-in user").hook("preAction", requireLogin);
4253
4356
  secrets.command("set").argument("<name>", "Secret name: uppercase letters, digits, and underscores; cannot start with a digit").description("Save a secret with a masked password prompt; returns only Owner and name").option("--stdin", "Read the exact UTF-8 value from stdin instead of prompting; preserves whitespace and newlines").action(async (name, _options, command) => {
4254
4357
  await setSecret(secretNameSchema.parse(name), secretOptionsSchema.parse(command.optsWithGlobals()));
4255
4358
  });