@miphamai/cli 0.48.0 → 0.50.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 (32) hide show
  1. package/package.json +1 -1
  2. package/skills/mipham/doc-sync.mipham-skill.md +198 -0
  3. package/skills/mipham/self-audit.mipham-skill.md +1 -1
  4. package/skills/standard/github-ops.SKILL.md +1 -1
  5. package/skills/standard/web-access/references/cdp-api.md +132 -0
  6. package/skills/standard/web-access/references/site-patterns/.gitkeep +0 -0
  7. package/skills/standard/web-access/scripts/cdp-proxy.mjs +756 -0
  8. package/skills/standard/web-access/scripts/check-deps.mjs +187 -0
  9. package/skills/standard/web-access/scripts/find-url.mjs +271 -0
  10. package/skills/standard/web-access/scripts/match-site.mjs +48 -0
  11. package/skills/standard/web-access.SKILL.md +77 -158
  12. package/src/agent/cross-session/discovery.ts +26 -1
  13. package/src/config/defaults.ts +3 -0
  14. package/src/core/behavior-tasks.json +101 -0
  15. package/src/core/behavior-tasks.ts +44 -0
  16. package/src/core/crsi-producer.ts +214 -0
  17. package/src/core/crsi-sandbox.ts +5 -0
  18. package/src/core/eval-harness.ts +7 -0
  19. package/src/core/instructions.ts +11 -0
  20. package/src/core/proposal-guard.ts +100 -0
  21. package/src/core/task-runner-tasks.json +14 -0
  22. package/src/core/task-runner.ts +163 -0
  23. package/src/daemon/heartbeat.ts +83 -0
  24. package/src/daemon/server.ts +34 -2
  25. package/src/shared/package-info.ts +4 -1
  26. package/src/skills/bundled-skill-assets.ts +19 -0
  27. package/src/skills/bundled-skills.ts +4 -3
  28. package/src/skills/skill-assets.ts +37 -0
  29. package/src/tools/agent/skill.ts +13 -2
  30. package/src/tools/file/grep.ts +11 -1
  31. package/src/ui/app.tsx +21 -1
  32. package/src/ui/commands.ts +81 -6
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // 环境检查 + 确保 CDP Proxy 就绪(跨平台,替代 check-deps.sh)
3
+
4
+ import { spawn } from 'node:child_process'
5
+ import fs from 'node:fs'
6
+ import net from 'node:net'
7
+ import os from 'node:os'
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
12
+ const PROXY_SCRIPT = path.join(ROOT, 'scripts', 'cdp-proxy.mjs')
13
+ const PROXY_PORT = Number(process.env.CDP_PROXY_PORT || 3456)
14
+
15
+ // --- Node.js 版本检查 ---
16
+
17
+ function checkNode() {
18
+ const major = Number(process.versions.node.split('.')[0])
19
+ const version = `v${process.versions.node}`
20
+ if (major >= 22) {
21
+ console.log(`node: ok (${version})`)
22
+ } else {
23
+ console.log(`node: warn (${version}, 建议升级到 22+)`)
24
+ }
25
+ }
26
+
27
+ // --- TCP 端口探测 ---
28
+
29
+ function checkPort(port, host = '127.0.0.1', timeoutMs = 2000) {
30
+ return new Promise((resolve) => {
31
+ const socket = net.createConnection(port, host)
32
+ const timer = setTimeout(() => {
33
+ socket.destroy()
34
+ resolve(false)
35
+ }, timeoutMs)
36
+ socket.once('connect', () => {
37
+ clearTimeout(timer)
38
+ socket.destroy()
39
+ resolve(true)
40
+ })
41
+ socket.once('error', () => {
42
+ clearTimeout(timer)
43
+ resolve(false)
44
+ })
45
+ })
46
+ }
47
+
48
+ // --- Chrome 调试端口检测(DevToolsActivePort 多路径 + 常见端口回退) ---
49
+
50
+ function activePortFiles() {
51
+ const home = os.homedir()
52
+ const localAppData = process.env.LOCALAPPDATA || ''
53
+ switch (os.platform()) {
54
+ case 'darwin':
55
+ return [
56
+ path.join(home, 'Library/Application Support/Google/Chrome/DevToolsActivePort'),
57
+ path.join(home, 'Library/Application Support/Google/Chrome Canary/DevToolsActivePort'),
58
+ path.join(home, 'Library/Application Support/Chromium/DevToolsActivePort'),
59
+ ]
60
+ case 'linux':
61
+ return [
62
+ path.join(home, '.config/google-chrome/DevToolsActivePort'),
63
+ path.join(home, '.config/chromium/DevToolsActivePort'),
64
+ ]
65
+ case 'win32':
66
+ return [
67
+ path.join(localAppData, 'Google/Chrome/User Data/DevToolsActivePort'),
68
+ path.join(localAppData, 'Chromium/User Data/DevToolsActivePort'),
69
+ ]
70
+ default:
71
+ return []
72
+ }
73
+ }
74
+
75
+ async function detectChromePort() {
76
+ // 优先从 DevToolsActivePort 文件读取
77
+ for (const filePath of activePortFiles()) {
78
+ try {
79
+ const lines = fs.readFileSync(filePath, 'utf8').trim().split(/\r?\n/).filter(Boolean)
80
+ const port = parseInt(lines[0], 10)
81
+ if (port > 0 && port < 65536 && (await checkPort(port))) {
82
+ return port
83
+ }
84
+ } catch (_) {}
85
+ }
86
+ // 回退:探测常见端口
87
+ for (const port of [9222, 9229, 9333]) {
88
+ if (await checkPort(port)) {
89
+ return port
90
+ }
91
+ }
92
+ return null
93
+ }
94
+
95
+ // --- CDP Proxy 启动与等待 ---
96
+
97
+ function httpGetJson(url, timeoutMs = 3000) {
98
+ return fetch(url, { signal: AbortSignal.timeout(timeoutMs) })
99
+ .then(async (res) => {
100
+ try {
101
+ return JSON.parse(await res.text())
102
+ } catch {
103
+ return null
104
+ }
105
+ })
106
+ .catch(() => null)
107
+ }
108
+
109
+ function startProxyDetached() {
110
+ const logFile = path.join(os.tmpdir(), 'cdp-proxy.log')
111
+ const logFd = fs.openSync(logFile, 'a')
112
+ const child = spawn(process.execPath, [PROXY_SCRIPT], {
113
+ detached: true,
114
+ stdio: ['ignore', logFd, logFd],
115
+ ...(os.platform() === 'win32' ? { windowsHide: true } : {}),
116
+ })
117
+ child.unref()
118
+ fs.closeSync(logFd)
119
+ }
120
+
121
+ async function ensureProxy() {
122
+ const targetsUrl = `http://127.0.0.1:${PROXY_PORT}/targets`
123
+
124
+ // /targets 返回 JSON 数组即 ready
125
+ const targets = await httpGetJson(targetsUrl)
126
+ if (Array.isArray(targets)) {
127
+ console.log('proxy: ready')
128
+ return true
129
+ }
130
+
131
+ // 未运行或未连接,启动并等待
132
+ console.log('proxy: connecting...')
133
+ startProxyDetached()
134
+
135
+ // 等 proxy 进程就绪
136
+ await new Promise((r) => setTimeout(r, 2000))
137
+
138
+ for (let i = 1; i <= 15; i++) {
139
+ const result = await httpGetJson(targetsUrl, 8000)
140
+ if (Array.isArray(result)) {
141
+ console.log('proxy: ready')
142
+ return true
143
+ }
144
+ if (i === 1) {
145
+ console.log('⚠️ Chrome 可能有授权弹窗,请点击「允许」后等待连接...')
146
+ }
147
+ await new Promise((r) => setTimeout(r, 1000))
148
+ }
149
+
150
+ console.log('❌ 连接超时,请检查 Chrome 调试设置')
151
+ console.log(` 日志:${path.join(os.tmpdir(), 'cdp-proxy.log')}`)
152
+ return false
153
+ }
154
+
155
+ // --- main ---
156
+
157
+ async function main() {
158
+ checkNode()
159
+
160
+ const chromePort = await detectChromePort()
161
+ if (!chromePort) {
162
+ console.log(
163
+ 'chrome: not connected — 请确保 Chrome 已打开,然后访问 chrome://inspect/#remote-debugging 并勾选 Allow remote debugging',
164
+ )
165
+ process.exit(1)
166
+ }
167
+ console.log(`chrome: ok (port ${chromePort})`)
168
+
169
+ const proxyOk = await ensureProxy()
170
+ if (!proxyOk) {
171
+ process.exit(1)
172
+ }
173
+
174
+ // 列出已有站点经验
175
+ const patternsDir = path.join(ROOT, 'references', 'site-patterns')
176
+ try {
177
+ const sites = fs
178
+ .readdirSync(patternsDir)
179
+ .filter((f) => f.endsWith('.md'))
180
+ .map((f) => f.replace(/\.md$/, ''))
181
+ if (sites.length) {
182
+ console.log(`\nsite-patterns: ${sites.join(', ')}`)
183
+ }
184
+ } catch {}
185
+ }
186
+
187
+ await main()
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ // find-url - 从本地 Chrome 书签/历史中检索 URL
3
+ // 用于定位公网搜索覆盖不到的目标(组织内部系统、SSO 后台、内网域名等)。
4
+ //
5
+ // 用法:
6
+ // node find-url.mjs [关键词...] [--only bookmarks|history] [--limit N] [--since 1d|7h|YYYY-MM-DD]
7
+ //
8
+ // <关键词> 空格分词、多词 AND,匹配 title + url;可省略
9
+ // --only <source> 限定数据源(bookmarks / history),默认两者都查
10
+ // --limit N 条数上限,默认 20;0 = 不限
11
+ // --since <window> 时间窗(仅作用于历史)。1d / 7h / 30m 或 YYYY-MM-DD
12
+ // --sort recent|visits 历史排序:按最近访问 / 按访问次数,默认 recent
13
+ //
14
+ // 示例:
15
+ // node find-url.mjs 财务小智
16
+ // node find-url.mjs agent skills
17
+ // node find-url.mjs github --since 7d --only history
18
+ // node find-url.mjs --since 7d --only history --sort visits # 最近一周高频网站
19
+ // node find-url.mjs --since 2d --only history --limit 0
20
+
21
+ import fs from 'node:fs'
22
+ import path from 'node:path'
23
+ import os from 'node:os'
24
+ import { execFileSync } from 'node:child_process'
25
+
26
+ // --- 参数解析 -----------------------------------------------------------
27
+ function parseArgs(argv) {
28
+ const a = { keywords: [], only: null, limit: 20, since: null, sort: 'recent' }
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const v = argv[i]
31
+ if (v === '--only') a.only = argv[++i]
32
+ else if (v === '--limit') a.limit = parseInt(argv[++i], 10)
33
+ else if (v === '--since') a.since = parseSince(argv[++i])
34
+ else if (v === '--sort') a.sort = argv[++i]
35
+ else if (v === '-h' || v === '--help') {
36
+ printUsage()
37
+ process.exit(0)
38
+ } else if (v.startsWith('--')) die(`未知参数: ${v}`)
39
+ else a.keywords.push(v)
40
+ }
41
+ if (a.only && !['bookmarks', 'history'].includes(a.only)) die(`--only 仅支持 bookmarks|history`)
42
+ if (!['recent', 'visits'].includes(a.sort)) die(`--sort 仅支持 recent|visits`)
43
+ if (Number.isNaN(a.limit) || a.limit < 0) die('--limit 需为非负整数')
44
+ return a
45
+ }
46
+
47
+ function parseSince(s) {
48
+ if (!s) die('--since 需要值')
49
+ const m = s.match(/^(\d+)([dhm])$/)
50
+ if (m) {
51
+ const n = parseInt(m[1], 10)
52
+ const ms = { d: 86400000, h: 3600000, m: 60000 }[m[2]]
53
+ return new Date(Date.now() - n * ms)
54
+ }
55
+ const d = new Date(s)
56
+ if (Number.isNaN(d.getTime())) die(`无效 --since 值: ${s}(用 1d / 7h / 30m / YYYY-MM-DD)`)
57
+ return d
58
+ }
59
+
60
+ function die(msg) {
61
+ console.error(msg)
62
+ process.exit(1)
63
+ }
64
+ function printUsage() {
65
+ console.error(
66
+ fs
67
+ .readFileSync(new URL(import.meta.url))
68
+ .toString()
69
+ .split('\n')
70
+ .slice(1, 19)
71
+ .map((l) => l.replace(/^\/\/ ?/, ''))
72
+ .join('\n'),
73
+ )
74
+ }
75
+
76
+ // --- Chrome 用户数据目录(跨平台) ---------------------------------------
77
+ function getChromeDataDir() {
78
+ const home = os.homedir()
79
+ switch (os.platform()) {
80
+ case 'darwin':
81
+ return path.join(home, 'Library/Application Support/Google/Chrome')
82
+ case 'linux':
83
+ return path.join(home, '.config/google-chrome')
84
+ case 'win32':
85
+ return path.join(process.env.LOCALAPPDATA || '', 'Google/Chrome/User Data')
86
+ default:
87
+ return null
88
+ }
89
+ }
90
+
91
+ // --- Profile 枚举 -------------------------------------------------------
92
+ function listProfiles(dataDir) {
93
+ try {
94
+ const state = JSON.parse(fs.readFileSync(path.join(dataDir, 'Local State'), 'utf-8'))
95
+ const info = state?.profile?.info_cache || {}
96
+ const list = Object.keys(info).map((dir) => ({ dir, name: info[dir].name || dir }))
97
+ if (list.length) return list
98
+ } catch {
99
+ /* 回退 */
100
+ }
101
+ return [{ dir: 'Default', name: 'Default' }]
102
+ }
103
+
104
+ // --- 书签检索 -----------------------------------------------------------
105
+ function searchBookmarks(profileDir, profileName, keywords) {
106
+ const file = path.join(profileDir, 'Bookmarks')
107
+ if (!fs.existsSync(file)) return []
108
+ let data
109
+ try {
110
+ data = JSON.parse(fs.readFileSync(file, 'utf-8'))
111
+ } catch {
112
+ return []
113
+ }
114
+ if (!keywords.length) return [] // 书签无时间维度,无关键词不返回
115
+
116
+ const needles = keywords.map((k) => k.toLowerCase())
117
+ const out = []
118
+ function walk(node, trail) {
119
+ if (!node) return
120
+ if (node.type === 'url') {
121
+ const hay = `${node.name || ''} ${node.url || ''}`.toLowerCase()
122
+ if (needles.every((n) => hay.includes(n))) {
123
+ out.push({
124
+ profile: profileName,
125
+ name: node.name || '',
126
+ url: node.url || '',
127
+ folder: trail.join(' / '),
128
+ })
129
+ }
130
+ }
131
+ if (Array.isArray(node.children)) {
132
+ const sub = node.name ? [...trail, node.name] : trail
133
+ for (const c of node.children) walk(c, sub)
134
+ }
135
+ }
136
+ for (const root of Object.values(data.roots || {})) walk(root, [])
137
+ return out
138
+ }
139
+
140
+ // --- 历史检索(SQLite 运行时锁定,需 copy 到 tmp) ------------------------
141
+ const WEBKIT_EPOCH_DIFF_US = 11644473600000000n // 1601→1970 微秒差
142
+
143
+ function searchHistory(profileDir, profileName, keywords, since, limit, sort) {
144
+ const src = path.join(profileDir, 'History')
145
+ if (!fs.existsSync(src)) return []
146
+ const tmp = path.join(os.tmpdir(), `chrome-history-${process.pid}-${Date.now()}.sqlite`)
147
+ try {
148
+ fs.copyFileSync(src, tmp)
149
+ const conds = ['last_visit_time > 0']
150
+ for (const kw of keywords) {
151
+ const esc = kw.toLowerCase().replace(/'/g, "''")
152
+ conds.push(`LOWER(title || ' ' || url) LIKE '%${esc}%'`)
153
+ }
154
+ if (since) {
155
+ const webkitUs = BigInt(since.getTime()) * 1000n + WEBKIT_EPOCH_DIFF_US
156
+ conds.push(`last_visit_time >= ${webkitUs}`)
157
+ }
158
+ const limitClause = limit === 0 ? -1 : limit
159
+ const orderBy =
160
+ sort === 'visits' ? 'visit_count DESC, last_visit_time DESC' : 'last_visit_time DESC'
161
+ const sql = `SELECT title, url,
162
+ datetime((last_visit_time - 11644473600000000)/1000000, 'unixepoch', 'localtime') AS visit,
163
+ visit_count
164
+ FROM urls WHERE ${conds.join(' AND ')}
165
+ ORDER BY ${orderBy} LIMIT ${limitClause};`
166
+
167
+ const raw = execFileSync('sqlite3', ['-separator', '\t', tmp, sql], {
168
+ encoding: 'utf-8',
169
+ maxBuffer: 50 * 1024 * 1024,
170
+ })
171
+ return raw
172
+ .trim()
173
+ .split('\n')
174
+ .filter(Boolean)
175
+ .map((line) => {
176
+ const [title, url, visit, visit_count] = line.split('\t')
177
+ return { profile: profileName, title, url, visit, visit_count: parseInt(visit_count, 10) }
178
+ })
179
+ } catch (e) {
180
+ if (e.code === 'ENOENT')
181
+ die(
182
+ '未找到 sqlite3 命令。macOS/Linux 通常自带;Windows 可用 `winget install sqlite.sqlite` 或从 https://sqlite.org/download.html 下载后加入 PATH。',
183
+ )
184
+ return []
185
+ } finally {
186
+ try {
187
+ fs.unlinkSync(tmp)
188
+ } catch {}
189
+ }
190
+ }
191
+
192
+ // --- 输出格式化 ---------------------------------------------------------
193
+ // 用 `|` 作字段分隔符;字段内含 `|` 的替换成 `│`(全宽竖线)避免歧义
194
+ const clean = (s) =>
195
+ String(s ?? '')
196
+ .replaceAll('|', '│')
197
+ .trim()
198
+
199
+ function printBookmarks(items, multiProfile) {
200
+ console.log(`[书签] ${items.length} 条`)
201
+ for (const b of items) {
202
+ const segs = [clean(b.name) || '(无标题)', clean(b.url)]
203
+ if (b.folder) segs.push(clean(b.folder))
204
+ if (multiProfile) segs.push('@' + clean(b.profile))
205
+ console.log(' ' + segs.join(' | '))
206
+ }
207
+ }
208
+
209
+ function printHistory(items, multiProfile, sortLabel) {
210
+ console.log(`[历史] ${items.length} 条(${sortLabel})`)
211
+ for (const h of items) {
212
+ const segs = [clean(h.title) || '(无标题)', clean(h.url), h.visit]
213
+ if (h.visit_count > 1) segs.push(`visits=${h.visit_count}`)
214
+ if (multiProfile) segs.push('@' + clean(h.profile))
215
+ console.log(' ' + segs.join(' | '))
216
+ }
217
+ }
218
+
219
+ // --- main ---------------------------------------------------------------
220
+ const args = parseArgs(process.argv.slice(2))
221
+
222
+ const dataDir = getChromeDataDir()
223
+ if (!dataDir || !fs.existsSync(dataDir)) die('未找到 Chrome 用户数据目录')
224
+
225
+ const profiles = listProfiles(dataDir)
226
+ const doBookmarks = args.only !== 'history'
227
+ const doHistory = args.only !== 'bookmarks'
228
+
229
+ const bookmarks = []
230
+ const history = []
231
+ for (const p of profiles) {
232
+ const pDir = path.join(dataDir, p.dir)
233
+ if (!fs.existsSync(pDir)) continue
234
+ if (doBookmarks) bookmarks.push(...searchBookmarks(pDir, p.name, args.keywords))
235
+ if (doHistory)
236
+ history.push(
237
+ ...searchHistory(
238
+ pDir,
239
+ p.name,
240
+ args.keywords,
241
+ args.since,
242
+ args.limit === 0 ? 0 : args.limit * 2,
243
+ args.sort,
244
+ ),
245
+ )
246
+ }
247
+
248
+ // 历史跨 profile 合并后按指定 sort 重排 + 切顶
249
+ if (args.sort === 'visits') {
250
+ history.sort(
251
+ (a, b) =>
252
+ (b.visit_count || 0) - (a.visit_count || 0) || (b.visit || '').localeCompare(a.visit || ''),
253
+ )
254
+ } else {
255
+ history.sort((a, b) => (b.visit || '').localeCompare(a.visit || ''))
256
+ }
257
+ const bookmarksOut = args.limit === 0 ? bookmarks : bookmarks.slice(0, args.limit)
258
+ const historyOut = args.limit === 0 ? history : history.slice(0, args.limit)
259
+
260
+ // 仅当结果真的横跨多个 profile 时,才输出 @profile 标注(空 profile 不算)
261
+ const seenProfiles = new Set([...bookmarksOut, ...historyOut].map((x) => x.profile))
262
+ const showProfile = seenProfiles.size > 1
263
+
264
+ const sortLabel = args.sort === 'visits' ? '按访问次数' : '按最近访问'
265
+ if (doBookmarks) printBookmarks(bookmarksOut, showProfile)
266
+ if (doBookmarks && doHistory) console.log()
267
+ if (doHistory) printHistory(historyOut, showProfile, sortLabel)
268
+
269
+ if (!args.keywords.length && doBookmarks && !doHistory) {
270
+ console.error('\n提示:书签无时间维度,无关键词查询无意义。加关键词或切换 --only history。')
271
+ }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // 根据用户输入匹配站点经验文件(跨平台,替代 match-site.sh)
3
+ // 用法:node match-site.mjs "用户输入文本"
4
+ // 输出:匹配到的站点经验内容,无匹配则静默
5
+
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
11
+ const PATTERNS_DIR = path.join(ROOT, 'references', 'site-patterns')
12
+ const query = (process.argv[2] || '').trim()
13
+
14
+ if (!query || !fs.existsSync(PATTERNS_DIR)) {
15
+ process.exit(0)
16
+ }
17
+
18
+ for (const entry of fs.readdirSync(PATTERNS_DIR, { withFileTypes: true })) {
19
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue
20
+
21
+ const domain = entry.name.replace(/\.md$/, '')
22
+ const raw = fs.readFileSync(path.join(PATTERNS_DIR, entry.name), 'utf8')
23
+
24
+ // 提取 aliases
25
+ const aliasesLine = raw.split(/\r?\n/).find((l) => l.startsWith('aliases:')) || ''
26
+ const aliases = aliasesLine
27
+ .replace(/^aliases:\s*/, '')
28
+ .replace(/^\[/, '')
29
+ .replace(/\]$/, '')
30
+ .split(',')
31
+ .map((v) => v.trim())
32
+ .filter(Boolean)
33
+
34
+ // 构建匹配模式
35
+ const escaped = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
36
+ const pattern = [domain, ...aliases].map(escaped).join('|')
37
+ if (!new RegExp(pattern, 'i').test(query)) continue
38
+
39
+ // 跳过 frontmatter,输出正文
40
+ const fences = [...raw.matchAll(/^---\s*$/gm)]
41
+ const body =
42
+ fences.length >= 2
43
+ ? raw.slice(fences[1].index + fences[1][0].length).replace(/^\r?\n/, '')
44
+ : raw
45
+
46
+ process.stdout.write(`--- 站点经验: ${domain} ---\n`)
47
+ process.stdout.write(body.trimEnd() + '\n\n')
48
+ }