@ruvnet/open-claude-code 2.0.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 (63) hide show
  1. package/package.json +27 -0
  2. package/src/agents/loader.mjs +89 -0
  3. package/src/agents/parser.mjs +98 -0
  4. package/src/agents/teams.mjs +123 -0
  5. package/src/auth/oauth.mjs +220 -0
  6. package/src/config/cli-args.mjs +139 -0
  7. package/src/config/env.mjs +261 -0
  8. package/src/config/settings.mjs +119 -0
  9. package/src/core/agent-loop.mjs +462 -0
  10. package/src/core/cache.mjs +105 -0
  11. package/src/core/checkpoints.mjs +141 -0
  12. package/src/core/context-manager.mjs +198 -0
  13. package/src/core/providers.mjs +219 -0
  14. package/src/core/rate-limiter.mjs +119 -0
  15. package/src/core/scheduler.mjs +173 -0
  16. package/src/core/session.mjs +143 -0
  17. package/src/core/streaming.mjs +182 -0
  18. package/src/core/system-prompt.mjs +135 -0
  19. package/src/hooks/engine.mjs +162 -0
  20. package/src/index.mjs +220 -0
  21. package/src/mcp/client.mjs +253 -0
  22. package/src/mcp/transport-shttp.mjs +130 -0
  23. package/src/mcp/transport-sse.mjs +131 -0
  24. package/src/mcp/transport-ws.mjs +134 -0
  25. package/src/permissions/checker.mjs +57 -0
  26. package/src/permissions/injection-check.mjs +60 -0
  27. package/src/permissions/path-check.mjs +102 -0
  28. package/src/permissions/prompt.mjs +73 -0
  29. package/src/permissions/sandbox.mjs +112 -0
  30. package/src/plugins/loader.mjs +138 -0
  31. package/src/skills/loader.mjs +147 -0
  32. package/src/skills/runner.mjs +55 -0
  33. package/src/telemetry/index.mjs +96 -0
  34. package/src/tools/agent.mjs +127 -0
  35. package/src/tools/ask-user.mjs +61 -0
  36. package/src/tools/bash.mjs +148 -0
  37. package/src/tools/cron-create.mjs +120 -0
  38. package/src/tools/cron-delete.mjs +49 -0
  39. package/src/tools/cron-list.mjs +37 -0
  40. package/src/tools/edit.mjs +82 -0
  41. package/src/tools/enter-worktree.mjs +69 -0
  42. package/src/tools/exit-worktree.mjs +57 -0
  43. package/src/tools/glob.mjs +117 -0
  44. package/src/tools/grep.mjs +127 -0
  45. package/src/tools/ls.mjs +58 -0
  46. package/src/tools/lsp.mjs +115 -0
  47. package/src/tools/multi-edit.mjs +94 -0
  48. package/src/tools/notebook-edit.mjs +96 -0
  49. package/src/tools/read-mcp-resource.mjs +57 -0
  50. package/src/tools/read.mjs +138 -0
  51. package/src/tools/registry.mjs +114 -0
  52. package/src/tools/remote-trigger.mjs +84 -0
  53. package/src/tools/send-message.mjs +64 -0
  54. package/src/tools/skill.mjs +52 -0
  55. package/src/tools/todo-write.mjs +68 -0
  56. package/src/tools/tool-search.mjs +77 -0
  57. package/src/tools/web-fetch.mjs +65 -0
  58. package/src/tools/web-search.mjs +89 -0
  59. package/src/tools/write.mjs +55 -0
  60. package/src/ui/commands.mjs +499 -0
  61. package/src/ui/ink-app.mjs +231 -0
  62. package/src/ui/repl.mjs +144 -0
  63. package/test/test.mjs +1857 -0
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ruvnet/open-claude-code",
3
+ "version": "2.0.0",
4
+ "description": "Open source Claude Code CLI — based on ruDevolution decompilation of v2.1.91",
5
+ "type": "module",
6
+ "bin": {
7
+ "occ": "src/index.mjs"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "test/"
12
+ ],
13
+ "scripts": {
14
+ "start": "node src/index.mjs",
15
+ "test": "node test/test.mjs"
16
+ },
17
+ "keywords": [
18
+ "claude",
19
+ "code",
20
+ "cli",
21
+ "ai",
22
+ "decompiler",
23
+ "open-source"
24
+ ],
25
+ "license": "MIT",
26
+ "repository": "https://github.com/ruvnet/open-claude-code"
27
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Agent Loader — loads custom agent definitions from .claude/agents/
3
+ *
4
+ * Supports two formats:
5
+ * - JSON: .claude/agents/*.json
6
+ * - Markdown with YAML frontmatter: .claude/agents/*.md
7
+ *
8
+ * Agent definitions specify: name, description, model, tools, hooks, prompt.
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { parseAgentDefinition } from './parser.mjs';
14
+
15
+ export class AgentLoader {
16
+ constructor() {
17
+ this.agents = new Map();
18
+ this.searchPaths = [];
19
+ }
20
+
21
+ /**
22
+ * Load agents from standard directories.
23
+ * @param {string} [cwd] - project working directory
24
+ */
25
+ load(cwd = process.cwd()) {
26
+ this.searchPaths = [
27
+ path.join(cwd, '.claude', 'agents'),
28
+ path.join(process.env.HOME || '', '.claude', 'agents'),
29
+ ];
30
+
31
+ for (const dir of this.searchPaths) {
32
+ this._loadFromDir(dir);
33
+ }
34
+
35
+ return this;
36
+ }
37
+
38
+ _loadFromDir(dir) {
39
+ try {
40
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ if (!entry.isFile()) continue;
43
+ const ext = path.extname(entry.name);
44
+ if (ext !== '.json' && ext !== '.md') continue;
45
+
46
+ const filePath = path.join(dir, entry.name);
47
+ try {
48
+ const content = fs.readFileSync(filePath, 'utf-8');
49
+ const agent = parseAgentDefinition(content, ext);
50
+ if (agent && agent.name) {
51
+ this.agents.set(agent.name, { ...agent, source: filePath });
52
+ }
53
+ } catch (err) {
54
+ if (process.env.DEBUG) {
55
+ console.error(`Failed to load agent ${filePath}: ${err.message}`);
56
+ }
57
+ }
58
+ }
59
+ } catch {
60
+ // Directory does not exist
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get an agent definition by name.
66
+ * @param {string} name
67
+ * @returns {object|null}
68
+ */
69
+ get(name) {
70
+ return this.agents.get(name) || null;
71
+ }
72
+
73
+ /**
74
+ * List all loaded agents.
75
+ * @returns {Array<object>}
76
+ */
77
+ list() {
78
+ return [...this.agents.values()];
79
+ }
80
+
81
+ /**
82
+ * Check if an agent exists.
83
+ * @param {string} name
84
+ * @returns {boolean}
85
+ */
86
+ has(name) {
87
+ return this.agents.has(name);
88
+ }
89
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Agent Parser — parses agent definitions from JSON and Markdown formats.
3
+ *
4
+ * JSON format:
5
+ * {
6
+ * "name": "my-agent",
7
+ * "description": "Does things",
8
+ * "model": "claude-sonnet-4-6",
9
+ * "tools": ["Bash", "Read", "Write"],
10
+ * "hooks": { ... },
11
+ * "prompt": "You are a specialized agent..."
12
+ * }
13
+ *
14
+ * Markdown format (YAML frontmatter):
15
+ * ---
16
+ * name: my-agent
17
+ * description: Does things
18
+ * model: claude-sonnet-4-6
19
+ * tools: [Bash, Read, Write]
20
+ * ---
21
+ * You are a specialized agent...
22
+ */
23
+
24
+ /**
25
+ * Parse an agent definition from file content.
26
+ * @param {string} content - file content
27
+ * @param {string} ext - file extension (.json or .md)
28
+ * @returns {object} agent definition
29
+ */
30
+ export function parseAgentDefinition(content, ext) {
31
+ if (ext === '.json') {
32
+ return parseJsonAgent(content);
33
+ }
34
+ if (ext === '.md') {
35
+ return parseMarkdownAgent(content);
36
+ }
37
+ throw new Error(`Unsupported agent format: ${ext}`);
38
+ }
39
+
40
+ function parseJsonAgent(content) {
41
+ const data = JSON.parse(content);
42
+ return normalizeAgent(data);
43
+ }
44
+
45
+ function parseMarkdownAgent(content) {
46
+ const { frontmatter, body } = parseFrontmatter(content);
47
+ return normalizeAgent({ ...frontmatter, prompt: body });
48
+ }
49
+
50
+ /**
51
+ * Parse YAML-like frontmatter from markdown.
52
+ * Simple key-value parser (no full YAML dependency).
53
+ */
54
+ function parseFrontmatter(content) {
55
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
56
+ if (!match) {
57
+ return { frontmatter: {}, body: content };
58
+ }
59
+
60
+ const yamlBlock = match[1];
61
+ const body = match[2].trim();
62
+ const frontmatter = {};
63
+
64
+ for (const line of yamlBlock.split('\n')) {
65
+ const colonIdx = line.indexOf(':');
66
+ if (colonIdx === -1) continue;
67
+
68
+ const key = line.slice(0, colonIdx).trim();
69
+ let value = line.slice(colonIdx + 1).trim();
70
+
71
+ // Parse arrays: [a, b, c]
72
+ if (value.startsWith('[') && value.endsWith(']')) {
73
+ value = value.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
74
+ }
75
+ // Parse booleans
76
+ else if (value === 'true') value = true;
77
+ else if (value === 'false') value = false;
78
+ // Parse numbers
79
+ else if (/^\d+$/.test(value)) value = parseInt(value, 10);
80
+
81
+ frontmatter[key] = value;
82
+ }
83
+
84
+ return { frontmatter, body };
85
+ }
86
+
87
+ function normalizeAgent(data) {
88
+ return {
89
+ name: data.name || 'unnamed',
90
+ description: data.description || '',
91
+ model: data.model || null,
92
+ tools: Array.isArray(data.tools) ? data.tools : [],
93
+ hooks: data.hooks || {},
94
+ prompt: data.prompt || '',
95
+ maxTurns: data.maxTurns || data.max_turns || 10,
96
+ temperature: data.temperature || undefined,
97
+ };
98
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Agent Teams — multi-agent coordination.
3
+ *
4
+ * Allows registering named agents that can communicate via messages.
5
+ * Each teammate is an agent loop that can be invoked with a prompt.
6
+ */
7
+
8
+ export class AgentTeams {
9
+ constructor() {
10
+ /** @type {Map<string, { loop: object, role?: string, status: string }>} */
11
+ this.teammates = new Map();
12
+ this.messageLog = [];
13
+ }
14
+
15
+ /**
16
+ * Register a named agent.
17
+ * @param {string} name - unique agent name
18
+ * @param {object} agentLoop - agent loop with .run() async generator
19
+ * @param {object} [options]
20
+ * @param {string} [options.role] - agent role description
21
+ */
22
+ register(name, agentLoop, options = {}) {
23
+ if (this.teammates.has(name)) {
24
+ throw new Error(`Agent "${name}" is already registered`);
25
+ }
26
+ this.teammates.set(name, {
27
+ loop: agentLoop,
28
+ role: options.role || 'general',
29
+ status: 'idle',
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Unregister an agent.
35
+ * @param {string} name
36
+ * @returns {boolean}
37
+ */
38
+ unregister(name) {
39
+ return this.teammates.delete(name);
40
+ }
41
+
42
+ /**
43
+ * Send a message to a teammate and collect all events.
44
+ * @param {string} to - target agent name
45
+ * @param {string} message - prompt to send
46
+ * @returns {Promise<Array<object>>} collected events
47
+ */
48
+ async sendMessage(to, message) {
49
+ const agent = this.teammates.get(to);
50
+ if (!agent) throw new Error(`Unknown teammate: ${to}`);
51
+
52
+ agent.status = 'running';
53
+ const results = [];
54
+
55
+ try {
56
+ for await (const event of agent.loop.run(message)) {
57
+ results.push(event);
58
+ }
59
+ } finally {
60
+ agent.status = 'idle';
61
+ }
62
+
63
+ this.messageLog.push({
64
+ to,
65
+ message: message.substring(0, 100),
66
+ resultCount: results.length,
67
+ timestamp: new Date().toISOString(),
68
+ });
69
+
70
+ return results;
71
+ }
72
+
73
+ /**
74
+ * Broadcast a message to all teammates.
75
+ * @param {string} message
76
+ * @returns {Promise<Map<string, Array<object>>>} results per agent
77
+ */
78
+ async broadcast(message) {
79
+ const results = new Map();
80
+ const promises = [];
81
+
82
+ for (const [name] of this.teammates) {
83
+ promises.push(
84
+ this.sendMessage(name, message)
85
+ .then(events => results.set(name, events))
86
+ .catch(err => results.set(name, [{ type: 'error', message: err.message }]))
87
+ );
88
+ }
89
+
90
+ await Promise.all(promises);
91
+ return results;
92
+ }
93
+
94
+ /**
95
+ * List all registered teammates.
96
+ * @returns {Array<{ name: string, role: string, status: string }>}
97
+ */
98
+ list() {
99
+ return [...this.teammates.entries()].map(([name, info]) => ({
100
+ name,
101
+ role: info.role,
102
+ status: info.status,
103
+ }));
104
+ }
105
+
106
+ /**
107
+ * Get the message log.
108
+ * @param {number} [limit] - max entries to return
109
+ * @returns {Array<object>}
110
+ */
111
+ getMessageLog(limit) {
112
+ if (limit) return this.messageLog.slice(-limit);
113
+ return [...this.messageLog];
114
+ }
115
+
116
+ /**
117
+ * Get count of registered teammates.
118
+ * @returns {number}
119
+ */
120
+ size() {
121
+ return this.teammates.size;
122
+ }
123
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * OAuth Client — PKCE OAuth flow for Anthropic and other providers.
3
+ *
4
+ * Supports:
5
+ * - Device code flow (for headless environments)
6
+ * - Authorization code + PKCE
7
+ * - Token refresh
8
+ * - Credential storage in ~/.claude/credentials
9
+ */
10
+
11
+ import crypto from 'crypto';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import os from 'os';
15
+
16
+ export class OAuthClient {
17
+ /**
18
+ * @param {string} clientId - OAuth client ID
19
+ * @param {object} [options]
20
+ * @param {string} [options.authUrl] - authorization endpoint
21
+ * @param {string} [options.tokenUrl] - token endpoint
22
+ * @param {string} [options.deviceUrl] - device authorization endpoint
23
+ * @param {string} [options.credentialsPath] - path to store credentials
24
+ */
25
+ constructor(clientId, options = {}) {
26
+ this.clientId = clientId;
27
+ this.authUrl = options.authUrl || 'https://console.anthropic.com/oauth/authorize';
28
+ this.tokenUrl = options.tokenUrl || 'https://console.anthropic.com/oauth/token';
29
+ this.deviceUrl = options.deviceUrl || 'https://console.anthropic.com/oauth/device';
30
+ this.credentialsPath = options.credentialsPath ||
31
+ path.join(os.homedir(), '.claude', 'credentials');
32
+ }
33
+
34
+ /**
35
+ * Generate a PKCE code verifier and challenge.
36
+ * @returns {{ verifier: string, challenge: string }}
37
+ */
38
+ generatePKCE() {
39
+ const verifier = crypto.randomBytes(32)
40
+ .toString('base64url')
41
+ .replace(/[^a-zA-Z0-9]/g, '')
42
+ .substring(0, 128);
43
+
44
+ const challenge = crypto
45
+ .createHash('sha256')
46
+ .update(verifier)
47
+ .digest('base64url');
48
+
49
+ return { verifier, challenge };
50
+ }
51
+
52
+ /**
53
+ * Get the authorization URL for the PKCE flow.
54
+ * @param {object} [options]
55
+ * @param {string} [options.redirectUri] - redirect URI
56
+ * @param {string} [options.scope] - requested scope
57
+ * @returns {{ url: string, verifier: string, state: string }}
58
+ */
59
+ getAuthorizationUrl(options = {}) {
60
+ const { verifier, challenge } = this.generatePKCE();
61
+ const state = crypto.randomBytes(16).toString('hex');
62
+
63
+ const params = new URLSearchParams({
64
+ client_id: this.clientId,
65
+ response_type: 'code',
66
+ code_challenge: challenge,
67
+ code_challenge_method: 'S256',
68
+ state,
69
+ redirect_uri: options.redirectUri || 'http://localhost:9876/callback',
70
+ ...(options.scope && { scope: options.scope }),
71
+ });
72
+
73
+ return {
74
+ url: `${this.authUrl}?${params.toString()}`,
75
+ verifier,
76
+ state,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Start a device code flow (for headless environments).
82
+ * @returns {Promise<{ device_code: string, user_code: string, verification_uri: string, interval: number, expires_in: number }>}
83
+ */
84
+ async startDeviceFlow() {
85
+ const body = new URLSearchParams({
86
+ client_id: this.clientId,
87
+ });
88
+
89
+ const res = await fetch(this.deviceUrl, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
92
+ body: body.toString(),
93
+ });
94
+
95
+ if (!res.ok) {
96
+ const text = await res.text();
97
+ throw new Error(`Device flow failed (${res.status}): ${text}`);
98
+ }
99
+
100
+ return res.json();
101
+ }
102
+
103
+ /**
104
+ * Exchange an authorization code for tokens (PKCE flow).
105
+ * @param {string} code - authorization code
106
+ * @param {string} verifier - PKCE code verifier
107
+ * @param {string} [redirectUri]
108
+ * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number }>}
109
+ */
110
+ async exchangeCode(code, verifier, redirectUri) {
111
+ const body = new URLSearchParams({
112
+ grant_type: 'authorization_code',
113
+ client_id: this.clientId,
114
+ code,
115
+ code_verifier: verifier,
116
+ redirect_uri: redirectUri || 'http://localhost:9876/callback',
117
+ });
118
+
119
+ const res = await fetch(this.tokenUrl, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
122
+ body: body.toString(),
123
+ });
124
+
125
+ if (!res.ok) {
126
+ const text = await res.text();
127
+ throw new Error(`Token exchange failed (${res.status}): ${text}`);
128
+ }
129
+
130
+ const token = await res.json();
131
+ this.saveToken(token);
132
+ return token;
133
+ }
134
+
135
+ /**
136
+ * Refresh an access token using a refresh token.
137
+ * @param {string} refreshToken
138
+ * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number }>}
139
+ */
140
+ async refreshToken(refreshToken) {
141
+ const body = new URLSearchParams({
142
+ grant_type: 'refresh_token',
143
+ client_id: this.clientId,
144
+ refresh_token: refreshToken,
145
+ });
146
+
147
+ const res = await fetch(this.tokenUrl, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
150
+ body: body.toString(),
151
+ });
152
+
153
+ if (!res.ok) {
154
+ const text = await res.text();
155
+ throw new Error(`Token refresh failed (${res.status}): ${text}`);
156
+ }
157
+
158
+ const token = await res.json();
159
+ this.saveToken(token);
160
+ return token;
161
+ }
162
+
163
+ /**
164
+ * Get stored token from credentials file.
165
+ * @returns {object|null}
166
+ */
167
+ getStoredToken() {
168
+ try {
169
+ const raw = fs.readFileSync(this.credentialsPath, 'utf-8');
170
+ return JSON.parse(raw);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Save a token to the credentials file.
178
+ * @param {object} token
179
+ */
180
+ saveToken(token) {
181
+ try {
182
+ const dir = path.dirname(this.credentialsPath);
183
+ fs.mkdirSync(dir, { recursive: true });
184
+
185
+ const data = {
186
+ ...token,
187
+ saved_at: new Date().toISOString(),
188
+ };
189
+
190
+ fs.writeFileSync(this.credentialsPath, JSON.stringify(data, null, 2), { mode: 0o600 });
191
+ } catch {
192
+ // Best effort
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Delete stored credentials.
198
+ */
199
+ clearToken() {
200
+ try {
201
+ fs.unlinkSync(this.credentialsPath);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Check if the stored token is expired.
210
+ * @returns {boolean}
211
+ */
212
+ isTokenExpired() {
213
+ const token = this.getStoredToken();
214
+ if (!token || !token.saved_at || !token.expires_in) return true;
215
+
216
+ const savedAt = new Date(token.saved_at).getTime();
217
+ const expiresAt = savedAt + (token.expires_in * 1000);
218
+ return Date.now() >= expiresAt;
219
+ }
220
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * CLI Argument Parser — supports all major Claude Code flags.
3
+ *
4
+ * Flags:
5
+ * --model, -m Model to use
6
+ * --permission-mode Permission mode
7
+ * --print, -p Print mode (non-interactive prompt)
8
+ * --output-format json, text, stream-json
9
+ * --system-prompt Override system prompt
10
+ * --add-dir Additional CLAUDE.md directories
11
+ * --max-turns Maximum conversation turns
12
+ * --allowedTools Comma-separated allowed tools
13
+ * --disallowedTools Comma-separated denied tools
14
+ * --verbose, -v Verbose output
15
+ * --debug, -d Debug mode
16
+ * --version Show version
17
+ * --help, -h Show help
18
+ */
19
+
20
+ export function parseArgs(args) {
21
+ const result = {
22
+ prompt: null,
23
+ model: null,
24
+ permissionMode: null,
25
+ outputFormat: null,
26
+ systemPrompt: null,
27
+ addDirs: [],
28
+ maxTurns: null,
29
+ allowedTools: null,
30
+ disallowedTools: null,
31
+ verbose: false,
32
+ debug: false,
33
+ showVersion: false,
34
+ showHelp: false,
35
+ };
36
+
37
+ for (let i = 0; i < args.length; i++) {
38
+ const arg = args[i];
39
+
40
+ switch (arg) {
41
+ case '--model':
42
+ case '-m':
43
+ result.model = args[++i];
44
+ break;
45
+
46
+ case '--permission-mode':
47
+ result.permissionMode = args[++i];
48
+ break;
49
+
50
+ case '--print':
51
+ case '-p':
52
+ result.prompt = args[++i];
53
+ break;
54
+
55
+ case '--output-format':
56
+ result.outputFormat = args[++i];
57
+ break;
58
+
59
+ case '--system-prompt':
60
+ result.systemPrompt = args[++i];
61
+ break;
62
+
63
+ case '--add-dir':
64
+ result.addDirs.push(args[++i]);
65
+ break;
66
+
67
+ case '--max-turns':
68
+ result.maxTurns = parseInt(args[++i], 10);
69
+ break;
70
+
71
+ case '--allowedTools':
72
+ result.allowedTools = args[++i]?.split(',').map(s => s.trim());
73
+ break;
74
+
75
+ case '--disallowedTools':
76
+ result.disallowedTools = args[++i]?.split(',').map(s => s.trim());
77
+ break;
78
+
79
+ case '--verbose':
80
+ case '-v':
81
+ result.verbose = true;
82
+ break;
83
+
84
+ case '--debug':
85
+ case '-d':
86
+ result.debug = true;
87
+ break;
88
+
89
+ case '--version':
90
+ result.showVersion = true;
91
+ break;
92
+
93
+ case '--help':
94
+ case '-h':
95
+ result.showHelp = true;
96
+ break;
97
+
98
+ default:
99
+ // Bare argument becomes prompt
100
+ if (!arg.startsWith('-')) {
101
+ result.prompt = arg;
102
+ }
103
+ break;
104
+ }
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ /**
111
+ * Print usage/help text.
112
+ * @returns {string}
113
+ */
114
+ export function getUsageText() {
115
+ return `
116
+ Usage: occ [options] [prompt]
117
+
118
+ Options:
119
+ --model, -m <model> Model to use (default: claude-sonnet-4-6)
120
+ --permission-mode <mode> Permission mode (bypassPermissions, acceptEdits, plan, auto, dontAsk)
121
+ --print, -p <prompt> Non-interactive mode: run prompt and exit
122
+ --output-format <fmt> Output format: text, json, stream-json
123
+ --system-prompt <text> Override system prompt
124
+ --add-dir <dir> Additional directory to search for CLAUDE.md
125
+ --max-turns <n> Maximum conversation turns
126
+ --allowedTools <tools> Comma-separated list of allowed tools
127
+ --disallowedTools <tools> Comma-separated list of denied tools
128
+ --verbose, -v Verbose output
129
+ --debug, -d Debug mode
130
+ --version Show version
131
+ --help, -h Show this help
132
+
133
+ Examples:
134
+ occ Start interactive REPL
135
+ occ -p "What is 2+2?" Run prompt and exit
136
+ occ -m claude-haiku-4-5 Use Haiku model
137
+ occ --debug -p "Fix bug" Debug mode with prompt
138
+ `.trim();
139
+ }