xitto-kernel 0.2.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.
@@ -0,0 +1,30 @@
1
+ // devops/SRE pack — 維運自動化 agent。shell 為主 + 後台服務 + 設定檔 + 日誌搜尋 + 健康檢查。
2
+ // 工具全由共用模組組成(fs-tools / code-nav / web-tools / bg)。
3
+ import { createFsTools } from '../shared/fs-tools.js';
4
+ import { createGrepTool, createGlobTool } from '../shared/code-nav.js';
5
+ import { createHttpTool } from '../shared/web-tools.js';
6
+ import { createBackgroundTools } from '../../kernel/bg.js';
7
+
8
+ const SYSTEM_PROMPT = [
9
+ '你是 DevOps/SRE agent,做系統維運與自動化。準則:',
10
+ '- 改動前先看現況(read 設定、bash 查狀態、grep 日誌),不要盲改。',
11
+ '- 操作盡量「冪等」(重跑不會壞);寫腳本記得處理錯誤與權限。',
12
+ '- 長時間/常駐服務(dev server、watch、daemon)用 bash_bg,再用 bash_output 看輸出。',
13
+ '- 部署/操作後用 http 或命令做健康檢查,確認真的好了。',
14
+ '- 對正式環境的破壞性操作(刪資料、重啟服務)先確認。',
15
+ ].join('\n');
16
+
17
+ export function createDevopsPack({ cwd = process.cwd() } = {}) {
18
+ const fs = createFsTools(cwd);
19
+ const bg = createBackgroundTools(cwd);
20
+ return {
21
+ name: 'devops',
22
+ tools: () => [fs.read, fs.ls, createGlobTool(cwd), createGrepTool(cwd), fs.write, fs.edit, fs.bash, ...bg.tools, createHttpTool()],
23
+ systemPrompt: SYSTEM_PROMPT,
24
+ contextFiles: ['RUNBOOK.md', 'AGENTS.md'],
25
+ preToolPolicy: { check: fs.readBeforeEdit },
26
+ permissionPolicy: { defaultMode: 'default' },
27
+ };
28
+ }
29
+
30
+ export const devopsPack = createDevopsPack();
@@ -1,16 +1,22 @@
1
1
  // general pack — 通用自主 agent。廣的 system prompt + 檔案/shell/web 工具。
2
2
  // 搭配 kernel 的 runGoal(目標循環)+ 子 agent + MCP,即為「給目標、自己做到完成」的通用 agent。
3
3
  import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
4
- import { isAbsolute, join } from 'node:path';
5
- import { execSync } from 'node:child_process';
4
+ import { isAbsolute, join, extname } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { createGrepTool, createGlobTool } from '../shared/code-nav.js';
7
+ import { createWebFetchTool, createWebSearchTool, createHttpTool } from '../shared/web-tools.js';
8
+
9
+ const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' };
6
10
 
7
11
  const txt = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s) }] });
8
12
 
9
13
  const SYSTEM_PROMPT = [
10
14
  '你是通用自主 agent。給你一個目標,你用工具反覆推進直到完成:',
11
- '- 可讀寫檔案、跑 shell 命令、搜尋網路(web_search)、抓網頁(web_fetch)、派子 agent 做聚焦調查。',
15
+ '- 可讀寫檔案、grep/glob 找東西、跑 shell、搜尋網路(web_search)、抓網頁(web_fetch)、',
16
+ ' 串接 API(http)、讀圖(read_image)、派子 agent、用待辦(todo_write)規劃多步任務。',
12
17
  '- 先想清楚步驟再動手;每步簡述你在做什麼。',
13
- '- 需要外部資料:先 web_search 找來源,再 web_fetch 讀全文,不要憑空編造。',
18
+ '- 需要外部資料:先 web_search 找來源,再 web_fetch 讀全文;串 API 用 http。不要憑空編造。',
19
+ '- 還缺的能力(瀏覽器點擊、特定服務)可由使用者掛 MCP server 補上。',
14
20
  '- 編輯既有檔案前先 read;破壞性/對外操作前確認。',
15
21
  '- 完成後明確說「已完成」並總結結果與如何驗證。',
16
22
  ].join('\n');
@@ -40,48 +46,42 @@ export function createGeneralPack({ cwd = process.cwd() } = {}) {
40
46
  execute: async (_id, { path, oldText, newText }) => { const p = abs(path); if (!existsSync(p)) return txt({ error: '檔案不存在', path }); const b = readFileSync(p, 'utf8'); if (!b.includes(oldText)) return txt({ error: 'oldText 未找到', path }); writeFileSync(p, b.replace(oldText, newText), 'utf8'); return txt({ edited: path }); },
41
47
  };
