xitto-kernel 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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/bin/xitto-kernel.js +3 -0
- package/docs/01-architecture.md +105 -0
- package/docs/02-domain-pack-spec.md +109 -0
- package/docs/03-kernel-contract.md +79 -0
- package/docs/04-migration-from-xitto-code.md +70 -0
- package/docs/05-example-packs.md +95 -0
- package/docs/06-authoring-a-pack.md +86 -0
- package/package.json +55 -0
- package/src/app/cli.js +243 -0
- package/src/app/index.js +5 -0
- package/src/app/main.js +87 -0
- package/src/app/markdown.js +36 -0
- package/src/app/providers.js +39 -0
- package/src/app/scaffold.js +40 -0
- package/src/app/templates/README.md.tmpl +32 -0
- package/src/app/templates/gitignore.tmpl +4 -0
- package/src/app/templates/index.js.tmpl +7 -0
- package/src/app/templates/pack.js.tmpl +32 -0
- package/src/app/templates/package.json.tmpl +12 -0
- package/src/index.js +15 -0
- package/src/kernel/agent-loop.js +285 -0
- package/src/kernel/compaction.js +65 -0
- package/src/kernel/guard-chain.js +42 -0
- package/src/kernel/hooks.js +47 -0
- package/src/kernel/index.js +291 -0
- package/src/kernel/mcp.js +54 -0
- package/src/kernel/memory.js +45 -0
- package/src/kernel/pack-loader.js +45 -0
- package/src/kernel/provider.js +20 -0
- package/src/kernel/security/allow.js +41 -0
- package/src/kernel/security/danger.js +37 -0
- package/src/kernel/security/permission-step.js +63 -0
- package/src/kernel/security/sandbox.js +111 -0
- package/src/kernel/session.js +36 -0
- package/src/kernel/skills.js +37 -0
- package/src/kernel/subagent.js +46 -0
- package/src/kernel/tool-registry.js +45 -0
- package/src/packs/coding/index.js +157 -0
- package/src/packs/data-query/index.js +67 -0
- package/src/packs/notes/index.js +79 -0
- package/src/types.js +79 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# 06 · 怎麼用底座做一個新領域 agent
|
|
2
|
+
|
|
3
|
+
一句話:**寫一個 DomainPack(最少 3 欄)→ 註冊到 CLI → 跑**。kernel 與其他 pack 零改動。
|
|
4
|
+
|
|
5
|
+
## 最小可跑(3 個必填欄位)
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// src/packs/hello/index.js
|
|
9
|
+
export function createHelloPack() {
|
|
10
|
+
return {
|
|
11
|
+
name: 'hello',
|
|
12
|
+
systemPrompt: '你是友善的助手,用繁體中文回答。',
|
|
13
|
+
tools: () => [{
|
|
14
|
+
name: 'now', label: '時間', description: '回傳目前時間', readOnly: true,
|
|
15
|
+
parameters: { type: 'object', properties: {} },
|
|
16
|
+
execute: async () => ({ content: [{ type: 'text', text: new Date().toISOString() }] }),
|
|
17
|
+
}],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
這樣就是一個能跑的 agent 了。`createKernel(createHelloPack(), { model, getApiKey })` → `runTurn(...)`。
|
|
23
|
+
|
|
24
|
+
## 工具長什麼樣(kernel 的唯一要求)
|
|
25
|
+
|
|
26
|
+
kernel 不在乎工具做什麼,只要這個形狀(**metadata 決定安全行為**):
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
{
|
|
30
|
+
name: 'do_thing',
|
|
31
|
+
label: '顯示名',
|
|
32
|
+
description: '給模型看的用途說明(寫清楚,模型靠它決定何時呼叫)',
|
|
33
|
+
parameters: { /* JSON Schema */ },
|
|
34
|
+
execute: async (id, params, signal, onUpdate, services) => ({ content: [{ type: 'text', text: '...' }] }),
|
|
35
|
+
|
|
36
|
+
// ── metadata:決定 kernel 怎麼對待它 ──
|
|
37
|
+
readOnly: true, // 唯讀 → 守衛鏈自動放行、不問權限
|
|
38
|
+
mutating: true, // 會改動 → 計劃模式擋它、自動算進 mutatingTools
|
|
39
|
+
sandboxable: true, // 走 shell → 沙箱開時自動包進 Seatbelt(只對有 params.command 的工具有意義)
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 六個選填插槽(要更像「專業 agent」時才填)
|
|
44
|
+
|
|
45
|
+
| 插槽 | 用途 | 範例 |
|
|
46
|
+
|------|------|------|
|
|
47
|
+
| `contextFiles` | 啟動載入的領域規範檔 | `['NOTES.md']` |
|
|
48
|
+
| `mutatingTools` | 顯式指定哪些算改動(不給就從 `tool.mutating` 推) | `['add_note']` |
|
|
49
|
+
| `verify` | 每輪收尾自我驗收 | 連結檢查 / 測試 |
|
|
50
|
+
| `preToolPolicy` | 工具前領域守衛(守衛鏈第 3 格) | 「add 前必先 search」 |
|
|
51
|
+
| `permissionPolicy` | 沙箱/deny 預設 | `{ deny: ['bash:DROP'] }` |
|
|
52
|
+
| `memoryGuide` | 何時主動存記憶的提示 | 領域特化提示 |
|
|
53
|
+
|
|
54
|
+
> `preToolPolicy` 是領域「規矩」的所在:編碼是 read-before-edit、資料是 schema-before-query、
|
|
55
|
+
> 筆記是 search-before-add ——**同一個插槽,不同領域填不同規矩**。
|
|
56
|
+
|
|
57
|
+
## 三步驟:從零到能用
|
|
58
|
+
|
|
59
|
+
### 1) 寫 pack
|
|
60
|
+
`src/packs/<你的領域>/index.js`,export 一個 `create<Name>Pack({ cwd })`。
|
|
61
|
+
|
|
62
|
+
### 2) 註冊到 CLI
|
|
63
|
+
`src/app/main.js` 的 `PACKS` 加一行:
|
|
64
|
+
```js
|
|
65
|
+
import { createNotesPack } from '../packs/notes/index.js';
|
|
66
|
+
const PACKS = { coding: createCodingPack, 'data-query': createDataQueryPack, notes: createNotesPack };
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3) 跑
|
|
70
|
+
```bash
|
|
71
|
+
npm start -- --pack notes # 互動 CLI
|
|
72
|
+
npm start -- --pack notes --sandbox # 要沙箱就加 --sandbox
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 該把什麼放進工具 vs prompt?
|
|
76
|
+
|
|
77
|
+
- **工具** = agent 能對世界做的動作(查 DB、發 API、寫檔、跑命令)。要副作用或要拿外部資料 → 工具。
|
|
78
|
+
- **systemPrompt** = 行為準則、口吻、流程規矩、何時用哪個工具。純推理/格式 → prompt。
|
|
79
|
+
- **preToolPolicy** = 硬性前置條件(程式擋,不靠模型自律)。「沒做 X 不准做 Y」→ 守衛。
|
|
80
|
+
|
|
81
|
+
## 不用碰的東西(kernel 免費給你)
|
|
82
|
+
|
|
83
|
+
接上任何 pack,自動獲得:多步工具循環、串流、權限/沙箱(含 Seatbelt)、危險命令偵測、
|
|
84
|
+
mutatingTools 推導、計劃模式、Ctrl+C 中斷、多輪歷史、CLI。**領域作者只寫「會什麼、守什麼」。**
|
|
85
|
+
|
|
86
|
+
完整範例見 `src/packs/notes/`(知識庫 agent),對照 `coding` / `data-query`。
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xitto-kernel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "領域無關的 agent 底座(kernel + 可插拔 DomainPack),從 xitto-code 抽象而來",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent",
|
|
8
|
+
"llm",
|
|
9
|
+
"ai-agent",
|
|
10
|
+
"agent-framework",
|
|
11
|
+
"domain-pack",
|
|
12
|
+
"sandbox",
|
|
13
|
+
"seatbelt",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "ishoplus",
|
|
18
|
+
"homepage": "https://github.com/ishoplus/xitto-kernel#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/ishoplus/xitto-kernel.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/ishoplus/xitto-kernel/issues"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"xitto-kernel": "bin/xitto-kernel.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"bin",
|
|
35
|
+
"docs",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test",
|
|
41
|
+
"demo": "node examples/demo.js",
|
|
42
|
+
"start": "node bin/xitto-kernel.js"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": "./src/index.js",
|
|
46
|
+
"./app": "./src/app/index.js",
|
|
47
|
+
"./packs/coding": "./src/packs/coding/index.js",
|
|
48
|
+
"./packs/data-query": "./src/packs/data-query/index.js",
|
|
49
|
+
"./packs/notes": "./src/packs/notes/index.js"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@mariozechner/pi-ai": "^0.70.6",
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/app/cli.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// App 層:互動式終端 CLI,消費 kernel.runTurn 的事件流。
|
|
2
|
+
// 這是「kernel + 薄 app」的 app 半部——TUI 不在 kernel 內;更豐富的 Ink 前端可作為另一個
|
|
3
|
+
// app 消費同一組 kernel 事件(證明 kernel/app 分離)。
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
import { createKernel } from '../kernel/index.js';
|
|
6
|
+
import { seatbeltAvailable } from '../kernel/security/sandbox.js';
|
|
7
|
+
import { createStreamRenderer } from './markdown.js';
|
|
8
|
+
|
|
9
|
+
const e = (n) => (s) => `\x1b[${n}m${s}\x1b[0m`;
|
|
10
|
+
const c = { gray: e(90), green: e(32), yellow: e(33), red: e(31), cyan: e(36), bold: e(1), blue: e(34) };
|
|
11
|
+
|
|
12
|
+
const summarize = (args) => {
|
|
13
|
+
const s = JSON.stringify(args ?? {});
|
|
14
|
+
return s.length > 60 ? s.slice(0, 57) + '…' : s;
|
|
15
|
+
};
|
|
16
|
+
const preview = (result) => {
|
|
17
|
+
const t = (result?.content || []).map((x) => x.text || '').join(' ').replace(/\s+/g, ' ').trim();
|
|
18
|
+
return t.length > 70 ? t.slice(0, 67) + '…' : t;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 啟動互動 CLI。
|
|
23
|
+
* @param {Object} o
|
|
24
|
+
* @param {import('../types.js').DomainPack} o.pack
|
|
25
|
+
* @param {object} o.model
|
|
26
|
+
* @param {() => string} o.getApiKey
|
|
27
|
+
* @param {boolean} [o.sandbox] 初始沙箱狀態(預設關)
|
|
28
|
+
*/
|
|
29
|
+
export function runCli({ pack, model, getApiKey, sandbox = false, resume = null, auto = false, extraTools = [], onExit = null }) {
|
|
30
|
+
let sandboxOn = !!sandbox;
|
|
31
|
+
let autoApprove = !!auto;
|
|
32
|
+
let planMode = false;
|
|
33
|
+
const kernel = createKernel(pack, {
|
|
34
|
+
model, getApiKey, extraTools,
|
|
35
|
+
sandbox: { enabled: sandboxOn }, // 提供策略(blockNetwork/allowWritePrefixes)
|
|
36
|
+
getSandbox: () => sandboxOn, // on/off 由 CLI 即時切換
|
|
37
|
+
getPlanMode: () => planMode, // 計劃模式:守衛擋 mutating 工具
|
|
38
|
+
confirm: askConfirm, // 互動權限確認(mutating/危險工具執行前)
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let history = [];
|
|
42
|
+
let currentAgent = null;
|
|
43
|
+
let streaming = false;
|
|
44
|
+
|
|
45
|
+
// 互動權限確認:守衛鏈第 5 格對 mutating/危險工具呼叫此函數。autoApprove → 一律放行。
|
|
46
|
+
// 回 'yes'(允許一次)/ 'always'(此工具全部)/ 'no'(拒絕)。
|
|
47
|
+
async function askConfirm(name, args, danger) {
|
|
48
|
+
if (autoApprove && !danger) return 'yes'; // 自動模式仍對危險命令把關
|
|
49
|
+
endStream();
|
|
50
|
+
return new Promise((res) => {
|
|
51
|
+
const warn = danger ? c.red(` ⛔ 危險:${danger}\n`) : '';
|
|
52
|
+
out(warn + c.yellow(' 需要許可 ') + c.bold(name) + c.gray('(' + summarize(args) + ')') + '\n');
|
|
53
|
+
const hint = danger ? '[y]允許一次 [n]拒絕' : '[y]允許 [a]此工具全部 [n]拒絕';
|
|
54
|
+
try {
|
|
55
|
+
rl.question(c.yellow(` ${hint} › `), (ans) => {
|
|
56
|
+
const a = (ans || '').trim().toLowerCase();
|
|
57
|
+
if (danger) return res(a === 'y' || a === 'yes' ? 'yes' : 'no');
|
|
58
|
+
res(a === 'y' || a === 'yes' ? 'yes' : a === 'a' ? 'always' : 'no');
|
|
59
|
+
});
|
|
60
|
+
} catch { res('no'); }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// session:續接(--resume [id])或開新;每輪結束自動存檔
|
|
65
|
+
let sessionId = kernel.session.newId();
|
|
66
|
+
let resumedNote = '';
|
|
67
|
+
if (resume) {
|
|
68
|
+
const data = resume === true ? (kernel.session.latest() && kernel.session.load(kernel.session.latest().id)) : kernel.session.load(resume);
|
|
69
|
+
if (data?.messages?.length) { history = data.messages; sessionId = data.id; resumedNote = `(續接 ${data.id},${data.messages.length} 則)`; }
|
|
70
|
+
else resumedNote = resume === true ? '(找不到可續的對話,開新)' : `(找不到 session "${resume}",開新)`;
|
|
71
|
+
}
|
|
72
|
+
const persist = () => { try { kernel.session.save(sessionId, history); } catch { /* 略 */ } };
|
|
73
|
+
|
|
74
|
+
const out = (s) => process.stdout.write(s);
|
|
75
|
+
const md = createStreamRenderer(out); // 串流 markdown 渲染(粗體/標題/code/inline code)
|
|
76
|
+
const endStream = () => { if (md.active()) md.flush(); streaming = false; };
|
|
77
|
+
|
|
78
|
+
// edit/write 的彩色 diff 預覽(紅 - 舊 / 綠 + 新)
|
|
79
|
+
const diffPreview = (name, args) => {
|
|
80
|
+
if (name === 'edit' && args?.oldText != null) {
|
|
81
|
+
for (const l of String(args.oldText).split('\n').slice(0, 8)) out(c.red(' - ' + l) + '\n');
|
|
82
|
+
for (const l of String(args.newText ?? '').split('\n').slice(0, 8)) out(c.green(' + ' + l) + '\n');
|
|
83
|
+
} else if (name === 'write' && args?.content != null) {
|
|
84
|
+
const lines = String(args.content).split('\n');
|
|
85
|
+
for (const l of lines.slice(0, 8)) out(c.green(' + ' + l) + '\n');
|
|
86
|
+
if (lines.length > 8) out(c.gray(` … 共 ${lines.length} 行`) + '\n');
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const onEvent = (ev) => {
|
|
91
|
+
switch (ev.type) {
|
|
92
|
+
case 'message_update': {
|
|
93
|
+
const a = ev.assistantMessageEvent;
|
|
94
|
+
if (a?.type === 'text_delta' && a.delta) { md.push(a.delta); streaming = true; }
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'tool_execution_start':
|
|
98
|
+
endStream();
|
|
99
|
+
out(c.yellow('⚙ ' + ev.toolName) + c.gray('(' + summarize(ev.args) + ')\n'));
|
|
100
|
+
diffPreview(ev.toolName, ev.args);
|
|
101
|
+
break;
|
|
102
|
+
case 'tool_execution_end':
|
|
103
|
+
out((ev.isError ? c.red(' ⎿ ✗') : c.gray(' ⎿ ✓')) + c.gray(' ' + preview(ev.result)) + '\n');
|
|
104
|
+
break;
|
|
105
|
+
case 'verify_start':
|
|
106
|
+
endStream();
|
|
107
|
+
out(c.gray(' 🔎 自動驗收…\n'));
|
|
108
|
+
break;
|
|
109
|
+
case 'verify_end':
|
|
110
|
+
out(ev.ok ? c.gray(' ✓ 驗收通過\n') : c.yellow(' ✗ 驗收失敗,請 agent 修正…\n'));
|
|
111
|
+
break;
|
|
112
|
+
case 'hook_fail':
|
|
113
|
+
out(c.yellow(` ✗ hook 失敗 ${ev.command},回灌讓 agent 修正…\n`));
|
|
114
|
+
break;
|
|
115
|
+
case 'compact':
|
|
116
|
+
endStream();
|
|
117
|
+
out(c.gray(` ⊙ 已壓縮上下文:${ev.tokensBefore}→${ev.tokensAfter} tokens(摘要 ${ev.summarized} 則,保留 ${ev.kept} 則)\n`));
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// 斜線指令;回 true 表示已處理(不送 LLM)
|
|
123
|
+
const handleSlash = (input) => {
|
|
124
|
+
const [cmd, arg] = input.trim().split(/\s+/);
|
|
125
|
+
switch (cmd) {
|
|
126
|
+
case '/exit': case '/quit': cleanup(); process.exit(0); return true;
|
|
127
|
+
case '/help':
|
|
128
|
+
out(c.gray([
|
|
129
|
+
' /help 說明',
|
|
130
|
+
' /sandbox [on|off] 切換沙箱(macOS=Seatbelt 真隔離)',
|
|
131
|
+
' /auto [on|off] 自動核准 mutating 工具(危險命令仍把關)',
|
|
132
|
+
' /plan [on|off] 計劃模式(只規劃、擋下實際改動)',
|
|
133
|
+
' /undo 撤銷上一次檔案改動(write/edit)',
|
|
134
|
+
' /tools 列出此 pack 的工具',
|
|
135
|
+
' /memory 顯示跨 session 記憶',
|
|
136
|
+
' /sessions 列出已保存的對話',
|
|
137
|
+
' /resume [id] 續接對話(不給 id=最近一次)',
|
|
138
|
+
' /clear 清除歷史(開新 session)',
|
|
139
|
+
' /exit 離開',
|
|
140
|
+
].join('\n') + '\n'));
|
|
141
|
+
return true;
|
|
142
|
+
case '/memory': {
|
|
143
|
+
const mems = kernel.memory.list();
|
|
144
|
+
out(mems.length ? c.gray(mems.map((m) => ' • ' + m).join('\n') + '\n') : c.gray('(尚無記憶)\n'));
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
case '/sessions': {
|
|
148
|
+
const ss = kernel.session.list();
|
|
149
|
+
out(ss.length
|
|
150
|
+
? c.gray(ss.map((s) => ` ${s.id} [${s.count} 則${s.model ? ' ' + s.model : ''}]`).join('\n') + '\n')
|
|
151
|
+
: c.gray('(尚無保存的對話)\n'));
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
case '/resume': {
|
|
155
|
+
const target = arg || (kernel.session.latest()?.id);
|
|
156
|
+
const data = target ? kernel.session.load(target) : null;
|
|
157
|
+
if (data?.messages?.length) {
|
|
158
|
+
history = data.messages; sessionId = data.id;
|
|
159
|
+
out(c.gray(`(已續接 ${data.id},${data.messages.length} 則)\n`));
|
|
160
|
+
} else out(c.yellow(`找不到可續接的 session${arg ? ` "${arg}"` : ''}\n`));
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
case '/sandbox': {
|
|
164
|
+
sandboxOn = arg ? arg === 'on' : !sandboxOn;
|
|
165
|
+
const real = seatbeltAvailable();
|
|
166
|
+
out(sandboxOn
|
|
167
|
+
? c.yellow(`🔒 沙箱開${real ? '(macOS Seatbelt OS 級隔離)' : '(靜態策略;此平台無 Seatbelt)'}\n`)
|
|
168
|
+
: c.gray('沙箱關\n'));
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
case '/auto':
|
|
172
|
+
autoApprove = arg ? arg === 'on' : !autoApprove;
|
|
173
|
+
out(autoApprove ? c.yellow('⚡ 自動核准開(mutating 工具不再逐一確認;危險命令仍把關)\n') : c.gray('自動核准關(mutating 工具會逐一確認)\n'));
|
|
174
|
+
return true;
|
|
175
|
+
case '/plan':
|
|
176
|
+
planMode = arg ? arg === 'on' : !planMode;
|
|
177
|
+
out(planMode ? c.cyan('📋 計劃模式開(只規劃、擋下 write/edit/bash)\n') : c.gray('計劃模式關\n'));
|
|
178
|
+
return true;
|
|
179
|
+
case '/undo': {
|
|
180
|
+
const r = kernel.undo();
|
|
181
|
+
out(r.undone ? c.gray(`↩ 已撤銷 ${r.path}${r.created ? '(刪除新建檔)' : ''}\n`) : c.yellow(`${r.reason}\n`));
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
case '/tools':
|
|
185
|
+
out(c.gray(kernel.registry.all().map((t) =>
|
|
186
|
+
` ${t.name}${t.readOnly ? c.gray(' [唯讀]') : ''}${t.mutating ? c.yellow(' [mutating]') : ''}${t.sandboxable ? c.cyan(' [sandboxable]') : ''}`,
|
|
187
|
+
).join('\n') + '\n'));
|
|
188
|
+
return true;
|
|
189
|
+
case '/clear': history = []; sessionId = kernel.session.newId(); out(c.gray('(已清除歷史,開新 session)\n')); return true;
|
|
190
|
+
default:
|
|
191
|
+
if (cmd.startsWith('/')) { out(c.red(`未知指令 ${cmd}(/help)\n`)); return true; }
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: c.blue('› ') });
|
|
197
|
+
let closed = false;
|
|
198
|
+
const cleanup = () => { try { rl.close(); } catch { /* 略 */ } try { onExit?.(); } catch { /* 略 */ } };
|
|
199
|
+
const finish = () => { cleanup(); process.exit(0); };
|
|
200
|
+
rl.on('close', () => { closed = true; }); // 管線輸入結束(非 TTY)→ 收尾
|
|
201
|
+
|
|
202
|
+
// Ctrl+C:執行中→中斷該輪;閒置→離開
|
|
203
|
+
process.on('SIGINT', () => {
|
|
204
|
+
if (currentAgent) { currentAgent.abort(); out(c.yellow('\n⏹ 已中斷本輪\n')); }
|
|
205
|
+
else { out(c.gray('\n再見。\n')); cleanup(); process.exit(0); }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// 橫幅
|
|
209
|
+
out('\n' + c.cyan('✻ ') + c.bold('xitto-kernel') + c.gray(` · ${pack.name} pack · ${model.id}`) + '\n');
|
|
210
|
+
out(c.gray(` 沙箱 ${sandboxOn ? '開' : '關'}${seatbeltAvailable() ? '(Seatbelt 可用)' : ''} · /help 看指令 · Ctrl+C 中斷/離開`) + '\n');
|
|
211
|
+
if (resumedNote) out(c.gray(' ' + resumedNote) + '\n');
|
|
212
|
+
out('\n');
|
|
213
|
+
|
|
214
|
+
const loop = () => {
|
|
215
|
+
if (closed) return finish();
|
|
216
|
+
let q;
|
|
217
|
+
try { q = rl.question.bind(rl); } catch { return finish(); }
|
|
218
|
+
q(c.blue('› '), async (raw) => {
|
|
219
|
+
const input = (raw || '').trim();
|
|
220
|
+
if (!input) return loop();
|
|
221
|
+
if (handleSlash(input)) return loop();
|
|
222
|
+
try {
|
|
223
|
+
const text = planMode
|
|
224
|
+
? `[計劃模式:只制定計劃,列出你打算做的步驟與會改動的檔案,不要實際寫檔或執行命令]\n\n${input}`
|
|
225
|
+
: input;
|
|
226
|
+
const r = await kernel.runTurn(text, {
|
|
227
|
+
history, onEvent, onAgent: (a) => { currentAgent = a; },
|
|
228
|
+
});
|
|
229
|
+
endStream();
|
|
230
|
+
history = r.messages;
|
|
231
|
+
persist(); // 每輪結束自動存檔(可 /resume 續接)
|
|
232
|
+
} catch (err) {
|
|
233
|
+
endStream();
|
|
234
|
+
out(c.red('錯誤:' + err.message) + '\n');
|
|
235
|
+
} finally {
|
|
236
|
+
currentAgent = null;
|
|
237
|
+
}
|
|
238
|
+
out('\n');
|
|
239
|
+
loop();
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
loop();
|
|
243
|
+
}
|
package/src/app/index.js
ADDED
package/src/app/main.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// App 進入點:解析參數 → 載入 model(providers.json)→ 選 pack → 啟動 CLI。
|
|
2
|
+
// 子指令:new-agent <name> → 產出依賴 kernel 的獨立 agent 專案。
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { loadModel } from './providers.js';
|
|
5
|
+
import { runCli } from './cli.js';
|
|
6
|
+
import { newAgent } from './scaffold.js';
|
|
7
|
+
import { loadMcpTools } from '../kernel/mcp.js';
|
|
8
|
+
import { createCodingPack } from '../packs/coding/index.js';
|
|
9
|
+
import { createDataQueryPack } from '../packs/data-query/index.js';
|
|
10
|
+
import { createNotesPack } from '../packs/notes/index.js';
|
|
11
|
+
|
|
12
|
+
const e = (n) => (s) => `\x1b[${n}m${s}\x1b[0m`;
|
|
13
|
+
const green = e(32); const gray = e(90); const red = e(31);
|
|
14
|
+
|
|
15
|
+
const PACKS = {
|
|
16
|
+
coding: createCodingPack,
|
|
17
|
+
'data-query': createDataQueryPack,
|
|
18
|
+
notes: createNotesPack,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
22
|
+
// 子指令:new-agent <name> —— 產出獨立 agent 專案(不碰 kernel)
|
|
23
|
+
if (argv[0] === 'new-agent') {
|
|
24
|
+
const name = argv[1];
|
|
25
|
+
try {
|
|
26
|
+
const { target, files } = newAgent(name);
|
|
27
|
+
console.log(green(`✓ 已建立獨立 agent 專案:${target}`));
|
|
28
|
+
console.log(gray(` ${files.join(' ')}`));
|
|
29
|
+
console.log('\n下一步:');
|
|
30
|
+
console.log(gray(` cd ${name} && npm install && npm start`));
|
|
31
|
+
console.log(gray(' (改 pack.js 換成你的領域;npm update xitto-kernel 升級底座,不固化)'));
|
|
32
|
+
} catch (err) { console.error(red(err.message)); process.exit(1); }
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const opts = parse(argv);
|
|
37
|
+
if (opts.help) return printHelp();
|
|
38
|
+
|
|
39
|
+
const make = PACKS[opts.pack];
|
|
40
|
+
if (!make) { console.error(`未知 pack「${opts.pack}」。可用:${Object.keys(PACKS).join(', ')}`); process.exit(1); }
|
|
41
|
+
|
|
42
|
+
let model, getApiKey;
|
|
43
|
+
try { ({ model, getApiKey } = loadModel(opts.model)); }
|
|
44
|
+
catch (err) { console.error('\x1b[31m' + err.message + '\x1b[0m'); process.exit(1); }
|
|
45
|
+
|
|
46
|
+
// MCP:啟動時連 .xitto-kernel/<pack>/mcp.json 的 server,工具以 extraTools 注入
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
const mcp = await loadMcpTools(join(cwd, '.xitto-kernel', opts.pack, 'mcp.json'), (m) => console.log(gray(` [MCP] ${m}`)));
|
|
49
|
+
|
|
50
|
+
runCli({
|
|
51
|
+
pack: make({ cwd }), model, getApiKey,
|
|
52
|
+
sandbox: opts.sandbox, resume: opts.resume, auto: opts.yes,
|
|
53
|
+
extraTools: mcp.tools, onExit: mcp.close,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parse(argv) {
|
|
58
|
+
const o = { pack: 'coding', model: undefined, sandbox: false, help: false, resume: null, yes: false };
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
if (a === '--help' || a === '-h') o.help = true;
|
|
62
|
+
else if (a === '--pack') o.pack = argv[++i];
|
|
63
|
+
else if (a === '--model') o.model = argv[++i];
|
|
64
|
+
else if (a === '--sandbox') o.sandbox = true;
|
|
65
|
+
else if (a === '--yes' || a === '-y') o.yes = true;
|
|
66
|
+
else if (a === '--resume') { const nxt = argv[i + 1]; if (nxt && !nxt.startsWith('--')) { o.resume = nxt; i++; } else o.resume = true; }
|
|
67
|
+
}
|
|
68
|
+
return o;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function printHelp() {
|
|
72
|
+
console.log([
|
|
73
|
+
'xitto-kernel — 領域無關 agent 底座',
|
|
74
|
+
'',
|
|
75
|
+
'用法:',
|
|
76
|
+
' xitto-kernel [--pack <name>] [--model <id>] [--sandbox] [--resume [id]] [--yes] 互動跑內建 pack',
|
|
77
|
+
' xitto-kernel new-agent <name> 產出依賴 kernel 的獨立 agent 專案',
|
|
78
|
+
'',
|
|
79
|
+
' --pack <name> 選擇內建 DomainPack(coding | data-query | notes;預設 coding)',
|
|
80
|
+
' --model <id> 指定 model(預設用 providers.json 的 defaultModel)',
|
|
81
|
+
' --sandbox 啟動即開啟沙箱(macOS=Seatbelt 真隔離)',
|
|
82
|
+
' --help 顯示說明',
|
|
83
|
+
'',
|
|
84
|
+
'需要 ~/.xitto-code/providers.json(與 xitto-code 共用)。',
|
|
85
|
+
'new-agent 產出的是獨立專案,import xitto-kernel 而非修改它——升級不固化。',
|
|
86
|
+
].join('\n'));
|
|
87
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// 輕量串流 markdown 渲染器(零依賴,逐行)。delta 進 buffer,遇換行就把整行套 ANSI 樣式輸出;
|
|
2
|
+
// 末尾未完成的行在 flush 時輸出。支援:標題、code block(```)、inline code(`)、粗體(**)。
|
|
3
|
+
const C = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', cyan: '\x1b[36m', green: '\x1b[32m' };
|
|
4
|
+
|
|
5
|
+
const inline = (s) => s
|
|
6
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, t) => C.bold + t + C.reset)
|
|
7
|
+
.replace(/`([^`]+)`/g, (_, t) => C.cyan + t + C.reset);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {(s: string) => void} out
|
|
11
|
+
*/
|
|
12
|
+
export function createStreamRenderer(out) {
|
|
13
|
+
let buf = '';
|
|
14
|
+
let inCode = false;
|
|
15
|
+
let firstDone = false;
|
|
16
|
+
|
|
17
|
+
const prefix = () => { if (!firstDone) { firstDone = true; return C.green + '● ' + C.reset; } return ''; };
|
|
18
|
+
|
|
19
|
+
const renderLine = (line) => {
|
|
20
|
+
if (/^\s*```/.test(line)) { inCode = !inCode; out(prefix() + C.dim + line + C.reset + '\n'); return; }
|
|
21
|
+
if (inCode) { out(prefix() + C.cyan + line + C.reset + '\n'); return; }
|
|
22
|
+
const h = line.match(/^(#{1,6})\s+(.*)/);
|
|
23
|
+
if (h) { out(prefix() + C.bold + h[2] + C.reset + '\n'); return; }
|
|
24
|
+
out(prefix() + inline(line) + '\n');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
push: (delta) => {
|
|
29
|
+
buf += delta;
|
|
30
|
+
let nl;
|
|
31
|
+
while ((nl = buf.indexOf('\n')) >= 0) { renderLine(buf.slice(0, nl)); buf = buf.slice(nl + 1); }
|
|
32
|
+
},
|
|
33
|
+
flush: () => { if (buf) { renderLine(buf); buf = ''; } firstDone = false; inCode = false; },
|
|
34
|
+
active: () => firstDone || buf.length > 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// App 層:從 providers.json 載入並組裝 model(沿用 xitto providers.json 格式)。
|
|
2
|
+
// 這是「provider 設定」,屬 app;kernel 本身 provider 無關(只收 model + getApiKey)。
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PATH = () => process.env.XITTO_CODE_CONFIG || join(homedir(), '.xitto-code', 'providers.json');
|
|
8
|
+
|
|
9
|
+
export function loadProvidersConfig(path = DEFAULT_PATH()) {
|
|
10
|
+
if (!existsSync(path)) throw new Error(`找不到 providers.json:${path}\n(可複用 xitto-code 的 ~/.xitto-code/providers.json)`);
|
|
11
|
+
return { ...JSON.parse(readFileSync(path, 'utf8')), path };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildModel(cfg, modelId) {
|
|
15
|
+
const providers = cfg.providers || {};
|
|
16
|
+
const targetId = modelId || cfg.defaultModel;
|
|
17
|
+
for (const [provider, pcfg] of Object.entries(providers)) {
|
|
18
|
+
const m = (pcfg.models || []).find((x) => x.id === targetId);
|
|
19
|
+
if (!m) continue;
|
|
20
|
+
const model = {
|
|
21
|
+
id: m.id, name: m.name || m.id, provider,
|
|
22
|
+
api: pcfg.api || 'openai-completions', baseUrl: pcfg.baseUrl,
|
|
23
|
+
reasoning: m.reasoning || false, input: m.input || ['text'], output: m.output || ['text'],
|
|
24
|
+
cost: m.cost || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
25
|
+
contextWindow: m.contextWindow || 32000, maxTokens: m.maxTokens || 4096,
|
|
26
|
+
cache: m.cache ?? pcfg.cache,
|
|
27
|
+
};
|
|
28
|
+
const apiKey = resolveEnv(pcfg.apiKey || '');
|
|
29
|
+
return { model, getApiKey: () => apiKey };
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`providers.json 找不到 model「${targetId}」`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 一步到位:載入設定 + 組裝指定(或預設)model
|
|
35
|
+
export function loadModel(modelId, path) {
|
|
36
|
+
return buildModel(loadProvidersConfig(path), modelId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resolveEnv = (v) => String(v).replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] || '');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// 腳手架:產出「依賴 xitto-kernel 的獨立 agent 專案」(不修改 kernel,故不固化)。
|
|
2
|
+
// 從 templates/*.tmpl 讀樣板 → 代換 __NAME__ / __KERNEL_PATH__ → 寫到 <dir>/<name>/。
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const KERNEL_ROOT = resolve(HERE, '..', '..'); // src/app/ → 套件根
|
|
9
|
+
const TEMPLATES = join(HERE, 'templates');
|
|
10
|
+
|
|
11
|
+
// [樣板檔, 產出檔名]
|
|
12
|
+
const FILES = [
|
|
13
|
+
['package.json.tmpl', 'package.json'],
|
|
14
|
+
['index.js.tmpl', 'index.js'],
|
|
15
|
+
['pack.js.tmpl', 'pack.js'],
|
|
16
|
+
['README.md.tmpl', 'README.md'],
|
|
17
|
+
['gitignore.tmpl', '.gitignore'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 產生一個獨立 agent 專案。
|
|
22
|
+
* @param {string} name agent 名(字母/數字/連字號)
|
|
23
|
+
* @param {{ dir?: string, kernelPath?: string }} [opts]
|
|
24
|
+
* dir:產出根目錄(預設 cwd);kernelPath:寫進 package.json 的 file: 依賴路徑(預設此 kernel)
|
|
25
|
+
* @returns {{ target: string, files: string[] }}
|
|
26
|
+
*/
|
|
27
|
+
export function newAgent(name, { dir = process.cwd(), kernelPath = KERNEL_ROOT } = {}) {
|
|
28
|
+
if (!name || !/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
|
|
29
|
+
throw new Error(`agent 名稱不合法:「${name}」(只能用字母/數字/連字號,且不以連字號開頭)`);
|
|
30
|
+
}
|
|
31
|
+
const target = join(dir, name);
|
|
32
|
+
if (existsSync(target)) throw new Error(`目錄已存在:${target}`);
|
|
33
|
+
|
|
34
|
+
mkdirSync(target, { recursive: true });
|
|
35
|
+
const subst = (s) => s.replaceAll('__NAME__', name).replaceAll('__KERNEL_PATH__', kernelPath);
|
|
36
|
+
for (const [tmpl, outName] of FILES) {
|
|
37
|
+
writeFileSync(join(target, outName), subst(readFileSync(join(TEMPLATES, tmpl), 'utf8')));
|
|
38
|
+
}
|
|
39
|
+
return { target, files: FILES.map(([, o]) => o) };
|
|
40
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
用 **xitto-kernel** 底座做的 `__NAME__` 領域 agent。
|
|
4
|
+
這份專案只擁有自己的 pack;runtime 由 xitto-kernel 提供,可獨立升級(不固化)。
|
|
5
|
+
|
|
6
|
+
## 跑
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm start # 互動 CLI
|
|
11
|
+
npm start -- --sandbox # 開沙箱(macOS=Seatbelt 真隔離)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
需要 `~/.xitto-code/providers.json`(與 xitto-code / xitto-kernel 共用)。
|
|
15
|
+
|
|
16
|
+
## 改成你的領域
|
|
17
|
+
|
|
18
|
+
編輯 `pack.js`:
|
|
19
|
+
|
|
20
|
+
- `tools` — 加你的領域工具(`readOnly` / `mutating` / `sandboxable` 決定安全行為)
|
|
21
|
+
- `systemPrompt` — 行為準則
|
|
22
|
+
- `preToolPolicy` — 領域硬規矩(選填,例如「做 X 前必先 Y」)
|
|
23
|
+
|
|
24
|
+
完整指南見 xitto-kernel 的 `docs/06-authoring-a-pack.md`。
|
|
25
|
+
|
|
26
|
+
## 升級 kernel
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm update xitto-kernel
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
kernel 修 bug / 加功能都從這裡拿,**這份 agent 不會被固化**。
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// 啟動:xitto-kernel 當依賴。這份專案只擁有「你的 pack + 幾行啟動」。
|
|
2
|
+
// kernel 升級(npm update xitto-kernel)不影響這裡——不會固化。
|
|
3
|
+
import { runCli, loadModel } from 'xitto-kernel/app';
|
|
4
|
+
import { createPack } from './pack.js';
|
|
5
|
+
|
|
6
|
+
const { model, getApiKey } = loadModel(); // 讀 ~/.xitto-code/providers.json
|
|
7
|
+
runCli({ pack: createPack({ cwd: process.cwd() }), model, getApiKey });
|