sharkcode 0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/cli.mjs +351 -0
  4. package/package.json +48 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 syy-shark
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Shark Code
2
+
3
+ > Local First, open-source AI coding agent.
4
+ > A small CLI inspired by Claude Code / OpenCode, with DeepSeek as the default provider.
5
+
6
+ ## Install
7
+
8
+ ### Global install from npm
9
+
10
+ ```bash
11
+ npm install -g sharkcode
12
+ ```
13
+
14
+ Then run:
15
+
16
+ ```bash
17
+ sharkcode "explain this codebase"
18
+ sharkcode "fix the null pointer bug in auth.ts"
19
+ sharkcode "add error handling to the API routes"
20
+ ```
21
+
22
+ ### Run from source
23
+
24
+ ```bash
25
+ git clone https://github.com/syy-shark/sharkcode.git
26
+ cd sharkcode
27
+ bun install
28
+ bun run start "explain this codebase"
29
+ ```
30
+
31
+ ## Configure API Key
32
+
33
+ Shark Code reads `DEEPSEEK_API_KEY` from either an environment variable or `~/.sharkcode/config.toml`.
34
+
35
+ ### Option 1: Environment variable
36
+
37
+ macOS / Linux:
38
+
39
+ ```bash
40
+ export DEEPSEEK_API_KEY=sk-xxxxxx
41
+ ```
42
+
43
+ Windows PowerShell:
44
+
45
+ ```powershell
46
+ $env:DEEPSEEK_API_KEY="sk-xxxxxx"
47
+ ```
48
+
49
+ ### Option 2: Config file
50
+
51
+ `~/.sharkcode/config.toml`
52
+
53
+ ```toml
54
+ [api]
55
+ key = "sk-xxxxxx"
56
+ model = "deepseek-chat"
57
+ base_url = "https://api.deepseek.com/v1"
58
+ ```
59
+
60
+ ## Upgrade
61
+
62
+ When you publish a new version to npm, users can upgrade with:
63
+
64
+ ```bash
65
+ npm update -g sharkcode
66
+ ```
67
+
68
+ ## Publish
69
+
70
+ ```bash
71
+ npm login
72
+ npm publish
73
+ ```
74
+
75
+ Every code update is published as a new npm version. Typical flow:
76
+
77
+ 1. Update code.
78
+ 2. Bump `version` in `package.json`.
79
+ 3. Run `npm publish`.
80
+
81
+ ## How It Works
82
+
83
+ ```text
84
+ User input -> Prompt + Tools -> LLM -> Tool execution -> Result -> Repeat
85
+ ```
86
+
87
+ Built-in tools:
88
+
89
+ | Tool | Description |
90
+ |------|-------------|
91
+ | `read_file` | Read a file |
92
+ | `write_file` | Create or overwrite a file |
93
+ | `edit_file` | Replace an exact string in a file |
94
+ | `bash` | Execute a shell command with approval |
95
+
96
+ ## Tech Stack
97
+
98
+ - Bun + TypeScript
99
+ - Vercel AI SDK
100
+ - DeepSeek API
101
+
102
+ ## License
103
+
104
+ MIT
package/dist/cli.mjs ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import chalk3 from "chalk";
5
+
6
+ // src/config.ts
7
+ import { parse } from "smol-toml";
8
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ var CONFIG_DIR = join(homedir(), ".sharkcode");
12
+ var CONFIG_FILE = join(CONFIG_DIR, "config.toml");
13
+ var DEFAULT_CONFIG = `# Shark Code Configuration
14
+ # https://github.com/syy-ex/sharkcode
15
+
16
+ [api]
17
+ # Get your API key from https://platform.deepseek.com
18
+ # Or set DEEPSEEK_API_KEY environment variable
19
+ key = ""
20
+ model = "deepseek-chat"
21
+ base_url = "https://api.deepseek.com/v1"
22
+ `;
23
+ function ensureConfigDir() {
24
+ if (!existsSync(CONFIG_DIR)) {
25
+ mkdirSync(CONFIG_DIR, { recursive: true });
26
+ }
27
+ if (!existsSync(CONFIG_FILE)) {
28
+ writeFileSync(CONFIG_FILE, DEFAULT_CONFIG, "utf-8");
29
+ }
30
+ }
31
+ function loadConfig() {
32
+ ensureConfigDir();
33
+ let toml = {};
34
+ try {
35
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
36
+ toml = parse(raw);
37
+ } catch {
38
+ }
39
+ const api = toml.api ?? {};
40
+ const apiKey = process.env.DEEPSEEK_API_KEY || api.key || "";
41
+ const model = api.model || "deepseek-chat";
42
+ const baseURL = api.base_url || "https://api.deepseek.com/v1";
43
+ if (!apiKey) {
44
+ console.error("\u274C No API key found.");
45
+ console.error(` Set DEEPSEEK_API_KEY env var or edit ${CONFIG_FILE}`);
46
+ process.exit(1);
47
+ }
48
+ return { apiKey, model, baseURL };
49
+ }
50
+
51
+ // src/agent.ts
52
+ import { stepCountIs, streamText } from "ai";
53
+ import chalk2 from "chalk";
54
+
55
+ // src/tools/read-file.ts
56
+ import { tool } from "ai";
57
+ import { z } from "zod";
58
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
59
+ import { resolve } from "path";
60
+ var readFileTool = tool({
61
+ description: "Read the contents of a file at the given path. Returns the file content as a string.",
62
+ inputSchema: z.object({
63
+ path: z.string().describe("File path to read (relative to current working directory)")
64
+ }),
65
+ execute: async ({ path: filePath }) => {
66
+ const fullPath = resolve(process.cwd(), filePath);
67
+ if (!existsSync2(fullPath)) {
68
+ return `Error: File not found: ${filePath}`;
69
+ }
70
+ try {
71
+ return readFileSync2(fullPath, "utf-8");
72
+ } catch (err) {
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ return `Error reading file: ${msg}`;
75
+ }
76
+ }
77
+ });
78
+
79
+ // src/tools/write-file.ts
80
+ import { tool as tool2 } from "ai";
81
+ import { z as z2 } from "zod";
82
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
83
+ import { resolve as resolve2, dirname } from "path";
84
+ var writeFileTool = tool2({
85
+ description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Creates parent directories as needed.",
86
+ inputSchema: z2.object({
87
+ path: z2.string().describe("File path to write to (relative to current working directory)"),
88
+ content: z2.string().describe("The content to write to the file")
89
+ }),
90
+ execute: async ({ path: filePath, content }) => {
91
+ const fullPath = resolve2(process.cwd(), filePath);
92
+ try {
93
+ mkdirSync2(dirname(fullPath), { recursive: true });
94
+ writeFileSync2(fullPath, content, "utf-8");
95
+ return `Successfully wrote to ${filePath}`;
96
+ } catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ return `Error writing file: ${msg}`;
99
+ }
100
+ }
101
+ });
102
+
103
+ // src/tools/edit-file.ts
104
+ import { tool as tool3 } from "ai";
105
+ import { z as z3 } from "zod";
106
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
107
+ import { resolve as resolve3 } from "path";
108
+ var editFileTool = tool3({
109
+ description: "Edit a file by replacing an exact string match with new content. The old_str must match exactly one occurrence in the file.",
110
+ inputSchema: z3.object({
111
+ path: z3.string().describe("File path to edit (relative to current working directory)"),
112
+ old_str: z3.string().describe("The exact string to find and replace"),
113
+ new_str: z3.string().describe("The replacement string")
114
+ }),
115
+ execute: async ({ path: filePath, old_str, new_str }) => {
116
+ const fullPath = resolve3(process.cwd(), filePath);
117
+ if (!existsSync3(fullPath)) {
118
+ return `Error: File not found: ${filePath}`;
119
+ }
120
+ try {
121
+ const content = readFileSync3(fullPath, "utf-8");
122
+ const count = content.split(old_str).length - 1;
123
+ if (count === 0) {
124
+ return `Error: old_str not found in ${filePath}`;
125
+ }
126
+ if (count > 1) {
127
+ return `Error: old_str found ${count} times in ${filePath}, expected exactly 1. Include more context to make it unique.`;
128
+ }
129
+ const newContent = content.replace(old_str, new_str);
130
+ writeFileSync3(fullPath, newContent, "utf-8");
131
+ return `Successfully edited ${filePath}`;
132
+ } catch (err) {
133
+ const msg = err instanceof Error ? err.message : String(err);
134
+ return `Error editing file: ${msg}`;
135
+ }
136
+ }
137
+ });
138
+
139
+ // src/tools/bash.ts
140
+ import { tool as tool4 } from "ai";
141
+ import { z as z4 } from "zod";
142
+ import { exec } from "child_process";
143
+
144
+ // src/permission.ts
145
+ import * as readline from "readline";
146
+ import chalk from "chalk";
147
+ async function askPermission(command) {
148
+ process.stderr.write(
149
+ chalk.yellow(`
150
+ \u26A0\uFE0F Will execute: `) + chalk.white(command) + "\n"
151
+ );
152
+ const rl = readline.createInterface({
153
+ input: process.stdin,
154
+ output: process.stderr
155
+ });
156
+ return new Promise((resolve4) => {
157
+ rl.question(chalk.yellow(" Allow? [y/N] "), (answer) => {
158
+ rl.close();
159
+ const yes = answer.trim().toLowerCase();
160
+ resolve4(yes === "y" || yes === "yes");
161
+ });
162
+ });
163
+ }
164
+
165
+ // src/tools/bash.ts
166
+ var bashTool = tool4({
167
+ description: "Execute a shell command. The command will be shown to the user for approval before execution. Use this for running tests, installing packages, checking git status, etc.",
168
+ inputSchema: z4.object({
169
+ command: z4.string().describe("The shell command to execute")
170
+ }),
171
+ execute: async ({ command }) => {
172
+ const allowed = await askPermission(command);
173
+ if (!allowed) {
174
+ return "Command execution denied by user.";
175
+ }
176
+ return new Promise((resolve4) => {
177
+ exec(
178
+ command,
179
+ {
180
+ cwd: process.cwd(),
181
+ timeout: 12e4,
182
+ maxBuffer: 1024 * 1024 * 5
183
+ },
184
+ (error, stdout, stderr) => {
185
+ let result = "";
186
+ if (stdout) result += stdout;
187
+ if (stderr) result += (result ? "\n" : "") + `[stderr]: ${stderr}`;
188
+ if (error && error.code !== null) {
189
+ result += (result ? "\n" : "") + `[exit code]: ${error.code}`;
190
+ }
191
+ resolve4(result || "(no output)");
192
+ }
193
+ );
194
+ });
195
+ }
196
+ });
197
+
198
+ // src/tools/index.ts
199
+ var tools = {
200
+ read_file: readFileTool,
201
+ write_file: writeFileTool,
202
+ edit_file: editFileTool,
203
+ bash: bashTool
204
+ };
205
+
206
+ // src/provider.ts
207
+ import { createOpenAI } from "@ai-sdk/openai";
208
+ function createProvider(config) {
209
+ const provider = createOpenAI({
210
+ baseURL: config.baseURL,
211
+ apiKey: config.apiKey,
212
+ name: "deepseek"
213
+ });
214
+ return provider.chat(config.model);
215
+ }
216
+
217
+ // src/agent.ts
218
+ function buildSystemPrompt() {
219
+ return `You are Shark Code, an AI coding assistant. You help users with coding tasks by reading, writing, and editing files, and running shell commands.
220
+
221
+ Current working directory: ${process.cwd()}
222
+ Environment: Windows with PowerShell. Use PowerShell-compatible commands, not bash syntax.
223
+
224
+ Available tools:
225
+ - read_file: Read the contents of a file
226
+ - write_file: Create or overwrite a file (creates parent dirs)
227
+ - edit_file: Find and replace an exact string in a file
228
+ - bash: Execute a shell command (requires user approval)
229
+
230
+ Guidelines:
231
+ - Always read a file before editing it
232
+ - Explain what you plan to do before making changes
233
+ - Use edit_file for targeted changes, write_file for creating new files
234
+ - When using shell commands, prefer PowerShell built-ins or commands that work on Windows
235
+ - Keep changes minimal and focused on the user's request
236
+ - When done, summarize what you changed`;
237
+ }
238
+ async function runAgent(prompt, config) {
239
+ const model = createProvider(config);
240
+ const result = streamText({
241
+ model,
242
+ system: buildSystemPrompt(),
243
+ prompt,
244
+ tools,
245
+ stopWhen: stepCountIs(30)
246
+ });
247
+ let currentStep = 0;
248
+ for await (const event of result.fullStream) {
249
+ switch (event.type) {
250
+ case "text-delta":
251
+ process.stdout.write(event.text);
252
+ break;
253
+ case "tool-input-available":
254
+ process.stdout.write(
255
+ chalk2.cyan(`
256
+ \u{1F527} ${event.toolName}`) + chalk2.gray(`(${formatArgs(event.input)})
257
+ `)
258
+ );
259
+ break;
260
+ case "tool-output-available":
261
+ process.stdout.write(
262
+ chalk2.green(`\u2705 tool`) + chalk2.gray(` \u2192 ${truncate(String(event.output), 120)}
263
+
264
+ `)
265
+ );
266
+ break;
267
+ case "tool-output-error":
268
+ process.stderr.write(chalk2.red(`
269
+ \u274C Tool error: ${event.errorText}
270
+ `));
271
+ break;
272
+ case "finish-step":
273
+ currentStep++;
274
+ break;
275
+ case "error":
276
+ process.stderr.write(chalk2.red(`
277
+ \u274C Error: ${event.errorText}
278
+ `));
279
+ break;
280
+ }
281
+ }
282
+ process.stdout.write("\n");
283
+ const usage = await result.totalUsage;
284
+ if (usage) {
285
+ process.stderr.write(
286
+ chalk2.gray(
287
+ `
288
+ \u{1F4CA} Tokens: ${usage.inputTokens} in / ${usage.outputTokens} out | Steps: ${currentStep}
289
+ `
290
+ )
291
+ );
292
+ }
293
+ }
294
+ function formatArgs(args) {
295
+ if (typeof args !== "object" || args === null) return String(args);
296
+ const obj = args;
297
+ return Object.entries(obj).map(([k, v]) => {
298
+ const val = typeof v === "string" ? truncate(v, 60) : String(v);
299
+ return `${k}: ${val}`;
300
+ }).join(", ");
301
+ }
302
+ function truncate(str, max) {
303
+ const oneLine = str.replace(/\n/g, "\\n");
304
+ return oneLine.length > max ? oneLine.slice(0, max) + "\u2026" : oneLine;
305
+ }
306
+
307
+ // src/cli.ts
308
+ var BANNER = chalk3.bold.cyan(`
309
+ \u{1F988} Shark Code v0.1
310
+ AI Coding Agent \u2014 Local First, Open Source
311
+ `);
312
+ var HELP = `${BANNER}
313
+ ${chalk3.white("Usage:")}
314
+ ${chalk3.green("sharkcode")} ${chalk3.yellow('"your prompt here"')}
315
+
316
+ ${chalk3.white("Examples:")}
317
+ sharkcode "explain this codebase"
318
+ sharkcode "fix the null pointer bug in auth.ts"
319
+ sharkcode "add error handling to the API routes"
320
+
321
+ ${chalk3.white("Config:")}
322
+ Set ${chalk3.yellow("DEEPSEEK_API_KEY")} env var, or edit ${chalk3.gray("~/.sharkcode/config.toml")}
323
+ Get your key at ${chalk3.underline("https://platform.deepseek.com")}
324
+ `;
325
+ async function main() {
326
+ const args = process.argv.slice(2);
327
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
328
+ console.log(HELP);
329
+ return;
330
+ }
331
+ if (args[0] === "--version" || args[0] === "-v") {
332
+ console.log("sharkcode v0.1.0");
333
+ return;
334
+ }
335
+ const prompt = args.join(" ");
336
+ const config = loadConfig();
337
+ process.stdout.write(
338
+ chalk3.cyan("\u{1F988} Shark Code") + chalk3.gray(` | model: ${config.model}
339
+
340
+ `)
341
+ );
342
+ await runAgent(prompt, config);
343
+ }
344
+ main().catch((err) => {
345
+ console.error(chalk3.red(`
346
+ \u274C Fatal: ${err.message}`));
347
+ if (err.message?.includes("401") || err.message?.includes("Unauthorized")) {
348
+ console.error(chalk3.yellow(" Check your API key in ~/.sharkcode/config.toml"));
349
+ }
350
+ process.exit(1);
351
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "sharkcode",
3
+ "version": "0.1.0",
4
+ "description": "Local First, open-source AI Coding Agent",
5
+ "type": "module",
6
+ "bin": {
7
+ "sharkcode": "dist/cli.mjs"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "start": "bun src/cli.ts",
16
+ "dev": "bun --watch src/cli.ts",
17
+ "build": "tsup",
18
+ "test": "bun test",
19
+ "prepublishOnly": "npm run build && bun test"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest",
23
+ "@types/node": "^24.6.0",
24
+ "tsup": "^8.5.0",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "dependencies": {
28
+ "@ai-sdk/openai": "^3.0.48",
29
+ "ai": "^6.0.141",
30
+ "chalk": "^5.6.2",
31
+ "smol-toml": "^1.6.1",
32
+ "zod": "^4.3.6"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/syy-shark/sharkcode.git"
43
+ },
44
+ "homepage": "https://github.com/syy-shark/sharkcode",
45
+ "bugs": {
46
+ "url": "https://github.com/syy-shark/sharkcode/issues"
47
+ }
48
+ }