42
48
  const bash = {
43
- name: 'bash', label: 'bash', description: '執行 shell 命令', mutating: true, sandboxable: true,
44
- parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
45
- execute: async (_id, { command }) => { try { return txt(execSync(command, { cwd, encoding: 'utf8', timeout: 120000 }) || '(no output)'); } catch (e) { return txt({ error: e.message, stdout: e.stdout, stderr: e.stderr }); } },
46
- };
47
- const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
48
- const stripTags = (s) => String(s).replace(/<[^>]+>/g, '').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim();
49
-
50
- const webFetch = {
51
- name: 'web_fetch', label: '抓網頁', description: '抓取一個 URL 的內容並回傳純文字(HTML 去標籤、截斷)。讀某頁全文用。', readOnly: true,
52
- parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
53
- execute: async (_id, { url }) => {
54
- try {
55
- const res = await fetch(url, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(20000) });
56
- const html = await res.text();
57
- const text = html
58
- .replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
59
- .replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim().slice(0, 8000);
60
- return txt({ url, status: res.status, text: text || '(空)' });
61
- } catch (e) { return txt({ error: e?.message || String(e), url }); }
49
+ name: 'bash', label: 'bash', description: '執行 shell 命令(可選 timeout 秒數,預設 120)', mutating: true, sandboxable: true,
50
+ parameters: { type: 'object', properties: { command: { type: 'string' }, timeout: { type: 'number' } }, required: ['command'] },
51
+ execute: async (_id, { command, timeout }) => {
52
+ const ms = Math.min(600, Math.max(1, timeout || 120)) * 1000;
53
+ const r = spawnSync(command, { shell: true, cwd, encoding: 'utf8', timeout: ms, maxBuffer: 16 * 1024 * 1024 });
54
+ const output = ((r.stdout || '') + (r.stderr || '')).trim();
55
+ if (r.error) return txt({ error: r.error.message, output });
56
+ if (r.status !== 0) return txt({ error: `命令結束碼 ${r.status}`, output: output || '(no output)' });
57
+ return txt(output || '(no output)');
62
58
  },
63
59
  };
60
+ const webFetch = createWebFetchTool();
61
+ const webSearch = createWebSearchTool();
62
+ const http = createHttpTool();
64
63
 
