@clipboard-health/groundcrew 2.0.0 → 2.1.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
@@ -49,18 +49,20 @@ This installs the `crew` binary. `@clipboard-health/clearance` is pulled in tran
49
49
 
50
50
  `crew` resolves the config path as: `GROUNDCREW_CONFIG` if set → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/config.ts` if it exists → a `config.ts` sitting next to `crew`'s own source files (only useful from a local checkout; see [Hacking on groundcrew](#hacking-on-groundcrew)). Set `GROUNDCREW_CONFIG` only when you want to override the XDG location.
51
51
 
52
- 4. **Provide a Linear API key.** `crew` expects `LINEAR_API_KEY` in its environment. Any mechanism works — shell export, [direnv](https://direnv.net/), a `.env` file you `source`, or piping through `op run` if you store the credential in 1Password:
52
+ 4. **Provide a Linear API key.** `crew` reads the key from `GROUNDCREW_LINEAR_API_KEY` first, then falls back to `LINEAR_API_KEY`. Prefer `GROUNDCREW_LINEAR_API_KEY` so the value does not clash with other tools that consume `LINEAR_API_KEY`. Any mechanism works — shell export, [direnv](https://direnv.net/), a `.env` file you `source`, or piping through `op run` if you store the credential in 1Password:
53
53
 
54
54
  ```bash
55
55
  # Direct
56
- export LINEAR_API_KEY="lin_api_..."
56
+ export GROUNDCREW_LINEAR_API_KEY="lin_api_..."
57
57
  crew doctor
58
58
 
59
59
  # Via 1Password CLI (`op`), if you keep the key in a vault
60
- echo "LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
60
+ echo "GROUNDCREW_LINEAR_API_KEY='op://<vault>/LINEAR_API_KEY/credential'" > .env.1password
61
61
  op run --env-file .env.1password -- crew doctor
62
62
  ```
63
63
 
64
+ `LINEAR_API_KEY` continues to work for existing setups; if both variables are set, `GROUNDCREW_LINEAR_API_KEY` wins.
65
+
64
66
  5. **Prepare the runner and agent auth.** Groundcrew supports one runner: a `cmux` or `tmux` workspace on macOS, with Safehouse on `PATH`, `clearance`, and locally authenticated agent CLIs.
65
67
 
66
68
  Setup fails before creating a worktree when the host is not macOS or `safehouse` is missing. `models.isolation`, per-model `isolation`, and per-model `sandbox` are legacy keys and now fail config validation.
@@ -185,7 +187,7 @@ For developers working on the package itself, clone this repo, run `npm install`
185
187
  cd ~/dev/c/groundcrew
186
188
  node --run crew -- doctor
187
189
 
188
- # With 1Password for LINEAR_API_KEY:
190
+ # With 1Password for GROUNDCREW_LINEAR_API_KEY:
189
191
  node --run crew:op -- run --watch
190
192
  ```
191
193
 
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiIH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA6D/C"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwIH,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CA6D/C"}
@@ -5,7 +5,7 @@
5
5
  import { existsSync, statSync } from "node:fs";
6
6
  import { loadConfig } from "../lib/config.js";
7
7
  import { detectHostCapabilities, which } from "../lib/host.js";
8
- import { errorMessage, readEnvironmentVariable, writeOutput } from "../lib/util.js";
8
+ import { errorMessage, resolveLinearApiKey, writeOutput } from "../lib/util.js";
9
9
  import { resolveWorkspaceKind } from "../lib/workspaces.js";
10
10
  // Tokenization stops after this many non-flag tokens. Two is enough to
11
11
  // catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
@@ -23,14 +23,21 @@ async function checkCmd(cmd, required, hint) {
23
23
  }
24
24
  return result;
25
25
  }
