@capyclaw/foundation 0.0.0-stage → 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/lib/host.mjs ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @capyclaw/foundation 的 Cordis 入口:向同进程插件提供 ctx.os。
3
+ *
4
+ * 这是整个工程里唯一一处「把内核交给 dsh」的接缝。应用插件不 import 这个文件,
5
+ * 它们只在自己的 apply 里拿到 ctx.os。
6
+ */
7
+ import { createKernel } from './kernel.mjs'
8
+
9
+ export const name = 'capyclaw-foundation'
10
+
11
+ export function apply(ctx, config = {}) {
12
+ const kernel = createKernel(ctx, config)
13
+ const dispose = ctx.provide('os', kernel.os)
14
+ console.log('[foundation] ready(abi v' + kernel.os.abi + ')')
15
+ return () => {
16
+ try {
17
+ kernel.dispose()
18
+ } finally {
19
+ if (typeof dispose === 'function') dispose()
20
+ }
21
+ }
22
+ }
package/lib/kernel.mjs ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 内核装配:把 ABI 加载器、适配器与 os 子系统接到一起。
3
+ *
4
+ * 能力表是**探测出来**的,不是写死的:ctx 里没有 skills 服务,就不承诺 os.skills,
5
+ * 依赖它的插件在 mount 阶段拿到 E_CAP_MISSING,而不是运行到一半才炸。
6
+ *
7
+ * 惰性:os.net 只在第一次挂路由时创建共享宿主并 listen;纯 skill 插件不占端口。
8
+ */
9
+ import { ABI_MAJOR } from '../abi/kinds.mjs'
10
+ import { createLoader } from '../abi/loader.mjs'
11
+ import { createSkillAdapter } from './adapters/skill-provider.mjs'
12
+ import { createToolProviderAdapter } from './adapters/tool-provider.mjs'
13
+ import { createRuntimeDriverAdapter } from './adapters/runtime-driver.mjs'
14
+ import { createHttpRouterAdapter } from './adapters/http-router.mjs'
15
+ import { createUiPanelAdapter } from './adapters/ui-panel.mjs'
16
+ import { createHookRegistry, createLifecycleAdapter } from './adapters/lifecycle-hook.mjs'
17
+ import { createEvents } from './subsystems/events.mjs'
18
+ import { createLogger } from './subsystems/log.mjs'
19
+ import { createNet } from './subsystems/net.mjs'
20
+
21
+ /** 内核自身就能提供的能力(不依赖任何 dsh 服务)。 */
22
+ export const BASE_CAPABILITIES = Object.freeze({
23
+ 'os.events': '1.0.0',
24
+ 'os.log': '1.0.0',
25
+ 'os.caps': '1.0.0',
26
+ })
27
+
28
+ /** 按 ctx 里实际存在的服务生成能力表。 */
29
+ export function detectCapabilities(ctx) {
30
+ const capabilities = { ...BASE_CAPABILITIES }
31
+ if (ctx?.skills?.registerProvider) capabilities['os.skills'] = '1.0.0'
32
+ if (ctx?.serviceHost?.createSharedHost && ctx?.serviceHost?.createApp) capabilities['os.net'] = '1.0.0'
33
+ if (ctx?.toolHost?.registerWithDsh) capabilities['os.tools'] = '1.0.0'
34
+ if (ctx?.serviceBroker?.start) capabilities['os.proc'] = '1.0.0'
35
+ if (ctx?.editorHost?.createEditorHost) capabilities['os.ui'] = '1.0.0'
36
+ return capabilities
37
+ }
38
+
39
+ export const KERNEL_VERSION = '0.1.0'
40
+ export const ABI_VERSION = ABI_MAJOR
41
+
42
+ export function createKernel(ctx, config = {}) {
43
+ const log = createLogger(ctx, config)
44
+ const events = createEvents({
45
+ onError: (error, topic) => log.error(`事件订阅者抛错 (${topic}): ${error.message}`),
46
+ })
47
+ const hooks = createHookRegistry()
48
+ const net = createNet(ctx, config)
49
+ const capabilities = detectCapabilities(ctx)
50
+
51
+ const adapters = {
52
+ 'skill-provider@1': createSkillAdapter({ ctx, log }),
53
+ 'tool-provider@1': createToolProviderAdapter({ ctx, log }),
54
+ 'runtime-driver@1': createRuntimeDriverAdapter({ ctx, config, log }),
55
+ 'http-router@1': createHttpRouterAdapter({ ctx, net, log }),
56
+ 'ui-panel@1': createUiPanelAdapter({ ctx, net, log }),
57
+ 'lifecycle-hook@1': createLifecycleAdapter({ hooks, log }),
58
+ }
59
+
60
+ const loader = createLoader({ adapters, capabilities, log })
61
+ const startedAt = Date.now()
62
+
63
+ const os = {
64
+ version: KERNEL_VERSION,
65
+ abi: ABI_VERSION,
66
+ mount: (spec, mountConfig = {}, meta = {}) => loader.mount(spec, mountConfig, meta),
67
+ report: () => ({
68
+ kernel: { version: KERNEL_VERSION, abi: ABI_VERSION, uptimeMs: Date.now() - startedAt },
69
+ ...loader.report(),
70
+ }),
71
+ events,
72
+ log,
73
+ hooks,
74
+ net,
75
+
76
+ /** 能力检查与自报。 */
77
+ caps: {
78
+ list: () => Object.keys(capabilities),
79
+ has: (name) => loader.capability(name) !== null,
80
+ version: (name) => loader.capability(name),
81
+ report: () => loader.report(),
82
+ },
83
+ }
84
+
85
+ return {
86
+ os,
87
+ dispose() {
88
+ loader.disposeAll()
89
+ events.clear()
90
+ void net.dispose()
91
+ },
92
+ }
93
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 技能扫描:SKILL.md 目录 → dsh skills provider。
3
+ *
4
+ * 这份实现是整个基石层里 skill-provider@1 的唯一实现。
5
+ * 此前 15 个包各自复制了一份 provider.mjs(sha256 相同),
6
+ * 迁移完成后由内核统一提供,应用包只声明目录。
7
+ */
8
+ import { readFileSync } from 'node:fs'
9
+ import { readFile } from 'node:fs/promises'
10
+ import { isAbsolute, join, sep } from 'node:path'
11
+
12
+ const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
13
+ const DEFAULT_RANK = 600
14
+
15
+ export function parseSkill(raw, path) {
16
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw)
17
+ if (!frontmatter?.[1]) throw new Error(`skill ${path} 缺少 YAML frontmatter`)
18
+ const meta = {}
19
+ for (const line of frontmatter[1].split(/\r?\n/)) {
20
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
21
+ if (match) meta[match[1]] = match[2].replace(/^['"]|['"]$/g, '').trim()
22
+ }
23
+ if (!meta.name || !meta.description) throw new Error(`skill ${path} 缺少 name/description`)
24
+ if (!NAME_RE.test(meta.name)) throw new Error(`skill ${path} name 必须是 kebab-case`)
25
+ return {
26
+ name: meta.name,
27
+ description: meta.description,
28
+ whenToUse: meta.whenToUse || undefined,
29
+ content: raw.slice(frontmatter[0].length).trim(),
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {{
35
+ * assetRoot: string,
36
+ * skillDirs: string[],
37
+ * providerName?: string,
38
+ * rank?: number,
39
+ * contract?: { version: string } | null,
40
+ * contractNote?: (version: string, skillName: string) => string
41
+ * }} config
42
+ */
43
+ export function createSkillProvider(config) {
44
+ const assetRoot = config.assetRoot
45
+ if (!assetRoot || !isAbsolute(assetRoot)) throw new Error('createSkillProvider: assetRoot 必须是绝对路径')
46
+ const rank = config.rank ?? DEFAULT_RANK
47
+ const dirs = config.skillDirs || []
48
+ if (!dirs.length) throw new Error('createSkillProvider: skillDirs 不能为空')
49
+
50
+ const candidates = dirs.map((dir) => {
51
+ if (!NAME_RE.test(dir)) throw new Error(`skill 目录名必须是 kebab-case:${dir}`)
52
+ const directory = join(assetRoot, dir)
53
+ const locator = join(directory, 'SKILL.md')
54
+ const parsed = parseSkill(readFileSync(locator, 'utf8'), locator)
55
+ if (parsed.name !== dir) throw new Error(`skill ${locator} name 必须是 ${dir}`)
56
+ return {
57
+ name: parsed.name,
58
+ description: parsed.description,
59
+ whenToUse: parsed.whenToUse,
60
+ invocation: { modelInvocable: true, userInvocable: true },
61
+ provider: config.providerName || 'dsh-plugin',
62
+ source: 'bundled',
63
+ rank,
64
+ resourceBase: { kind: 'directory', path: directory.split(sep).join('/') },
65
+ locator,
66
+ }
67
+ })
68
+
69
+ const byName = new Map(candidates.map((candidate) => [candidate.name, candidate]))
70
+ const noteFor = (name) => (config.contract && config.contractNote
71
+ ? config.contractNote(config.contract.contractVersion ?? config.contract.version, name)
72
+ : '')
73
+
74
+ return {
75
+ name: config.providerName || 'dsh-plugin',
76
+ list: () => Promise.resolve(candidates),
77
+ async get(candidate, options = {}) {
78
+ const hit = byName.get(candidate.name)
79
+ if (!hit) throw new Error(`未知 skill ${candidate.name}`)
80
+ const raw = await readFile(hit.locator, { encoding: 'utf8', signal: options.signal })
81
+ const parsed = parseSkill(raw, hit.locator)
82
+ const { rank: _rank, locator: _locator, ...summary } = hit
83
+ const note = noteFor(candidate.name)
84
+ return { ...summary, content: parsed.content + (note ? `\n\n---\n\n${note}` : '') }
85
+ },
86
+ }
87
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 进程内事件总线:插件之间唯一的广播通道。
3
+ *
4
+ * 约定:emit 永不因订阅者抛错而中断;错误交给 onError 记录。
5
+ */
6
+ export function createEvents({ onError } = {}) {
7
+ const listeners = new Map()
8
+
9
+ function on(topic, handler) {
10
+ if (typeof handler !== 'function') throw new TypeError('events.on 需要函数')
11
+ if (!listeners.has(topic)) listeners.set(topic, [])
12
+ listeners.get(topic).push(handler)
13
+ return () => off(topic, handler)
14
+ }
15
+
16
+ function once(topic, handler) {
17
+ const dispose = on(topic, (...args) => {
18
+ dispose()
19
+ handler(...args)
20
+ })
21
+ return dispose
22
+ }
23
+
24
+ function off(topic, handler) {
25
+ const bucket = listeners.get(topic)
26
+ if (!bucket) return
27
+ const index = bucket.indexOf(handler)
28
+ if (index >= 0) bucket.splice(index, 1)
29
+ if (!bucket.length) listeners.delete(topic)
30
+ }
31
+
32
+ function emit(topic, payload) {
33
+ const bucket = listeners.get(topic)
34
+ if (!bucket) return 0
35
+ let delivered = 0
36
+ for (const handler of [...bucket]) {
37
+ try {
38
+ handler(payload)
39
+ delivered++
40
+ } catch (error) {
41
+ onError?.(error, topic)
42
+ }
43
+ }
44
+ return delivered
45
+ }
46
+
47
+ return {
48
+ on,
49
+ once,
50
+ off,
51
+ emit,
52
+ topics: () => [...listeners.keys()],
53
+ clear: () => listeners.clear(),
54
+ }
55
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 结构化日志:优先复用 dsh 的 ctx.logger,没有就退回 console。
3
+ *
4
+ * 插件不自己拼前缀,一律通过 log.child({ plugin }) 拿作用域。
5
+ */
6
+ const LEVELS = ['debug', 'info', 'warn', 'error']
7
+
8
+ function createScoped(underlying, scope) {
9
+ const prefix = scope ? `[${scope}]` : ''
10
+ const scoped = {}
11
+ for (const level of LEVELS) {
12
+ scoped[level] = (...args) => {
13
+ const message = args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')
14
+ const line = `${prefix} ${message}`.trim()
15
+ const fn = underlying?.[level]
16
+ if (typeof fn === 'function') fn.call(underlying, line)
17
+ else if (level !== 'debug') console[level](line)
18
+ }
19
+ }
20
+ scoped.child = (next) => createScoped(underlying, scope ? `${scope}:${next}` : next)
21
+ return scoped
22
+ }
23
+
24
+ export function createLogger(ctx, config = {}) {
25
+ const underlying = ctx?.logger
26
+ return createScoped(underlying, config.scope || 'foundation')
27
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * HTTP 共享宿主:内核持有的唯一监听面。
3
+ *
4
+ * 惰性:第一次有插件挂路由时才创建共享宿主并 listen,
5
+ * 只注册 skill 的插件不会因为装了 foundation 就占一个端口。
6
+ */
7
+ import { AbiError, ERROR_CODES } from '../../abi/errors.mjs'
8
+
9
+ export function createNet(ctx, config = {}) {
10
+ const http = config.http || {}
11
+ let host = null
12
+ let listening = null
13
+ let closed = false
14
+
15
+ function ensure() {
16
+ const serviceHost = ctx?.serviceHost
17
+ if (!serviceHost?.createSharedHost) {
18
+ throw new AbiError(ERROR_CODES.E_CAP_MISSING,
19
+ 'ctx.serviceHost 不可用:无法挂载 HTTP 路由', { capability: 'os.net' })
20
+ }
21
+ if (!host) {
22
+ host = serviceHost.createSharedHost({ id: http.hostId || 'foundation-shared' })
23
+ listening = Promise.resolve(host.listen({ port: http.port ?? 0 }))
24
+ }
25
+ return host
26
+ }
27
+
28
+ return {
29
+ ensure,
30
+ mount(prefix, child) {
31
+ return ensure().mount(prefix, child)
32
+ },
33
+ children: () => (host ? host.children() : []),
34
+ ready() {
35
+ ensure()
36
+ return listening
37
+ },
38
+ async dispose() {
39
+ if (closed) return
40
+ closed = true
41
+ if (host) {
42
+ try {
43
+ await host.close()
44
+ } catch { /* 已关闭 */ }
45
+ }
46
+ host = null
47
+ },
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@capyclaw/foundation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "dsh 插件基石层:接口 ABI、插件加载器与内核服务 ctx.os。应用插件只声明接口,不写装配代码。",
5
+ "type": "module",
6
+ "main": "./lib/host.mjs",
7
+ "exports": {
8
+ ".": "./lib/host.mjs",
9
+ "./abi": "./abi/index.mjs",
10
+ "./kit": "./kit/index.mjs",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "engines": {
14
+ "node": ">=18",
15
+ "dsh": "^0.2.0-rc.2"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dsh": {
21
+ "bundle": {
22
+ "patch": "./cordis.patch.yml"
23
+ },
24
+ "category": "service",
25
+ "platform": "both",
26
+ "apiVersion": "1.0.0"
27
+ },
28
+ "meta": {
29
+ "title": "插件基石层",
30
+ "description": "接口 ABI、插件加载器与 ctx.os 内核服务;应用插件只声明接口"
31
+ },
32
+ "files": [
33
+ "abi/",
34
+ "lib/",
35
+ "kit/",
36
+ "cordis.patch.yml",
37
+ "README.md"
38
+ ],
39
+ "scripts": {
40
+ "test": "node --test test/*.test.mjs"
41
+ },
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@capyclaw/plugin-kit": "^0.2.0"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/Maple496/capyclaw.git",
49
+ "directory": "packages/capyclaw-foundation"
50
+ }
51
+ }