65
- const webSearch = {
66
- name: 'web_search', label: '網路搜尋', readOnly: true,
67
- description: '用關鍵字搜尋網路,回傳前幾筆結果(標題 + URL + 摘要)。先 search 找來源,再用 web_fetch 讀全文。',
68
- parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
69
- execute: async (_id, { query }) => {
70
- try {
71
- const res = await fetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query), { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(20000) });
72
- const html = await res.text();
73
- const links = [...html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
74
- const snippets = [...html.matchAll(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => stripTags(m[1]));
75
- const decode = (href) => { const u = /uddg=([^&]+)/.exec(href); return u ? decodeURIComponent(u[1]) : href; };
76
- const results = links.slice(0, 6).map((m, i) => ({ title: stripTags(m[2]), url: decode(m[1]), snippet: snippets[i] || '' }));
77
- return txt({ query, count: results.length, results });
78
- } catch (e) { return txt({ error: e?.message || String(e), query }); }
64
+ // 多模態:讀圖交給模型「看」(需模型支援影像輸入)
65
+ const readImage = {
66
+ name: 'read_image', label: '讀圖', readOnly: true,
67
+ description: '讀取一張圖片(png/jpg/gif/webp)交給模型分析(截圖/設計稿/圖表)。需模型支援影像輸入。',
68
+ parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
69
+ execute: async (_id, { path }) => {
70
+ const p = abs(path);
71
+ if (!existsSync(p)) return txt({ error: '檔案不存在', path });
72
+ const mimeType = MIME[extname(p).toLowerCase()];
73
+ if (!mimeType) return txt({ error: '不支援的格式(png/jpg/gif/webp)', path });
74
+ try { return { content: [{ type: 'image', data: readFileSync(p).toString('base64'), mimeType }] }; }
75
+ catch (e) { return txt({ error: e.message }); }
79
76
  },
80
77
  };
81
78
 
79
+ const grepTool = createGrepTool(cwd);
80
+ const globTool = createGlobTool(cwd);
81
+
82
82
  return {
83
83
  name: 'general',
84
- tools: () => [read, ls, write, edit, bash, webFetch, webSearch],
84
+ tools: () => [read, ls, globTool, grepTool, write, edit, bash, webSearch, webFetch, http, readImage],
85
85
  systemPrompt: SYSTEM_PROMPT,
86
86
  contextFiles: ['AGENTS.md', 'GENERAL.md'],
87
87
  preToolPolicy: {
Binary file
@@ -0,0 +1,82 @@
1
+ // 共用檔案/shell 工具(read/ls/write/edit/bash)+ read-before-edit 守衛。
2
+ // 多個 pack(coding/devops…)共用;read 附行號、edit 唯一性檢查、bash 用 spawnSync 捕捉 stderr。
3
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
4
+ import { isAbsolute, join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const txt = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s) }] });
8
+
9
+ /**
10
+ * @param {string} cwd
11
+ * @returns {{ read, ls, write, edit, bash, readBeforeEdit }}
12
+ */
13
+ export function createFsTools(cwd) {
14
+ const readFiles = new Set();
15
+ const abs = (p) => (isAbsolute(p) ? p : join(cwd, p));
16
+
17
+ const read = {
18
+ name: 'read', label: '讀檔', readOnly: true,
19
+ description: '讀取檔案內容(每行附行號)。大檔可用 offset(起始行,1-based)+limit(行數)。',
20
+ parameters: { type: 'object', properties: { path: { type: 'string' }, offset: { type: 'number' }, limit: { type: 'number' } }, required: ['path'] },
21
+ execute: async (_id, { path, offset, limit }) => {
22
+ const p = abs(path);
23
+ if (!existsSync(p)) return txt({ error: '檔案不存在', path });
24
+ readFiles.add(p);
25
+ const lines = readFileSync(p, 'utf8').split('\n');
26
+ const start = Math.max(0, (offset || 1) - 1);
27
+ const count = limit && limit > 0 ? limit : 2000;
28
+ const numbered = lines.slice(start, start + count).map((l, i) => `${String(start + i + 1).padStart(6)}\t${l}`).join('\n');
29
+ const remain = lines.length - (start + count);
30
+ return txt(numbered + (remain > 0 ? `\n… 還有 ${remain} 行(offset=${start + count + 1})` : ''));
31
+ },
32
+ };
33
+ const ls = {
34
+ name: 'ls', label: '列目錄', readOnly: true, description: '列出目錄內容',
35
+ parameters: { type: 'object', properties: { path: { type: 'string' } } },
36
+ execute: async (_id, { path = '.' }) => { const p = abs(path); if (!existsSync(p)) return txt({ error: '目錄不存在', path }); return txt(readdirSync(p).map((n) => n + (statSync(join(p, n)).isDirectory() ? '/' : '')).join('\n')); },
37
+ };
38
+ const write = {
39
+ name: 'write', label: '寫檔', mutating: true, description: '建立或覆寫檔案',
40
+ parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
41
+ execute: async (_id, { path, content }) => { const p = abs(path); writeFileSync(p, content ?? '', 'utf8'); readFiles.add(p); return txt({ written: path, bytes: Buffer.byteLength(content ?? '') }); },
42
+ };
43
+ const edit = {
44
+ name: 'edit', label: '編輯', mutating: true,
45
+ description: '把 oldText 換成 newText。oldText 須唯一(多次出現需 replaceAll)。',
46
+ parameters: { type: 'object', properties: { path: { type: 'string' }, oldText: { type: 'string' }, newText: { type: 'string' }, replaceAll: { type: 'boolean' } }, required: ['path', 'oldText', 'newText'] },
47
+ execute: async (_id, { path, oldText, newText, replaceAll }) => {
48
+ const p = abs(path);
49
+ if (!existsSync(p)) return txt({ error: '檔案不存在', path });
50
+ const before = readFileSync(p, 'utf8');
51
+ const n = before.split(oldText).length - 1;
52
+ if (n === 0) return txt({ error: 'oldText 未找到(先 read 確認)', path });
53
+ if (n > 1 && !replaceAll) return txt({ error: `oldText 出現 ${n} 次,請更精確或設 replaceAll`, path });
54
+ writeFileSync(p, replaceAll ? before.split(oldText).join(newText) : before.replace(oldText, newText), 'utf8');
55
+ return txt({ edited: path, replaced: replaceAll ? n : 1 });
56
+ },
57
+ };
58
+ const bash = {
59
+ name: 'bash', label: 'bash', mutating: true, sandboxable: true,
60
+ description: '執行 shell 命令(可選 timeout 秒數,預設 120)。長時間/常駐用 bash_bg。',
61
+ parameters: { type: 'object', properties: { command: { type: 'string' }, timeout: { type: 'number' } }, required: ['command'] },
62
+ execute: async (_id, { command, timeout }) => {
63
+ const ms = Math.min(600, Math.max(1, timeout || 120)) * 1000;
64
+ const r = spawnSync(command, { shell: true, cwd, encoding: 'utf8', timeout: ms, maxBuffer: 16 * 1024 * 1024 });
65
+ const output = ((r.stdout || '') + (r.stderr || '')).trim();
66
+ if (r.error) return txt({ error: r.error.message, output });
67
+ if (r.status !== 0) return txt({ error: `命令結束碼 ${r.status}`, output: output || '(no output)' });
68
+ return txt(output || '(no output)');
69
+ },
70
+ };
71
+
72
+ // read-before-edit 守衛(給 pack 的 preToolPolicy 用)
73
+ const readBeforeEdit = (ctx) => {
74
+ if ((ctx.name === 'edit' || ctx.name === 'write') && ctx.args?.path) {
75
+ const p = abs(ctx.args.path);
76
+ if (existsSync(p) && !readFiles.has(p)) return { block: true, reason: `請先 read ${ctx.args.path} 再編輯。` };
77
+ }
78
+ return undefined;
79
+ };
80
+
81
+ return { read, ls, write, edit, bash, readBeforeEdit };
82
+ }
@@ -0,0 +1,57 @@
1
+ // 共用 web 工具(web_search / web_fetch / http)— general 與 deep-research pack 共用。
2
+ const txt = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s) }] });
3
+ const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
4
+ const stripTags = (s) => String(s).replace(/<[^>]+>/g, '').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim();
5
+
6
+ export function createWebFetchTool() {
7
+ return {
8
+ name: 'web_fetch', label: '抓網頁', readOnly: true,
9
+ description: '抓取一個 URL 的內容並回傳純文字(HTML 去標籤、截斷)。讀某頁全文用。',
10
+ parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
11
+ execute: async (_id, { url }) => {
12
+ try {
13
+ const res = await fetch(url, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(20000) });
14
+ const html = await res.text();
15
+ const text = html
16
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
17
+ .replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim().slice(0, 8000);
18
+ return txt({ url, status: res.status, text: text || '(空)' });
19
+ } catch (e) { return txt({ error: e?.message || String(e), url }); }
20
+ },
21
+ };
22
+ }
23
+
24
+ export function createWebSearchTool() {
25
+ return {
26
+ name: 'web_search', label: '網路搜尋', readOnly: true,
27
+ description: '用關鍵字搜尋網路,回傳前幾筆結果(標題 + URL + 摘要)。先 search 找來源,再用 web_fetch 讀全文。',
28
+ parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
29
+ execute: async (_id, { query }) => {
30
+ try {
31
+ const res = await fetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query), { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(20000) });
32
+ const html = await res.text();
33
+ const links = [...html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
34
+ const snippets = [...html.matchAll(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => stripTags(m[1]));
35
+ const decode = (href) => { const u = /uddg=([^&]+)/.exec(href); return u ? decodeURIComponent(u[1]) : href; };
36
+ const results = links.slice(0, 6).map((m, i) => ({ title: stripTags(m[2]), url: decode(m[1]), snippet: snippets[i] || '' }));
37
+ return txt({ query, count: results.length, results });
38
+ } catch (e) { return txt({ error: e?.message || String(e), query }); }
39
+ },
40
+ };
41
+ }
42
+
43
+ export function createHttpTool() {
44
+ return {
45
+ name: 'http', label: 'HTTP 請求',
46
+ description: '發 HTTP 請求串接 API:method(預設 GET)、headers、body。回 status + headers + body(截斷)。',
47
+ parameters: { type: 'object', properties: { url: { type: 'string' }, method: { type: 'string' }, headers: { type: 'object' }, body: { type: 'string' } }, required: ['url'] },
48
+ execute: async (_id, { url, method = 'GET', headers, body }) => {
49
+ try {
50
+ const m = String(method).toUpperCase();
51
+ const res = await fetch(url, { method: m, headers: headers || {}, body: (m === 'GET' || m === 'HEAD') ? undefined : body, signal: AbortSignal.timeout(20000) });
52
+ const text = (await res.text()).slice(0, 8000);
53
+ return txt({ url, method: m, status: res.status, headers: Object.fromEntries(res.headers), body: text });
54
+ } catch (e) { return txt({ error: e?.message || String(e), url }); }
55
+ },
56
+ };
57
+ }