sharkcode 0.2.1 → 0.3.1

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 (3) hide show
  1. package/README.md +59 -39
  2. package/dist/cli.mjs +223 -51
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,49 +1,46 @@
1
1
  # Shark Code
2
2
 
3
- > Local First, open-source AI coding agent.
3
+ > Local First, open-source AI coding agent.
4
4
  > A small CLI inspired by Claude Code / OpenCode, with DeepSeek as the default provider.
5
5
 
6
6
  ## Install
7
7
 
8
- ### Global install from npm
9
-
10
8
  ```bash
11
9
  npm install -g sharkcode
12
10
  ```
13
11
 
14
- Then run:
12
+ Launch interactive mode:
15
13
 
16
14
  ```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"
15
+ sharkcode
20
16
  ```
21
17
 
22
- ### Run from source
18
+ Or single-shot mode:
23
19
 
24
20
  ```bash
25
- git clone https://github.com/syy-shark/sharkcode.git
26
- cd sharkcode
27
- bun install
28
- bun run start "explain this codebase"
21
+ sharkcode "explain this codebase"
22
+ sharkcode "fix the null pointer bug in auth.ts"
29
23
  ```
30
24
 
31
- ## Configure API Key
25
+ ## Providers
32
26
 
33
- Shark Code reads `DEEPSEEK_API_KEY` from either an environment variable or `~/.sharkcode/config.toml`.
27
+ Shark Code supports multiple providers. Switch between them anytime with `/provider`.
34
28
 
