xitto-kernel 0.3.0 → 0.3.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2
4
+
5
+ - **沒設定就啟動 → 直接進導引**:偵測到沒有 providers.json 且在真實終端時,
6
+ 不再只印提示,而是直接帶進 `init` 設定流程,完成後接續啟動該 pack(非 TTY 仍只給提示)。
7
+
8
+ ## 0.3.1
9
+
10
+ 首次使用導引 —— 不再假設使用者已有 xitto-code。
11
+
12
+ ### 新增
13
+
14
+ - **`xitto-kernel init`**:互動式設定導引,產生 `~/.xitto-code/providers.json`
15
+ - 內建 provider 範本(MiniMax / Anthropic / OpenAI / DeepSeek / 自訂)
16
+ - 引導選 provider → 填 model → 處理 API key(環境變數參照 `${NAME}` 不落地,或內嵌)
17
+ - 既有設定不覆寫;`--force` 合併新 provider
18
+ - pipe-safe 逐行讀取(可 `echo answers | xitto-kernel init` 腳本化)
19
+ - **沒設定就啟動**:改丟明確提示,引導跑 `xitto-kernel init`(不再叫人去找 xitto-code 的範例檔)
20
+ - README 快速開始改為 安裝 → `init` → 啟動 三步
21
+
3
22
  ## 0.3.0
4
23
 
5
24
  把底座的「能力」與「體驗」補到接近 Claude Code,並擴充領域 pack 與評測。
package/README.md CHANGED
@@ -23,20 +23,26 @@ xitto-code 經掃描後,約 **8 成已是領域無關的 kernel**;真正跟
23
23
 
24
24
  ## 快速開始
25
25
 
26
- **前置需求**
27
- - Node.js ≥ 20
28
- - `~/.xitto-code/providers.json` —— LLM provider 設定(與 xitto-code 共用,內含 API key)。
29
- 沒有的話,複製一份填入 key 即可(格式見 xitto-code 的 `providers.example.json`)。
26
+ **前置需求**:Node.js ≥ 20
30
27
 
31
- **安裝**(已發佈 npm)
28
+ **1. 安裝**(已發佈 npm)
32
29
  ```bash
33
30
  npm install -g xitto-kernel # 全域命令 xitto-kernel
34
31
  ```
35
32
  > 開發本倉庫:`cd xitto-kernel && npm install && npm link`。
36
33
 
