Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e36411a
feat(kiloclaw): add Linear CLI integration
evanjacobson Mar 23, 2026
573d2b8
style: format bootstrap test array
evanjacobson Mar 23, 2026
fb36afa
Merge branch 'main' of github.com:Kilo-Org/cloud into feature/kilocla…
evanjacobson Mar 25, 2026
24753c7
Use the official Linear logo as the icon. Add missing pieces of the i…
evanjacobson Mar 26, 2026
6104be8
Move Linear to end of tool catalog and use dedicated SVG icon
evanjacobson Mar 26, 2026
43a3aca
Linear API key validation correction, add redeploy trigger to save se…
evanjacobson Mar 26, 2026
1a4d5d4
Make Linear icon more visible
evanjacobson Mar 26, 2026
8c79231
Update Dockerfile to keep xz-utils until after linear install
evanjacobson Mar 26, 2026
a280237
Optimize the Linear bootstrap in TOOLS.md
evanjacobson Mar 26, 2026
f3c1b8c
Add more details to the TOOLS.md linear section
evanjacobson Mar 26, 2026
e916293
Change --status to --state
evanjacobson Mar 26, 2026
d7b3ae7
Format svg
evanjacobson Mar 26, 2026
3f7035a
Final pass at the TOOLS.md bootstrap
evanjacobson Mar 26, 2026
4a9670e
Merge branch 'main' of github.com:Kilo-Org/cloud into feature/kilocla…
evanjacobson Mar 26, 2026
ade1b4e
Fix API key regex and improve the short description for Linear
evanjacobson Mar 26, 2026
298b918
Clean up persisted Linear CLI credentials when API key is removed
evanjacobson Mar 26, 2026
eb2418b
Format bootstrap test file
evanjacobson Mar 26, 2026
b410d4d
Also remove ~/.linear.toml when LINEAR_API_KEY is unset
evanjacobson Mar 26, 2026
1e65e2f
Harden Linear credential cleanup error handling
evanjacobson Mar 26, 2026
dc6785b
Add tests for remaining Linear credential cleanup failure paths
evanjacobson Mar 26, 2026
b3fe47b
Format files
evanjacobson Mar 26, 2026
b466318
Add missing test
evanjacobson Mar 26, 2026
432f2fd
chore(kiloclaw): remove Linear CLI from Dockerfiles
evanjacobson Mar 26, 2026
3878f20
refactor(kiloclaw): replace Linear CLI with Linear MCP in bootstrap
evanjacobson Mar 26, 2026
edac005
feat(kiloclaw): add Linear MCP server to mcporter config
evanjacobson Mar 26, 2026
b9f6bbf
Small tweak to Linear bootstrap
evanjacobson Mar 26, 2026
5ee5389
Final tweak to Linear's bootstrap content
evanjacobson Mar 27, 2026
c1f3cee
Flip baseUrl --> url
evanjacobson Mar 27, 2026
d33284e
Flip baseUrl to url in test as well
evanjacobson Mar 27, 2026
e9491a6
refactor(kiloclaw): replace Linear CLI with Linear MCP server (#1635)
evanjacobson Mar 27, 2026
458dc89
Fix Linear heading in test
evanjacobson Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions kiloclaw/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ RUN apt-get update \
| gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh 1password-cli \
&& apt-get purge -y xz-utils \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& node --version \
&& npm --version
Expand All @@ -57,6 +55,13 @@ RUN npm install -g @steipete/summarize@0.12.0
# Install Kilo CLI (agentic coding assistant for the terminal)
RUN npm install -g @kilocode/cli@7.0.46

# Install Linear CLI (issue tracker)
RUN npm install -g @schpet/linear-cli@1.11.1
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are security vulnerabilities on a dependency of this package. I have posted a PR to fix them.

I will bump the version to the latest release after it merges.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are security vulnerabilities on a dependency of this package. I have posted a PR to fix them.

I will bump the version to the latest release after it merges.

I am still discussing the changes with the repo owner. This may need to be a follow-up. The vulnerabilities are not exposed in linear-cli regardless.


# Clean up xz-utils now that Node.js and linear-cli are installed
RUN apt-get purge -y xz-utils \
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linear-cli requires node for install

&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

# Install Go (available at runtime for users to `go install` additional tools)
ENV GO_VERSION=1.26.0
Expand Down
10 changes: 8 additions & 2 deletions kiloclaw/Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ RUN apt-get update \
| gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh 1password-cli=2.32.1-1 \
&& apt-get purge -y xz-utils \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& node --version \
&& npm --version
Expand All @@ -58,6 +56,14 @@ RUN npm install -g mcporter@0.7.3
# Install summarize (web page summarization CLI)
RUN npm install -g @steipete/summarize@0.11.1

# Install Linear CLI (issue tracker)
RUN npm install -g @schpet/linear-cli@1.11.1
Comment thread
evanjacobson marked this conversation as resolved.
Outdated

# Clean up xz-utils now that Node.js and linear-cli are installed
RUN apt-get purge -y xz-utils \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

# Install Go (available at runtime for users to `go install` additional tools)
ENV GO_VERSION=1.26.0
RUN ARCH="$(dpkg --print-architecture)" \
Expand Down
132 changes: 131 additions & 1 deletion kiloclaw/controller/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
applyFeatureFlags,
generateHooksToken,
configureGitHub,
configureLinear,
updateToolsMdLinearSection,
runOnboardOrDoctor,
updateToolsMdKiloCliSection,
updateToolsMd1PasswordSection,
Expand Down Expand Up @@ -487,6 +489,54 @@ describe('configureGitHub', () => {
});
});