35
- ### Option 1: Environment variable
29
+ | Provider | Description | Docs |
30
+ |----------|-------------|------|
31
+ | `deepseek` | DeepSeek 官网 (default) | [platform.deepseek.com](https://platform.deepseek.com) |
32
+ | `ark` | 火山引擎 方舟 Coding Plan | [volcengine.com/activity/codingplan](https://www.volcengine.com/activity/codingplan) |
36
33
 
37
- macOS / Linux:
34
+ ## Configure
38
35
 
39
- ```bash
40
- export DEEPSEEK_API_KEY=sk-xxxxxx
41
- ```
36
+ ### Option 1: Slash commands (recommended)
42
37
 
43
- Windows PowerShell:
38
+ Start `sharkcode`, then:
44
39
 
45
- ```powershell
46
- $env:DEEPSEEK_API_KEY="sk-xxxxxx"
40
+ ```
41
+ ◆ /provider ark # switch to 方舟 Coding Plan
42
+ ◆ /key sk-xxxxxxxxxx # set API key (saved automatically)
43
+ ◆ /model ark-code-latest # optionally change model
47
44
  ```
48
45
 
49
46
  ### Option 2: Config file
@@ -51,37 +48,60 @@ $env:DEEPSEEK_API_KEY="sk-xxxxxx"
51
48
  `~/.sharkcode/config.toml`
52
49
 
53
50
  ```toml
54
- [api]
51
+ [default]
52
+ provider = "deepseek" # or "ark"
53
+
54
+ [providers.deepseek]
55
+ # API key from https://platform.deepseek.com
55
56
  key = "sk-xxxxxx"
56
57
  model = "deepseek-chat"
57
- base_url = "https://api.deepseek.com/v1"
58
- ```
59
58
 
60
- ## Upgrade
59
+ [providers.ark]
60
+ # API key from https://ark.cn-beijing.volces.com (方舟 Coding Plan)
61
+ key = "sk-xxxxxx"
62
+ model = "ark-code-latest"
63
+ ```
61
64
 
62
- When you publish a new version to npm, users can upgrade with:
65
+ ### Option 3: Environment variables
63
66
 
64
- ```bash
65
- npm update -g sharkcode
67
+ ```powershell
68
+ $env:DEEPSEEK_API_KEY="sk-xxxxxx" # deepseek provider
69
+ $env:ARK_API_KEY="sk-xxxxxx" # ark provider
66
70
  ```
67
71
 
68
- ## Publish
72
+ ## Slash Commands
73
+
74
+ Type `/` commands anytime inside the interactive REPL:
75
+
76
+ | Command | Description |
77
+ |---------|-------------|
78
+ | `/provider` | show current provider & key status |
79
+ | `/provider <name>` | switch provider (`deepseek` \| `ark`) |
80
+ | `/key <api-key>` | set API key for current provider |
81
+ | `/model <model-id>` | set model for current provider |
82
+ | `/clear` | clear conversation history |
83
+ | `/help` | show command list |
84
+ | `/exit` | quit |
85
+
86
+ ## Run from source
69
87
 
70
88
  ```bash
71
- npm login
72
- npm publish
89
+ git clone https://github.com/syy-shark/sharkcode.git
90
+ cd sharkcode
91
+ bun install
92
+ bun run start
73
93
  ```
74
94
 
75
- Every code update is published as a new npm version. Typical flow:
95
+ ## Upgrade
76
96
 
77
- 1. Update code.
78
- 2. Bump `version` in `package.json`.
79
- 3. Run `npm publish`.
97
+ ```bash
98
+ npm update -g sharkcode
99
+ ```
80
100
 
81
101
  ## How It Works
82
102
 
83
- ```text
84
- User input -> Prompt + Tools -> LLM -> Tool execution -> Result -> Repeat
103
+ ```
104
+ User input Prompt + Tools LLM Tool execution Result Repeat
85
105
  ```
86
106
 
87
107
  Built-in tools:
@@ -97,7 +117,7 @@ Built-in tools:
97
117
 
98
118
  - Bun + TypeScript
99
119
  - Vercel AI SDK
100
- - DeepSeek API
120
+ - DeepSeek API / 火山引擎 方舟 API
101
121
 
102
122
  ## License
103
123
 
package/dist/cli.mjs CHANGED
@@ -3,50 +3,107 @@
3
3
  // src/cli.ts
4
4
  import chalk3 from "chalk";
5
5
  import * as readline2 from "readline";
6
+ import { select, input } from "@inquirer/prompts";
6
7
 
7
8
  // src/config.ts
8
9
  import { parse } from "smol-toml";
9
10
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
10
11
  import { join } from "path";
11
12
  import { homedir } from "os";
13
+ var PROVIDERS = {
14
+ deepseek: {
15
+ baseURL: "https://api.deepseek.com/v1",
16
+ defaultModel: "deepseek-chat",
17
+ label: "DeepSeek \u5B98\u7F51"
18
+ },
19
+ ark: {
20
+ // Coding Plan uses a dedicated endpoint — do NOT use /api/v3 (that's the pay-per-use endpoint)
21
+ baseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
22
+ defaultModel: "ark-code-latest",
23
+ label: "\u65B9\u821F Coding Plan"
24
+ }
25
+ };
12
26
  var CONFIG_DIR = join(homedir(), ".sharkcode");
13
27
  var CONFIG_FILE = join(CONFIG_DIR, "config.toml");
14
- var DEFAULT_CONFIG = `# Shark Code Configuration
15
- # https://github.com/syy-ex/sharkcode
16
-
17
- [api]
18
- # Get your API key from https://platform.deepseek.com
19
- # Or set DEEPSEEK_API_KEY environment variable
20
- key = ""
21
- model = "deepseek-chat"
22
- base_url = "https://api.deepseek.com/v1"
23
- `;
24
- function ensureConfigDir() {
28
+ function serializeConfig(mc) {
29
+ const dp = mc.providers.deepseek;
30
+ const ap = mc.providers.ark;
31
+ return [
32
+ "# Shark Code Configuration",
33
+ "# https://github.com/syy-shark/sharkcode",
34
+ "",
35
+ "[default]",
36
+ `provider = "${mc.activeProvider}"`,
37
+ "",
38
+ "[providers.deepseek]",
39
+ "# API key from https://platform.deepseek.com",
40
+ `key = "${dp?.key ?? ""}"`,
41
+ `model = "${dp?.model ?? PROVIDERS.deepseek.defaultModel}"`,
42
+ "",
43
+ "[providers.ark]",
44
+ "# API key from https://ark.cn-beijing.volces.com (\u65B9\u821F Coding Plan)",
45
+ `key = "${ap?.key ?? ""}"`,
46
+ `model = "${ap?.model ?? PROVIDERS.ark.defaultModel}"`,
47
+ ""
48
+ ].join("\n");
49
+ }
50
+ function ensureConfig() {
25
51
  if (!existsSync(CONFIG_DIR)) {
26
52
  mkdirSync(CONFIG_DIR, { recursive: true });
27
53
  }
28
54
  if (!existsSync(CONFIG_FILE)) {
29
- writeFileSync(CONFIG_FILE, DEFAULT_CONFIG, "utf-8");
55
+ const initial = {
56
+ activeProvider: "deepseek",
57
+ providers: {
58
+ deepseek: { key: "", model: PROVIDERS.deepseek.defaultModel },
59
+ ark: { key: "", model: PROVIDERS.ark.defaultModel }
60
+ }
61
+ };
62
+ writeFileSync(CONFIG_FILE, serializeConfig(initial), "utf-8");
30
63
  }
31
64
  }
32
- function loadConfig() {
33
- ensureConfigDir();
65
+ function readMultiConfig() {
66
+ ensureConfig();
34
67
  let toml = {};
35
68
  try {
36
69
  const raw = readFileSync(CONFIG_FILE, "utf-8");
37
70
  toml = parse(raw);
38
71
  } catch {
39
72
  }
40
- const api = toml.api ?? {};
41
- const apiKey = process.env.DEEPSEEK_API_KEY || api.key || "";
42
- const model = api.model || "deepseek-chat";
43
- const baseURL = api.base_url || "https://api.deepseek.com/v1";
44
- if (!apiKey) {
45
- console.error("\u274C No API key found.");
46
- console.error(` Set DEEPSEEK_API_KEY env var or edit ${CONFIG_FILE}`);
47
- process.exit(1);
48
- }
49
- return { apiKey, model, baseURL };
73
+ const legacy = toml.api;
74
+ const providersRaw = toml.providers ?? {};
75
+ const defaultSection = toml.default ?? {};
76
+ const deepseekKey = process.env.DEEPSEEK_API_KEY || providersRaw.deepseek?.key || legacy?.key || "";
77
+ const arkKey = process.env.ARK_API_KEY || providersRaw.ark?.key || "";
78
+ const activeProvider = defaultSection.provider || (legacy ? "deepseek" : "deepseek");
79
+ return {
80
+ activeProvider,
81
+ providers: {
82
+ deepseek: {
83
+ key: deepseekKey,
84
+ model: providersRaw.deepseek?.model || legacy?.model || PROVIDERS.deepseek.defaultModel
85
+ },
86
+ ark: {
87
+ key: arkKey,
88
+ model: providersRaw.ark?.model || PROVIDERS.ark.defaultModel
89
+ }
90
+ }
91
+ };
92
+ }
93
+ function saveMultiConfig(mc) {
94
+ ensureConfig();
95
+ writeFileSync(CONFIG_FILE, serializeConfig(mc), "utf-8");
96
+ }
97
+ function resolveConfig(mc) {
98
+ const name = mc.activeProvider;
99
+ const entry = mc.providers[name];
100
+ const meta = PROVIDERS[name];
101
+ return {
102
+ providerName: name,
103
+ apiKey: entry?.key || "",
104
+ model: entry?.model || meta?.defaultModel || "deepseek-chat",
105
+ baseURL: meta?.baseURL || PROVIDERS.deepseek.baseURL
106
+ };
50
107
  }
51
108
 
52
109
  // src/agent.ts
@@ -210,7 +267,8 @@ function createProvider(config) {
210
267
  const provider = createOpenAI({
211
268
  baseURL: config.baseURL,
212
269
  apiKey: config.apiKey,
213
- name: "deepseek"
270
+ // ARK requires a non-default name to avoid SDK header overrides
271
+ name: config.providerName === "ark" ? "ark" : "deepseek"
214
272
  });
215
273
  return provider.chat(config.model);
216
274
  }
@@ -348,6 +406,11 @@ function truncate(str, max) {
348
406
 
349
407
  // src/cli.ts
350
408
  var PURPLE2 = chalk3.hex("#a855f7");
409
+ var GRAY = chalk3.gray;
410
+ var YELLOW = chalk3.yellow;
411
+ var GREEN = chalk3.green;
412
+ var RED = chalk3.red;
413
+ var CYAN = chalk3.cyan;
351
414
  var GLYPHS = {
352
415
  S: [[0, 1, 1, 1, 1], [1, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 0, 0, 0, 1], [1, 1, 1, 1, 0]],
353
416
  H: [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]],
@@ -376,6 +439,80 @@ function renderWord(word, padLeft = 2) {
376
439
  return rows;
377
440
  }
378
441
  var BANNER = ["", ...renderWord("shark", 2), "", ...renderWord("code", 8), ""].join("\n");
442
+ async function showCommandMenu(multiConfig) {
443
+ console.log();
444
+ try {
445
+ const action = await select({
446
+ message: PURPLE2("\u25C6 \u9009\u62E9\u64CD\u4F5C"),
447
+ choices: [
448
+ { name: "\u{1F50C} \u5207\u6362 / \u914D\u7F6E Provider", value: "provider" },
449
+ { name: "\u{1F5D1}\uFE0F \u6E05\u7A7A\u5BF9\u8BDD\u5386\u53F2", value: "clear" },
450
+ { name: "\u{1F6AA} \u9000\u51FA", value: "exit" }
451
+ ]
452
+ });
453
+ switch (action) {
454
+ case "provider":
455
+ return showSetupFlow(multiConfig);
456
+ case "clear":
457
+ console.log(GRAY("\n \u2713 \u5BF9\u8BDD\u5DF2\u6E05\u7A7A\n"));
458
+ return { multiConfig, config: resolveConfig(multiConfig), clearHistory: true };
459
+ case "exit":
460
+ console.log(GRAY("\nBye! \u{1F988}"));
461
+ return { multiConfig, config: resolveConfig(multiConfig), exit: true };
462
+ }
463
+ } catch {
464
+ console.log(GRAY("\n \u53D6\u6D88\n"));
465
+ }
466
+ return { multiConfig, config: resolveConfig(multiConfig) };
467
+ }
468
+ async function showSetupFlow(multiConfig) {
469
+ console.log();
470
+ try {
471
+ const providerChoices = Object.entries(PROVIDERS).map(([id, meta]) => {
472
+ const hasKey = !!multiConfig.providers[id]?.key;
473
+ const badge = hasKey ? GREEN("\u2713 \u5DF2\u914D\u7F6E") : YELLOW("\u2717 \u672A\u914D\u7F6E");
474
+ return { name: `${meta.label} ${badge}`, value: id };
475
+ });
476
+ const selectedProvider = await select({
477
+ message: PURPLE2("\u25C6 \u9009\u62E9 Provider"),
478
+ choices: providerChoices,
479
+ default: multiConfig.activeProvider
480
+ });
481
+ const currentKey = multiConfig.providers[selectedProvider]?.key ?? "";
482
+ const hint = currentKey ? GRAY("(\u56DE\u8F66\u4FDD\u7559 " + currentKey.slice(0, 6) + "\u2022\u2022\u2022)") : GRAY("(\u5FC5\u586B)");
483
+ const rawKey = await input({
484
+ message: PURPLE2("\u25C6 API Key ") + hint,
485
+ default: currentKey || void 0
486
+ });
487
+ const newKey = rawKey.trim() || currentKey;
488
+ let updated = { ...multiConfig, activeProvider: selectedProvider };
489
+ if (newKey) {
490
+ updated = {
491
+ ...updated,
492
+ providers: {
493
+ ...updated.providers,
494
+ [selectedProvider]: {
495
+ model: PROVIDERS[selectedProvider]?.defaultModel ?? "",
496
+ ...updated.providers[selectedProvider] ?? {},
497
+ key: newKey
498
+ }
499
+ }
500
+ };
501
+ }
502
+ saveMultiConfig(updated);
503
+ const newConfig = resolveConfig(updated);
504
+ const keyMsg = newKey && newKey !== currentKey ? "\uFF0CAPI Key \u5DF2\u4FDD\u5B58" : "";
505
+ console.log(GREEN(`
506
+ \u2713 \u5DF2\u5207\u6362\u5230 ${PROVIDERS[selectedProvider].label}${keyMsg}`) + "\n");
507
+ if (!newConfig.apiKey) {
508
+ console.log(YELLOW(" \u26A0 \u8FD8\u672A\u586B\u5199 API Key\uFF0C\u65E0\u6CD5\u53D1\u9001\u6D88\u606F\n"));
509
+ }
510
+ return { multiConfig: updated, config: newConfig };
511
+ } catch {
512
+ console.log(GRAY("\n \u53D6\u6D88\n"));
513
+ return { multiConfig, config: resolveConfig(multiConfig) };
514
+ }
515
+ }
379
516
  async function readLine(prompt) {
380
517
  return new Promise((resolve4) => {
381
518
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
@@ -390,60 +527,95 @@ async function readLine(prompt) {
390
527
  });
391
528
  });
392
529
  }
530
+ function statusLine(config) {
531
+ const label = PROVIDERS[config.providerName]?.label ?? config.providerName;
532
+ return PURPLE2(" \u25C6") + GRAY(` ${label}`) + CYAN(` [${config.model}]`) + GRAY(" \u8F93\u5165 / \u8C03\u51FA\u6307\u4EE4\u83DC\u5355\n");
533
+ }
393
534
  async function main() {
394
535
  const args = process.argv.slice(2);
395
536
  if (args[0] === "--help" || args[0] === "-h") {
396
537
  console.log(BANNER);
397
538
  console.log(PURPLE2(" Usage:"));
398
- console.log(" " + PURPLE2("sharkcode") + chalk3.gray(" \u2014 interactive mode"));
399
- console.log(" " + PURPLE2("sharkcode") + chalk3.yellow(' "prompt"') + chalk3.gray(" \u2014 single-shot mode\n"));
539
+ console.log(" " + PURPLE2("sharkcode") + GRAY(" \u2014 \u4EA4\u4E92\u6A21\u5F0F\uFF08\u76F4\u63A5\u542F\u52A8\uFF09"));
540
+ console.log(" " + PURPLE2("sharkcode") + YELLOW(' "prompt"') + GRAY(" \u2014 \u5355\u6B21\u6267\u884C"));
541
+ console.log(GRAY("\n \u4EA4\u4E92\u6A21\u5F0F\u5185\u8F93\u5165 / \u8C03\u51FA\u6307\u4EE4\u83DC\u5355\n"));
400
542
  return;
401
543
  }
402
544
  if (args[0] === "--version" || args[0] === "-v") {
403
- console.log("sharkcode v0.2.0");
545
+ console.log("sharkcode v0.3.1");
404
546
  return;
405
547
  }
406
- const config = loadConfig();
407
548
  if (args.length > 0) {
408
- console.log(PURPLE2("\n\u{1F988} SharkCode") + chalk3.gray(` | model: ${config.model}
409
- `));
410
- const messages2 = [{ role: "user", content: args.join(" ") }];
411
- await runAgent(messages2, config);
549
+ const mc = readMultiConfig();
550
+ const config2 = resolveConfig(mc);
551
+ if (!config2.apiKey) {
552
+ console.error(RED("\u274C \u672A\u914D\u7F6E API Key\u3002\u8BF7\u5148\u8FD0\u884C sharkcode \u5E76\u8F93\u5165 / \u914D\u7F6E Provider\u3002"));
553
+ process.exit(1);
554
+ }
555
+ console.log(
556
+ PURPLE2("\n\u{1F988} SharkCode") + GRAY(` | ${PROVIDERS[config2.providerName]?.label ?? config2.providerName} | ${config2.model}
557
+ `)
558
+ );
559
+ await runAgent([{ role: "user", content: args.join(" ") }], config2);
412
560
  return;
413
561
  }
414
562
  console.log(BANNER);
415
- console.log(
416
- PURPLE2(" \u25C6") + chalk3.gray(` model: ${config.model}`) + chalk3.gray(' type "exit" to quit\n')
417
- );
563
+ let multiConfig = readMultiConfig();
564
+ let config = resolveConfig(multiConfig);
565
+ console.log(statusLine(config));
566
+ if (!config.apiKey) {
567
+ console.log(
568
+ YELLOW(" \u26A0 \u5C1A\u672A\u914D\u7F6E API Key\u3002") + GRAY("\u8F93\u5165 / \u7136\u540E\u9009\u62E9\u300C\u5207\u6362 / \u914D\u7F6E Provider\u300D\n")
569
+ );
570
+ }
418
571
  let messages = [];
419
572
  while (true) {
420
- const input = await readLine(PURPLE2("\n\u25C6 "));
421
- if (input === null) {
422
- console.log(chalk3.gray("\nBye! \u{1F988}"));
573
+ const raw = await readLine(PURPLE2("\n\u25C6 "));
574
+ if (raw === null) {
575
+ console.log(GRAY("\nBye! \u{1F988}"));
423
576
  break;
424
577
  }
425
- const trimmed = input.trim();
578
+ const trimmed = raw.trim();
426
579
  if (!trimmed) continue;
427
- if (trimmed === "exit" || trimmed === "quit" || trimmed === "/exit") {
428
- console.log(chalk3.gray("Bye! \u{1F988}"));
580
+ if (trimmed === "exit" || trimmed === "quit") {
581
+ console.log(GRAY("Bye! \u{1F988}"));
429
582
  break;
430
583
  }
584
+ if (trimmed === "/") {
585
+ const r = await showCommandMenu(multiConfig);
586
+ multiConfig = r.multiConfig;
587
+ config = r.config;
588
+ if (r.clearHistory) messages = [];
589
+ if (r.exit) break;
590
+ continue;
591
+ }
592
+ if (trimmed === "/provider" || trimmed.startsWith("/provider ") || trimmed === "/model" || trimmed.startsWith("/model ") || trimmed === "/key" || trimmed.startsWith("/key ")) {
593
+ const r = await showSetupFlow(multiConfig);
594
+ multiConfig = r.multiConfig;
595
+ config = r.config;
596
+ if (r.exit) break;
597
+ continue;
598
+ }
599
+ if (trimmed.startsWith("/")) {
600
+ console.log(GRAY(" \u672A\u77E5\u547D\u4EE4\u3002\u8F93\u5165 / \u8C03\u51FA\u6307\u4EE4\u83DC\u5355\n"));
601
+ continue;
602
+ }
603
+ if (!config.apiKey) {
604
+ console.log(YELLOW(" \u26A0 \u8FD8\u672A\u586B\u5199 API Key\u3002\u8F93\u5165 / \u2192 \u5207\u6362 / \u914D\u7F6E Provider\n"));
605
+ continue;
606
+ }
431
607
  messages.push({ role: "user", content: trimmed });
432
608
  try {
433
609
  messages = await runAgent(messages, config);
434
610
  } catch (err) {
435
- const error = err;
436
- console.error(chalk3.red(`
437
- \u274C Error: ${error.message}`));
438
- if (error.message?.includes("401") || error.message?.includes("Unauthorized")) {
439
- console.error(chalk3.yellow(" Check your API key in ~/.sharkcode/config.toml"));
440
- }
441
- messages = messages.slice(0, -1);
611
+ console.error(RED(`
612
+ \u274C ${String(err)}
613
+ `));
614
+ messages.pop();
442
615
  }
443
616
  }
444
617
  }
445
618
  main().catch((err) => {
446
- console.error(chalk3.red(`
447
- \u274C Fatal: ${err.message}`));
619
+ console.error(chalk3.red(`Fatal: ${String(err)}`));
448
620
  process.exit(1);
449
621
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharkcode",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Local First, open-source AI Coding Agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@ai-sdk/openai": "^3.0.48",
29
+ "@inquirer/prompts": "^8.3.2",
29
30
  "ai": "^6.0.141",
30
31
  "chalk": "^5.6.2",
31
32
  "smol-toml": "^1.6.1",