37
- **跑內建 pack(互動 CLI)**
34
+ **2. 首次設定**(互動導引,產生 `~/.xitto-code/providers.json`)
35
+ ```bash
36
+ xitto-kernel init
37
+ ```
38
+ 引導你選 provider(MiniMax / Anthropic / OpenAI / DeepSeek / 自訂)→ 填 model →
39
+ 設定 API key(建議用環境變數參照 `${NAME}`,金鑰不落地)。已是 xitto-code 使用者可直接共用既有設定、跳過此步。
40
+ (沒設定就啟動會提示你跑 `init`;既有設定不會被覆寫,`--force` 才會合併新 provider。)
41
+
42
+ **3. 跑內建 pack(互動 CLI)**
38
43
  ```bash
39
44
  xitto-kernel # coding agent(讀寫檔案、跑命令)
45
+ xitto-kernel --tui # 完整 Ink TUI(持久狀態列、串流、Esc 中斷;需真實終端)
40
46
  xitto-kernel --pack notes # 筆記 / 知識庫 agent
41
47
  xitto-kernel --pack data-query
42
48
  xitto-kernel --sandbox # 啟動就開 Seatbelt 沙箱
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xitto-kernel",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "領域無關的 agent 底座(kernel + 可插拔 DomainPack),從 xitto-code 抽象而來",
6
6
  "keywords": [
@@ -0,0 +1,134 @@
1
+ // 首次啟動導引:互動式產生 ~/.xitto-code/providers.json。
2
+ // 內建常見 provider 範本(MiniMax / Anthropic / OpenAI / DeepSeek / 自訂),
3
+ // 引導選 provider → 填 model → 處理 API key(環境變數或內嵌),寫檔不覆寫既有設定。
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join, dirname } from 'node:path';
7
+ import { createInterface } from 'node:readline';
8
+
9
+ const e = (n) => (s) => `\x1b[${n}m${s}\x1b[0m`;
10
+ const green = e(32); const gray = e(90); const cyan = e(36); const yellow = e(33); const bold = e(1); const red = e(31);
11
+
12
+ // provider 範本(沿用實測可用的格式;baseUrl/api/model 皆可在引導中改)
13
+ const PRESETS = {
14
+ minimax: { label: 'MiniMax(M2.7,anthropic 相容)', api: 'anthropic-messages', baseUrl: 'https://api.minimaxi.com/anthropic', env: 'MINIMAX_API_KEY', model: { id: 'MiniMax-M2.7', name: 'MiniMax M2.7', contextWindow: 1000192, maxTokens: 131072 } },
15
+ anthropic: { label: 'Anthropic Claude', api: 'anthropic-messages', baseUrl: 'https://api.anthropic.com', env: 'ANTHROPIC_API_KEY', model: { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', contextWindow: 200000, maxTokens: 64000 } },
16
+ openai: { label: 'OpenAI', api: 'openai-completions', baseUrl: 'https://api.openai.com/v1', env: 'OPENAI_API_KEY', model: { id: 'gpt-4o', name: 'GPT-4o', contextWindow: 128000, maxTokens: 16384 } },
17
+ deepseek: { label: 'DeepSeek', api: 'openai-completions', baseUrl: 'https://api.deepseek.com', env: 'DEEPSEEK_API_KEY', model: { id: 'deepseek-chat', name: 'DeepSeek Chat', contextWindow: 64000, maxTokens: 8192 } },
18
+ custom: { label: '自訂(手動填全部)', api: 'openai-completions', baseUrl: '', env: 'LLM_API_KEY', model: { id: '', name: '', contextWindow: 32000, maxTokens: 4096 } },
19
+ };
20
+
21
+ const PATH = () => process.env.XITTO_CODE_CONFIG || join(homedir(), '.xitto-code', 'providers.json');
22
+
23
+ export { PRESETS };
24
+
25
+ // 純函數:把答案組成 providers.json 結構(合併既有 providers)。互動殼與測試共用。
26
+ export function buildConfig(a, existing) {
27
+ const cfg = existing && existing.providers ? existing : { defaultModel: a.modelId, providers: {} };
28
+ cfg.providers = cfg.providers || {};
29
+ cfg.providers[a.providerName] = {
30
+ baseUrl: a.baseUrl, apiKey: a.apiKey, api: a.api,
31
+ models: [{ id: a.modelId, name: a.modelName || a.modelId, contextWindow: a.contextWindow, maxTokens: a.maxTokens }],
32
+ };
33
+ cfg.defaultModel = a.modelId;
34
+ return cfg;
35
+ }
36
+
37
+ // pipe-safe 逐行讀取:把 'line' 事件排入佇列,即使管線一次送進整批輸入也能依序消費
38
+ // (readline/promises 的 question() 在非 TTY 管線下會丟失已緩衝的行)。
39
+ function makeAsker() {
40
+ const rl = createInterface({ input: process.stdin });
41
+ const queue = []; const waiters = []; let closed = false;
42
+ rl.on('line', (l) => { const w = waiters.shift(); if (w) w(l); else queue.push(l); });
43
+ rl.on('close', () => { closed = true; while (waiters.length) waiters.shift()(null); });
44
+ const nextLine = () => new Promise((res) => { if (queue.length) res(queue.shift()); else if (closed) res(null); else waiters.push(res); });
45
+ return {
46
+ close: () => rl.close(),
47
+ ask: async (q, def) => {
48
+ process.stdout.write(gray(q + (def ? ` [${def}]` : '') + ' '));
49
+ const line = await nextLine();
50
+ return ((line == null ? '' : line).trim()) || def || '';
51
+ },
52
+ };
53
+ }
54
+
55
+ export async function runInit(argv = []) {
56
+ const force = argv.includes('--force');
57
+ const path = PATH();
58
+ const io = makeAsker();
59
+ const rl = { close: io.close };
60
+ const ask = io.ask;
61
+
62
+ try {
63
+ console.log('\n' + bold('🚀 xitto-kernel 首次設定') + gray(' —— 建立 LLM provider 設定'));
64
+ console.log(gray(`設定檔:${path}\n`));
65
+
66
+ // 既有檔保護
67
+ if (existsSync(path) && !force) {
68
+ console.log(yellow(`已存在設定檔。`) + gray(' 用 `xitto-kernel init --force` 覆寫,或直接編輯該檔。'));
69
+ let cfg; try { cfg = JSON.parse(readFileSync(path, 'utf8')); } catch { /* 壞檔忽略 */ }
70
+ if (cfg) {
71
+ const names = Object.keys(cfg.providers || {});
72
+ console.log(gray(` 目前 provider:${names.join(', ') || '(無)'} 預設 model:${cfg.defaultModel || '(未設)'}`));
73
+ }
74
+ rl.close();
75
+ return;
76
+ }
77
+
78
+ // 1) 選 provider
79
+ const keys = Object.keys(PRESETS);
80
+ console.log(bold('1) 選 LLM provider:'));
81
+ keys.forEach((k, i) => console.log(` ${cyan(String(i + 1))}. ${PRESETS[k].label}`));
82
+ const pick = await ask('輸入編號', '1');
83
+ const presetKey = keys[(parseInt(pick, 10) || 1) - 1] || 'minimax';
84
+ const preset = PRESETS[presetKey];
85
+ console.log(green(` → ${preset.label}\n`));
86
+
87
+ // 2) 連線 / model
88
+ console.log(bold('2) 連線與 model:'));
89
+ const providerName = await ask('provider 名稱(providers.json 的鍵)', presetKey);
90
+ const baseUrl = await ask('baseUrl', preset.baseUrl);
91
+ const api = await ask('api 型別(anthropic-messages | openai-completions)', preset.api);
92
+ const modelId = await ask('model id', preset.model.id);
93
+ const modelName = await ask('model 顯示名', preset.model.name || modelId);
94
+ const contextWindow = parseInt(await ask('contextWindow', String(preset.model.contextWindow)), 10) || preset.model.contextWindow;
95
+ const maxTokens = parseInt(await ask('maxTokens', String(preset.model.maxTokens)), 10) || preset.model.maxTokens;
96
+ if (!modelId) { console.log(red('\nmodel id 不可空,已取消。')); rl.close(); return; }
97
+
98
+ // 3) API key:環境變數(建議)或內嵌
99
+ console.log('\n' + bold('3) API key:'));
100
+ console.log(gray(' a) 用環境變數參照(建議,金鑰不落地在設定檔)'));
101
+ console.log(gray(' b) 現在貼上金鑰(直接存進設定檔,檔案在你家目錄)'));
102
+ const mode = (await ask('選 a 或 b', 'a')).toLowerCase();
103
+ let apiKey; let envHint = '';
104
+ if (mode === 'b') {
105
+ const k = await ask('貼上 API key', '');
106
+ apiKey = k;
107
+ if (!k) console.log(yellow(' (未填,稍後請手動補上 apiKey)'));
108
+ } else {
109
+ const envName = await ask('環境變數名', preset.env);
110
+ apiKey = '${' + envName + '}';
111
+ envHint = envName;
112
+ }
113
+
114
+ // 組設定(保留既有 providers,--force 時合併)
115
+ let existing; if (existsSync(path)) { try { existing = JSON.parse(readFileSync(path, 'utf8')); } catch { /* 壞檔重建 */ } }
116
+ const cfg = buildConfig({ providerName, baseUrl, api, modelId, modelName, contextWindow, maxTokens, apiKey }, existing);
117
+
118
+ mkdirSync(dirname(path), { recursive: true });
119
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
120
+
121
+ console.log('\n' + green('✓ 已寫入 ') + path);
122
+ if (envHint) {
123
+ console.log('\n' + bold('下一步:設定環境變數(金鑰)'));
124
+ console.log(gray(` export ${envHint}="你的金鑰" # 加進 ~/.zshrc 永久生效`));
125
+ }
126
+ console.log('\n' + bold('啟動:'));
127
+ console.log(green(' xitto-kernel') + gray(' # coding pack,互動對話'));
128
+ console.log(green(' xitto-kernel --tui') + gray(' # 完整 Ink TUI(真實終端)'));
129
+ console.log(green(' xitto-kernel --pack general') + gray(' # 通用 agent'));
130
+ console.log('');
131
+ } finally {
132
+ rl.close();
133
+ }
134
+ }
package/src/app/main.js CHANGED
@@ -5,6 +5,7 @@ import { loadModel } from './providers.js';
5
5
  import { runCli } from './cli.js';