26
- function checkEnvironment(name) {
27
- const value = readEnvironmentVariable(name);
28
- const set = value !== undefined && value.length > 0;
26
+ function checkLinearApiKey() {
27
+ const resolved = resolveLinearApiKey();
28
+ if (resolved !== undefined) {
29
+ return {
30
+ name: "linear api key",
31
+ ok: true,
32
+ required: true,
33
+ hint: `set via $${resolved.source}`,
34
+ };
35
+ }
29
36
  return {
30
- name: `$${name}`,
31
- ok: set,
37
+ name: "linear api key",
38
+ ok: false,
32
39
  required: true,
33
- hint: set ? "set" : "export the variable in your shell",
40
+ hint: "export $GROUNDCREW_LINEAR_API_KEY or $LINEAR_API_KEY",
34
41
  };
35
42
  }
36
43
  function checkDir(path, label) {
@@ -138,7 +145,7 @@ export async function doctor() {
138
145
  const workspaceOutcome = resolveWorkspaceOutcome(config, host);
139
146
  reportWorkspaceKind(config, workspaceOutcome);
140
147
  const checks = [
141
- checkEnvironment("LINEAR_API_KEY"),
148
+ checkLinearApiKey(),
142
149
  await checkCmd("git", true, "https://git-scm.com/"),
143
150
  ...(await workspaceChecks(workspaceOutcome)),
144
151
  checkDir(config.workspace.projectDir, "workspace.projectDir"),
@@ -41,7 +41,7 @@ async function verifyProject(client, config) {
41
41
  const { projects } = response.data;
42
42
  const [project] = projects.nodes;
43
43
  if (!project) {
44
- throw new Error(`No Linear project found with slugId "${config.linear.slugId}" (linear.projectSlug = "${config.linear.projectSlug}"). Confirm the slug matches the trailing segment of your project's URL and that LINEAR_API_KEY can access this workspace.`);
44
+ throw new Error(`No Linear project found with slugId "${config.linear.slugId}" (linear.projectSlug = "${config.linear.projectSlug}"). Confirm the slug matches the trailing segment of your project's URL and that your Linear API key can access this workspace.`);
45
45
  }
46
46
  log(`Resolved Linear project: ${project.name} (slugId ${project.slugId})`);
47
47
  }
@@ -8,6 +8,13 @@ export declare function log(message: string): void;
8
8
  type LogEventFieldValue = boolean | number | string | readonly string[] | undefined;
9
9
  export declare function logEvent(event: string, fields: Record<string, LogEventFieldValue>): void;
10
10
  export declare function readEnvironmentVariable(name: string): string | undefined;
11
+ declare const LINEAR_API_KEY_SOURCES: readonly ["GROUNDCREW_LINEAR_API_KEY", "LINEAR_API_KEY"];
12
+ export type LinearApiKeySource = (typeof LINEAR_API_KEY_SOURCES)[number];
13
+ export interface ResolvedLinearApiKey {
14
+ value: string;
15
+ source: LinearApiKeySource;
16
+ }
17
+ export declare function resolveLinearApiKey(): ResolvedLinearApiKey | undefined;
11
18
  export declare function getLinearClient(): LinearClient;
12
19
  export declare function errorMessage(error: unknown): string;
13
20
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAED,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAWxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,wBAAgB,eAAe,IAAI,YAAY,CAM9C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/lib/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAIlD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAED,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAkBD,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKzC;AAED,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;AAUpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAWxF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,QAAA,MAAM,sBAAsB,YAAI,2BAA2B,EAAE,gBAAgB,CAAU,CAAC;AAExF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,wBAAgB,mBAAmB,IAAI,oBAAoB,GAAG,SAAS,CAQtE;AAED,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcnD"}
package/dist/lib/util.js CHANGED
@@ -87,12 +87,22 @@ export function readEnvironmentVariable(name) {
87
87
  // oxlint-disable-next-line node/no-process-env -- Centralized environment accessor.
88
88
  return process.env[name];
89
89
  }
90
+ const LINEAR_API_KEY_SOURCES = ["GROUNDCREW_LINEAR_API_KEY", "LINEAR_API_KEY"];
91
+ export function resolveLinearApiKey() {
92
+ for (const source of LINEAR_API_KEY_SOURCES) {
93
+ const value = readEnvironmentVariable(source);
94
+ if (value !== undefined && value.length > 0) {
95
+ return { value, source };
96
+ }
97
+ }
98
+ return undefined;
99
+ }
90
100
  export function getLinearClient() {
91
- const apiKey = readEnvironmentVariable("LINEAR_API_KEY");
92
- if (apiKey === undefined || apiKey.length === 0) {
93
- throw new Error("LINEAR_API_KEY not set. Add it to your environment.");
101
+ const resolved = resolveLinearApiKey();
102
+ if (resolved === undefined) {
103
+ throw new Error("Linear API key not set. Set GROUNDCREW_LINEAR_API_KEY or LINEAR_API_KEY in your environment.");
94
104
  }
95
- return new LinearClient({ apiKey });
105
+ return new LinearClient({ apiKey: resolved.value });
96
106
  }
97
107
  export function errorMessage(error) {
98
108
  if (error instanceof Error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",