Skip to content

Commit 94d651d

Browse files
rdrussTabishB
andauthored
feat: add support for IBM Bob coding assistant (#886)
* test: add comprehensive tests for Bob Shell adapter - Add 7 tests covering toolId, file paths, formatting, and edge cases - Include Bob Shell adapter in cross-platform path handling tests - All 89 adapter tests now passing - Ensures Bob Shell adapter works correctly with all 11 workflows * feat: add Bob Shell adapter support - Implement Bob Shell command adapter with YAML frontmatter - Register adapter in CommandAdapterRegistry - Export adapter from adapters/index.ts - Add Bob Shell to AI_TOOLS configuration - Generates commands in .bob/commands/opsx-<id>.md format - Supports all 11 workflows via custom profile system * docs: add Bob Shell to supported tools documentation - Add Bob Shell to README.md supported tools list - Update docs/supported-tools.md with Bob Shell entry - Document .bob/commands/opsx-<id>.md command path pattern - Note that Bob Shell uses commands, not Agent Skills spec * docs: add Bob Shell support proposal and design documentation - Add comprehensive proposal for Bob Shell integration - Document command structure and file format - Include implementation plan and success criteria - Preserve change documentation for future reference * chore: update dependencies and gitignore - Update package-lock.json with latest dependencies - Add .bob/ to gitignore (test output directory) * chore: update gitignore to exclude .bob directory * openspec change not needed for project, should be kept local * Update reference from Bob Shell to IBM Bob Shell * fix: transform command references and add argument-hint for Bob adapter Bob derives command names from filenames (opsx-apply.md → /opsx-apply), so body text referencing /opsx:apply is incorrect. Apply the same transformToHyphenCommands rewrite that opencode.ts uses. Also add argument-hint frontmatter to match peer adapters (auggie, codebuddy, etc). --------- Co-authored-by: TabishB <tabishbidiwale@gmail.com> Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
1 parent 040e382 commit 94d651d

9 files changed

Lines changed: 141 additions & 5 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,6 @@ opencode.json
156156

157157
# Codex
158158
.codex/
159+
160+
# Bob
161+
.bob/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Now tell your AI: `/opsx:propose <what-you-want-to-build>`
103103
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
104104

105105
> [!NOTE]
106-
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 20+ tools and growing.
106+
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
107107
>
108108
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
109109

docs/supported-tools.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `b
2424
| Amazon Q Developer (`amazon-q`) | `.amazonq/skills/openspec-*/SKILL.md` | `.amazonq/prompts/opsx-<id>.md` |
2525
| Antigravity (`antigravity`) | `.agent/skills/openspec-*/SKILL.md` | `.agent/workflows/opsx-<id>.md` |
2626
| Auggie (`auggie`) | `.augment/skills/openspec-*/SKILL.md` | `.augment/commands/opsx-<id>.md` |
27+
| IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-<id>.md` |
2728
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
2829
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
2930
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
@@ -70,7 +71,7 @@ openspec init --tools none
7071
openspec init --profile core
7172
```
7273

73-
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `forgecode`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
74+
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `forgecode`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
7475

7576
## Workflow-Dependent Installation
7677

package-lock.json

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Bob Shell Command Adapter
3+
*
4+
* Formats commands for Bob Shell following its markdown specification.
5+
* Commands are stored in .bob/commands/ directory.
6+
*/
7+
8+
import path from 'path';
9+
import type { CommandContent, ToolCommandAdapter } from '../types.js';
10+
import { transformToHyphenCommands } from '../../../utils/command-references.js';
11+
12+
/**
13+
* Escapes a string value for safe YAML output.
14+
* Quotes the string if it contains special YAML characters.
15+
*/
16+
function escapeYamlValue(value: string): string {
17+
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
18+
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
19+
if (needsQuoting) {
20+
// Use double quotes and escape internal double quotes and backslashes
21+
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
22+
return `"${escaped}"`;
23+
}
24+
return value;
25+
}
26+
27+
/**
28+
* Bob Shell adapter for command generation.
29+
* File path: .bob/commands/opsx-<id>.md
30+
* Frontmatter: description, argument-hint
31+
*/
32+
export const bobAdapter: ToolCommandAdapter = {
33+
toolId: 'bob',
34+
35+
getFilePath(commandId: string): string {
36+
return path.join('.bob', 'commands', `opsx-${commandId}.md`);
37+
},
38+
39+
formatFile(content: CommandContent): string {
40+
// Transform command references from colon to hyphen format for Bob
41+
const transformedBody = transformToHyphenCommands(content.body);
42+
43+
return `---
44+
description: ${escapeYamlValue(content.description)}
45+
argument-hint: command arguments
46+
---
47+
48+
${transformedBody}
49+
`;
50+
},
51+
};

src/core/command-generation/adapters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
export { amazonQAdapter } from './amazon-q.js';
88
export { antigravityAdapter } from './antigravity.js';
99
export { auggieAdapter } from './auggie.js';
10+
export { bobAdapter } from './bob.js';
1011
export { claudeAdapter } from './claude.js';
1112
export { clineAdapter } from './cline.js';
1213
export { codexAdapter } from './codex.js';

src/core/command-generation/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ToolCommandAdapter } from './types.js';
99
import { amazonQAdapter } from './adapters/amazon-q.js';
1010
import { antigravityAdapter } from './adapters/antigravity.js';
1111
import { auggieAdapter } from './adapters/auggie.js';
12+
import { bobAdapter } from './adapters/bob.js';
1213
import { claudeAdapter } from './adapters/claude.js';
1314
import { clineAdapter } from './adapters/cline.js';
1415
import { codexAdapter } from './adapters/codex.js';
@@ -43,6 +44,7 @@ export class CommandAdapterRegistry {
4344
CommandAdapterRegistry.register(amazonQAdapter);
4445
CommandAdapterRegistry.register(antigravityAdapter);
4546
CommandAdapterRegistry.register(auggieAdapter);
47+
CommandAdapterRegistry.register(bobAdapter);
4648
CommandAdapterRegistry.register(claudeAdapter);
4749
CommandAdapterRegistry.register(clineAdapter);
4850
CommandAdapterRegistry.register(codexAdapter);

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const AI_TOOLS: AIToolOption[] = [
2222
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' },
2323
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' },
2424
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
25+
{ name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
2526
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
2627
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
2728
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },

test/core/command-generation/adapters.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'path';
44
import { amazonQAdapter } from '../../../src/core/command-generation/adapters/amazon-q.js';
55
import { antigravityAdapter } from '../../../src/core/command-generation/adapters/antigravity.js';
66
import { auggieAdapter } from '../../../src/core/command-generation/adapters/auggie.js';
7+
import { bobAdapter } from '../../../src/core/command-generation/adapters/bob.js';
78
import { claudeAdapter } from '../../../src/core/command-generation/adapters/claude.js';
89
import { clineAdapter } from '../../../src/core/command-generation/adapters/cline.js';
910
import { codexAdapter } from '../../../src/core/command-generation/adapters/codex.js';
@@ -183,6 +184,71 @@ describe('command-generation/adapters', () => {
183184
});
184185
});
185186

187+
188+
describe('bobAdapter', () => {
189+
it('should have correct toolId', () => {
190+
expect(bobAdapter.toolId).toBe('bob');
191+
});
192+
193+
it('should generate correct file path', () => {
194+
const filePath = bobAdapter.getFilePath('explore');
195+
expect(filePath).toBe(path.join('.bob', 'commands', 'opsx-explore.md'));
196+
});
197+
198+
it('should generate correct file paths for different commands', () => {
199+
expect(bobAdapter.getFilePath('new')).toBe(path.join('.bob', 'commands', 'opsx-new.md'));
200+
expect(bobAdapter.getFilePath('bulk-archive')).toBe(path.join('.bob', 'commands', 'opsx-bulk-archive.md'));
201+
});
202+
203+
it('should format file with description and argument-hint frontmatter', () => {
204+
const output = bobAdapter.formatFile(sampleContent);
205+
expect(output).toContain('---\n');
206+
expect(output).toContain('description: Enter explore mode for thinking');
207+
expect(output).toContain('argument-hint: command arguments');
208+
expect(output).toContain('---\n\n');
209+
expect(output).toContain('This is the command body.\n\nWith multiple lines.');
210+
});
211+
212+
it('should transform colon command references to hyphen format', () => {
213+
const contentWithRefs: CommandContent = {
214+
...sampleContent,
215+
body: 'Run /opsx:apply to implement. Then use /opsx:verify.',
216+
};
217+
const output = bobAdapter.formatFile(contentWithRefs);
218+
expect(output).toContain('/opsx-apply');
219+
expect(output).toContain('/opsx-verify');
220+
expect(output).not.toContain('/opsx:apply');
221+
expect(output).not.toContain('/opsx:verify');
222+
});
223+
224+
it('should escape YAML special characters in description', () => {
225+
const contentWithSpecialChars: CommandContent = {
226+
...sampleContent,
227+
description: 'Fix: regression in "auth" feature',
228+
};
229+
const output = bobAdapter.formatFile(contentWithSpecialChars);
230+
expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"');
231+
});
232+
233+
it('should escape newlines in description', () => {
234+
const contentWithNewline: CommandContent = {
235+
...sampleContent,
236+
description: 'Line 1\nLine 2',
237+
};
238+
const output = bobAdapter.formatFile(contentWithNewline);
239+
expect(output).toContain('description: "Line 1\\nLine 2"');
240+
});
241+
242+
it('should handle empty description', () => {
243+
const contentEmptyDesc: CommandContent = {
244+
...sampleContent,
245+
description: '',
246+
};
247+
const output = bobAdapter.formatFile(contentEmptyDesc);
248+
expect(output).toContain('description: \n');
249+
});
250+
});
251+
186252
describe('clineAdapter', () => {
187253
it('should have correct toolId', () => {
188254
expect(clineAdapter.toolId).toBe('cline');
@@ -628,7 +694,7 @@ describe('command-generation/adapters', () => {
628694
it('All adapters use path.join for paths', () => {
629695
// Verify all adapters produce valid paths
630696
const adapters = [
631-
amazonQAdapter, antigravityAdapter, auggieAdapter, clineAdapter,
697+
amazonQAdapter, antigravityAdapter, auggieAdapter, bobAdapter, clineAdapter,
632698
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
633699
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
634700
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,

0 commit comments

Comments
 (0)