// ---- configureLinear ----

describe('configureLinear', () => {
it('logs configured when LINEAR_API_KEY is set', () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { deps } = fakeDeps();
const env: Record<string, string | undefined> = {
LINEAR_API_KEY: 'lin_api_test123',
};

configureLinear(env, deps);

expect(env.LINEAR_API_KEY).toBe('lin_api_test123');
expect(logSpy).toHaveBeenCalledWith('Linear CLI configured via LINEAR_API_KEY');
logSpy.mockRestore();
});

it('removes persisted config directory when no LINEAR_API_KEY', () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { deps, execCalls } = fakeDeps();
const env: Record<string, string | undefined> = {};

configureLinear(env, deps);

expect(env.LINEAR_API_KEY).toBeUndefined();
expect(execCalls).toContainEqual({
cmd: 'rm',
args: ['-rf', '/root/.config/linear'],
input: undefined,
});
expect(logSpy).toHaveBeenCalledWith('Linear: not configured (credentials cleared)');
logSpy.mockRestore();
});

it('cleans up empty LINEAR_API_KEY', () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { deps } = fakeDeps();
const env: Record<string, string | undefined> = {
LINEAR_API_KEY: '',
};

configureLinear(env, deps);

expect(env.LINEAR_API_KEY).toBeUndefined();
logSpy.mockRestore();
});
});

// ---- runOnboardOrDoctor ----

describe('runOnboardOrDoctor', () => {
Expand Down Expand Up @@ -684,6 +734,79 @@ describe('updateToolsMd1PasswordSection', () => {
});
});

// ---- updateToolsMdLinearSection ----

describe('updateToolsMdLinearSection', () => {
it('adds Linear section when LINEAR_API_KEY is set', () => {
const harness = fakeDeps();
(harness.deps.readFileSync as ReturnType<typeof vi.fn>).mockReturnValue('# TOOLS\n');

const env: Record<string, string | undefined> = {
LINEAR_API_KEY: 'lin_api_test123',
};

updateToolsMdLinearSection(env, harness.deps);

expect(harness.writeCalls).toHaveLength(1);
expect(harness.writeCalls[0]!.data).toContain('<!-- BEGIN:linear -->');
expect(harness.writeCalls[0]!.data).toContain('linear issue list');
expect(harness.writeCalls[0]!.data).toContain('<!-- END:linear -->');
});

it('skips adding when section already present', () => {
const harness = fakeDeps();
(harness.deps.readFileSync as ReturnType<typeof vi.fn>).mockReturnValue(
'# TOOLS\n<!-- BEGIN:linear -->\nexisting\n<!-- END:linear -->'
);

const env: Record<string, string | undefined> = {
LINEAR_API_KEY: 'lin_api_test123',
};

updateToolsMdLinearSection(env, harness.deps);

expect(harness.writeCalls).toHaveLength(0);
});

it('removes stale section when key is absent', () => {
const harness = fakeDeps();
(harness.deps.readFileSync as ReturnType<typeof vi.fn>).mockReturnValue(
'# TOOLS\n<!-- BEGIN:linear -->\nold section\n<!-- END:linear -->\n'
);

const env: Record<string, string | undefined> = {};

updateToolsMdLinearSection(env, harness.deps);

expect(harness.writeCalls).toHaveLength(1);
expect(harness.writeCalls[0]!.data).not.toContain('<!-- BEGIN:linear -->');
});

it('no-ops when TOOLS.md does not exist', () => {
const harness = fakeDeps();
(harness.deps.existsSync as ReturnType<typeof vi.fn>).mockReturnValue(false);

const env: Record<string, string | undefined> = {
LINEAR_API_KEY: 'lin_api_test123',
};

updateToolsMdLinearSection(env, harness.deps);

expect(harness.writeCalls).toHaveLength(0);
});

it('no-ops when key absent and no stale section exists', () => {
const harness = fakeDeps();
(harness.deps.readFileSync as ReturnType<typeof vi.fn>).mockReturnValue('# TOOLS\n');

const env: Record<string, string | undefined> = {};

updateToolsMdLinearSection(env, harness.deps);

expect(harness.writeCalls).toHaveLength(0);
});
});