6
6
  import { runTui } from './tui-run.js';
7
7
  import { newAgent } from './scaffold.js';
8
+ import { runInit } from './init.js';
8
9
  import { createKernel } from '../kernel/index.js';
9
10
  import { loadMcpTools } from '../kernel/mcp.js';
10
11
  import { createCodingPack } from '../packs/coding/index.js';
@@ -27,6 +28,9 @@ const PACKS = {
27
28
  };
28
29
 
29
30
  export async function main(argv = process.argv.slice(2)) {
31
+ // 子指令:init —— 首次設定導引,產生 providers.json
32
+ if (argv[0] === 'init') { await runInit(argv.slice(1)); return; }
33
+
30
34
  // 子指令:new-agent <name> —— 產出獨立 agent 專案(不碰 kernel)
31
35
  if (argv[0] === 'new-agent') {
32
36
  const name = argv.find((a, i) => i >= 1 && !a.startsWith('--'));
@@ -50,7 +54,26 @@ export async function main(argv = process.argv.slice(2)) {
50
54
 
51
55
  let model, getApiKey;
52
56
  try { ({ model, getApiKey } = loadModel(opts.model)); }
53
- catch (err) { console.error('\x1b[31m' + err.message + '\x1b[0m'); process.exit(1); }
57
+ catch (err) {
58
+ // 沒設定 + 真實終端:直接帶進設定導引,完成後續跑;非 TTY 才只給提示
59
+ if (err.noConfig && process.stdin.isTTY) {
60
+ console.log(cyan('首次使用,沒找到 providers.json —— 進入設定導引。') + gray('(按 Ctrl+C 取消)'));
61
+ await runInit([]);
62
+ try { ({ model, getApiKey } = loadModel(opts.model)); }
63
+ catch (err2) {
64
+ console.error(red(err2.message));
65
+ console.error(gray(err2.noConfig ? '(未完成設定,已取消)' : '(設定好像缺東西,可編輯該檔或重跑 `xitto-kernel init`)'));
66
+ process.exit(1);
67
+ }
68
+ } else {
69
+ console.error(red(err.message));
70
+ if (err.noConfig) {
71
+ console.error('\n' + cyan('首次使用?') + ' 跑一次設定導引:');
72
+ console.error(green(' xitto-kernel init') + gray(' # 選 provider、填 model、設定 API key'));
73
+ }
74
+ process.exit(1);
75
+ }
76
+ }
54
77
 
55
78
  // MCP:啟動時連 .xitto-kernel/<pack>/mcp.json 的 server,工具以 extraTools 注入
56
79
  const cwd = process.cwd();
@@ -112,6 +135,7 @@ function printHelp() {
112
135
  'xitto-kernel — 領域無關 agent 底座',
113
136
  '',
114
137
  '用法:',
138
+ ' xitto-kernel init 首次設定導引(產生 providers.json)',
115
139
  ' xitto-kernel [--pack <name>] [--model <id>] [--sandbox] [--resume [id]] [--yes] 互動跑內建 pack',
116
140
  ' xitto-kernel --pack general --goal "..." [--yes] 目標驅動自主循環(headless)',
117
141
  ' xitto-kernel new-agent <name> 產出依賴 kernel 的獨立 agent 專案',
@@ -125,7 +149,7 @@ function printHelp() {
125
149
  ' --yes, -y 自動核准 mutating 工具(headless / 自主循環常用)',
126
150
  ' --help 顯示說明',
127
151
  '',
128
- '需要 ~/.xitto-code/providers.json(與 xitto-code 共用)。',
152
+ '首次使用先跑 `xitto-kernel init` 建立 ~/.xitto-code/providers.json(已是 xitto-code 使用者可直接共用)。',
129
153
  'new-agent 產出的是獨立專案,import xitto-kernel 而非修改它——升級不固化。',
130
154
  ].join('\n'));
131
155
  }
@@ -7,7 +7,7 @@ import { join } from 'node:path';
7
7
  const DEFAULT_PATH = () => process.env.XITTO_CODE_CONFIG || join(homedir(), '.xitto-code', 'providers.json');
8
8
 
9
9
  export function loadProvidersConfig(path = DEFAULT_PATH()) {
10
- if (!existsSync(path)) throw new Error(`找不到 providers.json:${path}\n(可複用 xitto-code ~/.xitto-code/providers.json)`);
10
+ if (!existsSync(path)) { const err = new Error(`找不到 providers.json:${path}`); err.noConfig = true; throw err; }
11
11
  return { ...JSON.parse(readFileSync(path, 'utf8')), path };
12
12
  }
13
13