dsh-paoding 0.2.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 +95 -0
- package/README_en.md +95 -0
- package/bin/dsh-paoding.mjs +12 -0
- package/cordis.patch.yml +6 -0
- package/docs/architecture.md +292 -0
- package/docs/architecture_en.md +363 -0
- package/docs/configuration.md +551 -0
- package/docs/configuration_en.md +560 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/installation.md +364 -0
- package/docs/installation_en.md +367 -0
- package/docs/orchestration.md +254 -0
- package/docs/orchestration_en.md +255 -0
- package/install.sh +31 -0
- package/package.json +29 -0
- package/plugins/paoding-config-ui/api-core.mjs +100 -0
- package/plugins/paoding-config-ui/index.mjs +327 -0
- package/plugins/paoding-config-ui/lib/base.css +615 -0
- package/plugins/paoding-config-ui/lib/client.js +1154 -0
- package/plugins/paoding-config-ui/package.json +23 -0
- package/plugins/paoding-config-ui/runtime-inventory.mjs +212 -0
- package/presets/orchestrator/agent.cordis.yml +323 -0
- package/presets/orchestrator/preset.yml +6 -0
- package/presets/orchestrator/restrict.mjs +191 -0
- package/tools/install.mjs +24 -0
- package/tools/lib/alloc.mjs +138 -0
- package/tools/lib/cli.mjs +522 -0
- package/tools/lib/compose.mjs +647 -0
- package/tools/lib/config.mjs +297 -0
- package/tools/lib/host.mjs +288 -0
- package/tools/lib/skills.mjs +146 -0
- package/tools/lib/spans.mjs +188 -0
- package/tools/lib/state.mjs +328 -0
- package/tools/lib/util.mjs +26 -0
- package/tools/lib/wizard.mjs +516 -0
- package/tools/lib/yaml.mjs +266 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML 装载与 patch 解析:loadYaml(经 createRequire 从 $DSH_HOME/node_modules
|
|
3
|
+
* 惰性加载 yaml 包)、preprocessPatchText(中和 cordis 的 !!js 表达式)与内置
|
|
4
|
+
* YAML subset 回退解析器(parsePatchFile / parseYamlSubset 家族,块标量 /
|
|
5
|
+
* insert 包裹等由 consumeBlockScalar / flattenEntries 等支撑)。
|
|
6
|
+
* 依赖 util(warn)与 node:fs / node:module / node:path;被 config / state 引用。
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { createRequire } from 'node:module'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { warn } from './util.mjs'
|
|
12
|
+
|
|
13
|
+
// ── yaml loading ────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the `yaml` package from $DSH_HOME/node_modules via createRequire.
|
|
17
|
+
* The directory is typically a symlink into dsh's npx node_modules, which
|
|
18
|
+
* ships yaml v2. Returns null when unavailable — callers then use the
|
|
19
|
+
* built-in fallback parser.
|
|
20
|
+
*/
|
|
21
|
+
export function loadYaml(dshHome) {
|
|
22
|
+
try {
|
|
23
|
+
const requireFromDsh = createRequire(path.join(dshHome, 'node_modules', 'package.json'))
|
|
24
|
+
const mod = requireFromDsh('yaml')
|
|
25
|
+
if (mod && typeof mod.parse === 'function' && typeof mod.parseDocument === 'function') {
|
|
26
|
+
return mod
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
/* yaml missing or unreadable — fall back to the subset parser */
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── patch parsing ───────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Neutralize the cordis `!!js` expressions found in real patch files before
|
|
38
|
+
* any parser sees them (yaml would otherwise raise unresolved-tag warnings).
|
|
39
|
+
*/
|
|
40
|
+
export function preprocessPatchText(text) {
|
|
41
|
+
const isWin = process.platform === 'win32'
|
|
42
|
+
return text
|
|
43
|
+
.replaceAll("!!js process.platform === 'win32'", isWin ? 'true' : 'false')
|
|
44
|
+
.replaceAll("!!js process.platform !== 'win32'", isWin ? 'false' : 'true')
|
|
45
|
+
.replaceAll('!!js process.cwd()', JSON.stringify(process.cwd()))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Parse one patch file. Returns the top-level array, or null (warned) on failure. */
|
|
49
|
+
export function parsePatchFile(file, yamlMod) {
|
|
50
|
+
let text
|
|
51
|
+
try {
|
|
52
|
+
text = readFileSync(file, 'utf8')
|
|
53
|
+
} catch (err) {
|
|
54
|
+
warn(`${file}: unreadable (${err.message}) — skipped`)
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
const processed = preprocessPatchText(text)
|
|
58
|
+
|
|
59
|
+
if (yamlMod) {
|
|
60
|
+
try {
|
|
61
|
+
const data = yamlMod.parse(processed)
|
|
62
|
+
if (Array.isArray(data)) return data
|
|
63
|
+
warn(`${file}: parsed but the top level is not a list — skipped`)
|
|
64
|
+
return null
|
|
65
|
+
} catch (yamlErr) {
|
|
66
|
+
warn(`${file}: yaml parse failed (${yamlErr.message}) — trying the built-in fallback parser`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const data = parseYamlSubset(processed)
|
|
72
|
+
if (Array.isArray(data)) return data
|
|
73
|
+
warn(`${file}: fallback parser produced a non-list result — skipped`)
|
|
74
|
+
return null
|
|
75
|
+
} catch (err) {
|
|
76
|
+
warn(`${file}: could not be parsed (fallback: ${err.message}) — skipped`)
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Minimal YAML subset parser, used only for patch files when `yaml` is
|
|
83
|
+
* unavailable. Supports: top-level list items, nested maps, scalar values
|
|
84
|
+
* (single/double-quoted strings, numbers, true/false), `#` comments and
|
|
85
|
+
* `- insert:` wrapping.
|
|
86
|
+
*/
|
|
87
|
+
export function parseYamlSubset(text) {
|
|
88
|
+
const lines = []
|
|
89
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
90
|
+
const line = stripInlineComment(raw).replace(/[ \t]+$/, '')
|
|
91
|
+
if (line.trim() === '') continue
|
|
92
|
+
const indent = line.match(/^[ \t]*/)[0].length
|
|
93
|
+
lines.push({ indent, text: line.slice(indent) })
|
|
94
|
+
}
|
|
95
|
+
if (lines.length === 0) return []
|
|
96
|
+
const [value] = parseBlock(lines, 0, lines[0].indent)
|
|
97
|
+
return value
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function stripInlineComment(line) {
|
|
101
|
+
let inSingle = false
|
|
102
|
+
let inDouble = false
|
|
103
|
+
for (let i = 0; i < line.length; i++) {
|
|
104
|
+
const c = line[i]
|
|
105
|
+
if (inSingle) {
|
|
106
|
+
if (c === "'") inSingle = false
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (inDouble) {
|
|
110
|
+
if (c === '"') inDouble = false
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (c === "'") inSingle = true
|
|
114
|
+
else if (c === '"') inDouble = true
|
|
115
|
+
// YAML comments start with '#' only at line start or after whitespace.
|
|
116
|
+
else if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i)
|
|
117
|
+
}
|
|
118
|
+
return line
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** YAML 块标量(| > 及其折叠/裁剪变体)值标记:后续更深的行全部属该标量正文,
|
|
122
|
+
* 直到遇到缩进 <= 键缩进的行(同级键/父级结束)。返回拼接正文与下一行下标。
|
|
123
|
+
* 说明:parseYamlSubset 是 yaml 包缺失时的回退解析器;此前块标量(如角色
|
|
124
|
+
* persona: |-)之后的同级键/顶层键(roles_remove / main_agent_extra 等)会被
|
|
125
|
+
* 误判为无法解析而静默丢弃,导致回读配置丢键。 */
|
|
126
|
+
export function consumeBlockScalar(lines, i, keyIndent) {
|
|
127
|
+
const parts = []
|
|
128
|
+
while (i < lines.length && lines[i].indent > keyIndent) {
|
|
129
|
+
parts.push(lines[i].text)
|
|
130
|
+
i++
|
|
131
|
+
}
|
|
132
|
+
return { value: parts.join('\n'), next: i }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseBlock(lines, i, indent) {
|
|
136
|
+
if (lines[i].text.startsWith('- ')) return parseList(lines, i, indent)
|
|
137
|
+
return parseMap(lines, i, indent)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function parseList(lines, i, indent) {
|
|
141
|
+
const out = []
|
|
142
|
+
while (i < lines.length && lines[i].indent === indent && lines[i].text.startsWith('- ')) {
|
|
143
|
+
const rest = lines[i].text.slice(2).trim()
|
|
144
|
+
i++
|
|
145
|
+
if (rest === '') {
|
|
146
|
+
// `- ` followed by a nested block (e.g. `- insert:`)
|
|
147
|
+
if (i < lines.length && lines[i].indent > indent) {
|
|
148
|
+
const [value, next] = parseBlock(lines, i, lines[i].indent)
|
|
149
|
+
out.push(value)
|
|
150
|
+
i = next
|
|
151
|
+
} else {
|
|
152
|
+
out.push(null)
|
|
153
|
+
}
|
|
154
|
+
} else if (looksLikeKeyValue(rest)) {
|
|
155
|
+
const [key, value] = splitKeyValue(rest)
|
|
156
|
+
const item = {}
|
|
157
|
+
if (value === undefined) {
|
|
158
|
+
if (i < lines.length && lines[i].indent > indent) {
|
|
159
|
+
const [nested, next] = parseBlock(lines, i, lines[i].indent)
|
|
160
|
+
item[key] = nested
|
|
161
|
+
i = next
|
|
162
|
+
} else {
|
|
163
|
+
item[key] = null
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
let parsed = parseScalar(value)
|
|
167
|
+
if (/^[|>][-+]?$/.test(String(value).trim()) && i < lines.length && lines[i].indent > indent) {
|
|
168
|
+
const blk = consumeBlockScalar(lines, i, indent)
|
|
169
|
+
parsed = blk.value
|
|
170
|
+
i = blk.next
|
|
171
|
+
}
|
|
172
|
+
item[key] = parsed
|
|
173
|
+
}
|
|
174
|
+
// Remaining keys of this map item continue at a deeper indent.
|
|
175
|
+
if (i < lines.length && lines[i].indent > indent) {
|
|
176
|
+
const [more, next] = parseMap(lines, i, lines[i].indent)
|
|
177
|
+
Object.assign(item, more)
|
|
178
|
+
i = next
|
|
179
|
+
}
|
|
180
|
+
out.push(item)
|
|
181
|
+
} else {
|
|
182
|
+
out.push(parseScalar(rest))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return [out, i]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function parseMap(lines, i, indent) {
|
|
189
|
+
const out = {}
|
|
190
|
+
while (i < lines.length && lines[i].indent === indent && !lines[i].text.startsWith('- ')) {
|
|
191
|
+
const line = lines[i].text
|
|
192
|
+
if (!looksLikeKeyValue(line)) break
|
|
193
|
+
const [key, value] = splitKeyValue(line)
|
|
194
|
+
i++
|
|
195
|
+
if (value === undefined) {
|
|
196
|
+
if (i < lines.length && lines[i].indent > indent) {
|
|
197
|
+
const [nested, next] = parseBlock(lines, i, lines[i].indent)
|
|
198
|
+
out[key] = nested
|
|
199
|
+
i = next
|
|
200
|
+
} else {
|
|
201
|
+
out[key] = null
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
let parsed = parseScalar(value)
|
|
205
|
+
if (/^[|>][-+]?$/.test(String(value).trim()) && i < lines.length && lines[i].indent > indent) {
|
|
206
|
+
const blk = consumeBlockScalar(lines, i, indent)
|
|
207
|
+
parsed = blk.value
|
|
208
|
+
i = blk.next
|
|
209
|
+
}
|
|
210
|
+
out[key] = parsed
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return [out, i]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function looksLikeKeyValue(rest) {
|
|
217
|
+
const trimmed = rest.trim()
|
|
218
|
+
if (trimmed.endsWith(':') && trimmed.length > 1) return true
|
|
219
|
+
return trimmed.includes(': ') || trimmed.includes(':\t')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Split `key: value` at the first colon; a trailing `key:` yields undefined. */
|
|
223
|
+
export function splitKeyValue(rest) {
|
|
224
|
+
const trimmed = rest.trim()
|
|
225
|
+
if (trimmed.endsWith(':')) return [trimmed.slice(0, -1).trim(), undefined]
|
|
226
|
+
const idx = trimmed.indexOf(': ')
|
|
227
|
+
if (idx === -1) return [trimmed, undefined]
|
|
228
|
+
return [trimmed.slice(0, idx).trim(), trimmed.slice(idx + 2).trim()]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function parseScalar(raw) {
|
|
232
|
+
const s = raw.trim()
|
|
233
|
+
if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) {
|
|
234
|
+
return s.slice(1, -1).replace(/''/g, "'")
|
|
235
|
+
}
|
|
236
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(s)
|
|
239
|
+
} catch {
|
|
240
|
+
return s.slice(1, -1)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (s === 'true') return true
|
|
244
|
+
if (s === 'false') return false
|
|
245
|
+
if (s === 'null' || s === '~') return null
|
|
246
|
+
// 空流式序列 []:serializeConfig 对显式空 main_agent_extra 落盘 `[]`,回读时
|
|
247
|
+
// 必须还原为数组而非字符串,否则 has_main_agent_extra 判否、显式空语义丢失。
|
|
248
|
+
// (非空流式 [a, b] 仍不支持 —— 现有消费键均走块列表或 yaml 包路径。)
|
|
249
|
+
if (s === '[]') return []
|
|
250
|
+
if (/^-?\d+$/.test(s) || /^-?\d*\.\d+$/.test(s)) return Number(s)
|
|
251
|
+
return s
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Recursively expand `- insert:` wrappers into plain entries. */
|
|
255
|
+
export function flattenEntries(list, out = []) {
|
|
256
|
+
for (const item of list ?? []) {
|
|
257
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) continue
|
|
258
|
+
if (item.insert !== undefined) {
|
|
259
|
+
if (Array.isArray(item.insert)) flattenEntries(item.insert, out)
|
|
260
|
+
else if (item.insert && typeof item.insert === 'object') flattenEntries([item.insert], out)
|
|
261
|
+
} else {
|
|
262
|
+
out.push(item)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return out
|
|
266
|
+
}
|