// ---- buildGatewayArgs ----

describe('buildGatewayArgs', () => {
Expand Down Expand Up @@ -743,7 +866,14 @@ describe('bootstrap', () => {

await bootstrap(env, phase => phases.push(phase), harness.deps);

expect(phases).toEqual(['decrypting', 'directories', 'feature-flags', 'github', 'onboard']);
expect(phases).toEqual([
'decrypting',
'directories',
'feature-flags',
'github',
'linear',
'onboard',
]);
});

it('reports doctor phase when config exists', async () => {
Expand Down
128 changes: 125 additions & 3 deletions kiloclaw/controller/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,39 @@ export function configureGitHub(env: EnvLike, deps: BootstrapDeps = defaultDeps)
}
}

// ---- Step 6: Onboard / doctor + config patching ----
// ---- Step 6: Linear config ----

/**
* Configure or clean up Linear CLI access.
* When LINEAR_API_KEY is present the CLI reads it natively.
* When absent, clean up any on-disk credentials left by a previous
* `linear auth login --plaintext` on the persistent volume.
* Best-effort: logs warnings on failure, does not throw.
*
* The CLI stores two files under ~/.config/linear/:
* - credentials.toml — workspace list + inline API keys (plaintext mode)
* - linear.toml — global config that can also carry an api_key field
* The system keyring is not available in this container (no libsecret-tools),
* so these files are the only persistence locations.
*/
export function configureLinear(env: EnvLike, deps: BootstrapDeps = defaultDeps): void {
if (env.LINEAR_API_KEY) {
console.log('Linear CLI configured via LINEAR_API_KEY');
} else {
// Clean up env var if explicitly set to empty
delete env.LINEAR_API_KEY;
Comment thread
evanjacobson marked this conversation as resolved.
// Remove any previously stored credentials from the persistent volume.
// The CLI recreates ~/.config/linear/ via ensureDir on next auth login.
try {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separately trying each file delete operation out of an abundance of caution — if one fails, the other one is still tried.

deps.execFileSync('rm', ['-rf', '/root/.config/linear'], { stdio: 'pipe' });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} catch {
// ignore — directory may not exist
}
console.log('Linear: not configured (credentials cleared)');
}
}

// ---- Step 7: Onboard / doctor + config patching ----

/**
* Run openclaw onboard (first boot) or openclaw doctor (subsequent boots),
Expand Down Expand Up @@ -386,7 +418,7 @@ export function runOnboardOrDoctor(env: EnvLike, deps: BootstrapDeps = defaultDe
}
}

// ---- Step 7: TOOLS.md Google Workspace section ----
// ---- Step 8: TOOLS.md Google Workspace section ----

const GOG_MARKER_BEGIN = '<!-- BEGIN:google-workspace -->';
const GOG_MARKER_END = '<!-- END:google-workspace -->';
Expand Down Expand Up @@ -539,7 +571,92 @@ export function updateToolsMd1PasswordSection(env: EnvLike, deps: BootstrapDeps)
}
}

// ---- Step 10: Gateway args ----
// ---- Step 10: TOOLS.md Linear section ----

const LINEAR_MARKER_BEGIN = '<!-- BEGIN:linear -->';
const LINEAR_MARKER_END = '<!-- END:linear -->';

const LINEAR_TOOLS_SECTION = `
${LINEAR_MARKER_BEGIN}
## Linear

The \`linear\` CLI is configured with your Linear API key. Use it to read and manage issues.

- Run \`linear --help\` for full command reference; \`--help\` after any subcommand for details.
- If you don't know the team key, run \`linear team list\`.

### Listing issues

Example — list all issues by priority:
\`\`\`
linear issue list --team <key> --sort priority --all-states --all-assignees
\`\`\`

**Flags that silently filter results when omitted:**
- \`--state\` defaults to \`backlog\`. Use \`--all-states\` for all, or \`--state <value>\` to filter to one: triage, backlog, unstarted, started, completed, canceled
- \`--assignee\` defaults to \`me\`. Use \`--all-assignees\` for all, or \`--assignee <user>\` to filter to one

### Writing issue content
Use file flags for markdown with newlines or special characters:
- \`--description-file <path>\` for \`issue create/update\`
- \`--body-file <path>\` for \`comment add/update\`

### Config file
Avoid repeated \`--team\` and \`--sort\` flags with \`.linear.toml\` in the project directory:
\`\`\`toml
team = "TEAM_KEY"
sort = "priority"
\`\`\`

### Gotchas
- \`--no-pager\` only works on \`issue list\` — errors on other commands
- GraphQL non-null types (\`String!\`) require heredoc: \`linear api --variable key=val <<'GRAPHQL'\`

### Advanced
- Get API token: \`linear auth token\`
- Direct GraphQL: \`curl -s -X POST https://api.linear.app/graphql -H "Authorization: $(linear auth token)" -d '{"query":"..."}'\`
${LINEAR_MARKER_END}`;

/**
* Manage the Linear section in TOOLS.md.
*
* When LINEAR_API_KEY is present, append a bounded section so the agent
* knows the linear CLI is available. When absent, remove any stale section.
* Idempotent: skips if the marker is already present.
*/
export function updateToolsMdLinearSection(env: EnvLike, deps: BootstrapDeps): void {
if (!deps.existsSync(TOOLS_MD_DEST)) return;

const content = deps.readFileSync(TOOLS_MD_DEST, 'utf8');

if (env.LINEAR_API_KEY) {
// Linear configured — add section if not already present
if (!content.includes(LINEAR_MARKER_BEGIN)) {
deps.writeFileSync(TOOLS_MD_DEST, content + LINEAR_TOOLS_SECTION);
console.log('TOOLS.md: added Linear section');
} else {
console.log('TOOLS.md: Linear section already present');
}
} else {
// Linear not configured — remove stale section if present
if (content.includes(LINEAR_MARKER_BEGIN)) {
const beginIdx = content.indexOf(LINEAR_MARKER_BEGIN);
const endIdx = content.indexOf(LINEAR_MARKER_END);
if (beginIdx !== -1 && endIdx !== -1) {
const before = content.slice(0, beginIdx).replace(/\n+$/, '\n');
const after = content.slice(endIdx + LINEAR_MARKER_END.length).replace(/^\n+/, '');
deps.writeFileSync(TOOLS_MD_DEST, before + after);
console.log('TOOLS.md: removed stale Linear section');
} else {
console.warn(
'TOOLS.md: Linear BEGIN marker found but END marker missing, skipping removal'
);
}
}
}
}

// ---- Step 11: Gateway args ----

/**
* Build the gateway CLI arguments array.
Expand Down Expand Up @@ -588,6 +705,10 @@ export async function bootstrap(
configureGitHub(env, deps);
await yieldToEventLoop();

setPhase('linear');
configureLinear(env, deps);
await yieldToEventLoop();

const configExists = deps.existsSync(CONFIG_PATH);
setPhase(configExists ? 'doctor' : 'onboard');
runOnboardOrDoctor(env, deps);
Expand All @@ -596,6 +717,7 @@ export async function bootstrap(
updateToolsMdKiloCliSection(env, deps);
updateToolsMdGoogleSection(env, deps);
updateToolsMd1PasswordSection(env, deps);
updateToolsMdLinearSection(env, deps);

// Write mcporter config for MCP servers (AgentCard, etc.)
writeMcporterConfig(env);
Expand Down
Loading
Loading