@menghuan1918/dsh-apollo 0.0.0-stage → 0.4.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 (40) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +253 -2
  3. package/cordis.patch.yml +78 -0
  4. package/docs/DEVELOPMENT.md +682 -0
  5. package/package.json +76 -4
  6. package/presets/eng/NOTICE +3 -0
  7. package/presets/eng/plugins/exit-guard/index.js +111 -0
  8. package/presets/eng/plugins/exit-guard/package.json +7 -0
  9. package/presets/eng/plugins/mode-instructions/index.js +124 -0
  10. package/presets/eng/plugins/mode-instructions/package.json +7 -0
  11. package/presets/eng/plugins/wait-subagent/index.js +233 -0
  12. package/presets/eng/plugins/wait-subagent/package.json +7 -0
  13. package/presets/eng-ptc.patch.yml +186 -0
  14. package/presets/eng.patch.yml +338 -0
  15. package/skills/brainstorming/SKILL.md +90 -0
  16. package/skills/brainstorming/spec-document-reviewer-prompt.md +50 -0
  17. package/skills/code-review/SKILL.md +90 -0
  18. package/skills/dev/SKILL.md +41 -0
  19. package/skills/diagnosing-bugs/SKILL.md +132 -0
  20. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +55 -0
  21. package/skills/exec/SKILL.md +78 -0
  22. package/skills/handoff/SKILL.md +23 -0
  23. package/skills/main-agent-driven-development/SKILL.md +64 -0
  24. package/skills/tdd/SKILL.md +34 -0
  25. package/skills/tdd/mocking.md +59 -0
  26. package/skills/tdd/tests.md +77 -0
  27. package/src/cli/claude.js +166 -0
  28. package/src/cli/codex.js +164 -0
  29. package/src/cli/jsonl.js +216 -0
  30. package/src/cli-adapter.js +198 -0
  31. package/src/cli-sessions.js +69 -0
  32. package/src/client.js +1128 -0
  33. package/src/config.js +327 -0
  34. package/src/dsh-home.js +38 -0
  35. package/src/index.js +181 -0
  36. package/src/panel.js +844 -0
  37. package/src/settings.js +191 -0
  38. package/src/subagents.js +198 -0
  39. package/third-party/LICENSE-MIT-dsh-standard-preset +21 -0
  40. package/third-party/NOTICE-mattpocock-skills +21 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * dsh-apollo —— 设置装配(src/settings.js)。
3
+ *
4
+ * 本文件负责两件「必须等到服务可用才能做」的事,以及一次性的旧配置迁移:
5
+ *
6
+ * 1. **只读能力路由**:`ctx.inject(['webServer'])` → 注册
7
+ * `/apollo/api/capabilities`(处理器与探针在 `src/panel.js`,一处注册避免
8
+ * webServer 的重复 (kind,path) 抛错)。headless 组合没有 webServer 就自然
9
+ * 跳过,面板装配照常返回。
10
+ *
11
+ * 2. **行配置页面策略**:`ctx.inject(['settings'])` → 在本插件实例上登记
12
+ * `configure({ auto: false }, ctx.fiber)`:本行自带配置页(浏览器半边的
13
+ * `plugins.row.config`)就是它唯一的编辑入口,不需要 DSH 再按 schema 自动
14
+ * 生成一页。策略随本行的 effect 生命周期增删。
15
+ *
16
+ * 3. **旧 eng.json / atlas.json 一次性只读导入**:DSH 0.2 起设置就是正式
17
+ * Config(volatile,见 `src/config.js`),落盘由 configEditor 写进活动
18
+ * profile 的 cordis.patch.yml;`<dshHome>/eng.json` 与旧名 `atlas.json`
19
+ * 不再被写入(**只读**),但第一次装配时会把里面的 `exitGuard` 搬进正式
20
+ * 配置,之后旧的读取路径(预设本地守卫)即使被删也不丢开关。
21
+ *
22
+ * 迁移规则:
23
+ * - 只在 `settings` 服务可用、且 Loader 已把所有 entry 装配完(本行 fiber
24
+ * 进入 active,`describe()` 才认得它)之后跑一次;
25
+ * - **当前 profile 显式给出的值优先**:`exitGuard` 已存在于用户层(哪怕是
26
+ * 显式 `false`)时不覆盖——判断「存在」而不是「等于」,与 dsh-settings
27
+ * 的 override 语义一致;
28
+ * - 值已经与生效值相同(等于 schema 默认)时不写盘,避免制造无意义的
29
+ * profile 覆盖层;
30
+ * - 旧文件永不改写、永不删除;导入失败只告警,不影响插件装载。
31
+ *
32
+ * 持久化与生效:写入经 `settings.update`(= configEditor 校验完整候选后落盘 +
33
+ * Loader 调和)。因为 `exitGuard` 是 volatile 字段,写入不会重挂本行,运行中的
34
+ * 消费方(预设守卫、CLI 路由)下一次读取即拿到新值。
35
+ *
36
+ * 零第三方依赖:只用 node 内置模块 + 本包 `src/config.js`。
37
+ */
38
+
39
+ import { readFileSync } from 'node:fs'
40
+ import { join } from 'node:path'
41
+ import { ENTRY_ID } from './config.js'
42
+ import { resolveDshHome } from './dsh-home.js'
43
+ import { installOwnsHostTap, registerCapabilitiesRoute } from './panel.js'
44
+
45
+ /** 旧配置文件名,按优先级排列(新名优先,旧名只读回退)。 */
46
+ export const LEGACY_FILES = Object.freeze(['eng.json', 'atlas.json'])
47
+
48
+ /** 读取一个 JSON 文件并确保顶层是普通对象;缺失/损坏/形状非法返回 undefined。 */
49
+ function readJsonObject(path) {
50
+ try {
51
+ const raw = JSON.parse(readFileSync(path, 'utf8'))
52
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return undefined
53
+ return raw
54
+ } catch {
55
+ return undefined
56
+ }
57
+ }
58
+
59
+ /** 是否是普通数据对象。 */
60
+ function isRecord(value) {
61
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
62
+ }
63
+
64
+ /** 用户层里是否显式写了这个字段(存在即为覆盖,与值无关)。 */
65
+ function hasOwn(value, key) {
66
+ return isRecord(value) && Object.hasOwn(value, key)
67
+ }
68
+
69
+ /**
70
+ * 读取旧运行时配置文档(只读)。
71
+ *
72
+ * `eng.json` 优先,缺失或损坏时回退旧名 `atlas.json`;两者都没有返回 undefined。
73
+ * @param env - 读取 DSH_HOME 的进程环境(测试注入点)。
74
+ * @returns `{ file, path, value }` 或 undefined。
75
+ */
76
+ export function readLegacyDocument(env = process.env) {
77
+ const home = resolveDshHome(env)
78
+ for (const file of LEGACY_FILES) {
79
+ const path = join(home, file)
80
+ const value = readJsonObject(path)
81
+ if (value !== undefined) return { file, path, value }
82
+ }
83
+ return undefined
84
+ }
85
+
86
+ /**
87
+ * 由旧文档算出要写进正式配置的补丁(纯函数,便于测试)。
88
+ *
89
+ * 目前唯一有对应关系的旧字段是 `exitGuard`(布尔)。规则:
90
+ * - 旧值不是布尔 → 不导入;
91
+ * - 用户层已显式给出 `exitGuard`(含显式 false)→ 不覆盖;
92
+ * - 生效值已与旧值相同 → 不写盘(避免无意义的 profile 覆盖层)。
93
+ * @param raw - 旧文档内容。
94
+ * @param user - 当前用户层(profile 覆盖层投影,仅含可编辑字段)。
95
+ * @param value - 当前生效值(仅含可编辑字段)。
96
+ * @returns 补丁对象;无需导入时为空对象。
97
+ */
98
+ export function legacyPatch(raw, user, value) {
99
+ const patch = {}
100
+ if (typeof raw?.exitGuard !== 'boolean') return patch
101
+ if (hasOwn(user, 'exitGuard')) return patch
102
+ if (isRecord(value) && value.exitGuard === raw.exitGuard) return patch
103
+ patch.exitGuard = raw.exitGuard
104
+ return patch
105
+ }
106
+
107
+ /** 取 Loader 服务(用于等待所有 entry 装配完);拿不到返回 undefined。 */
108
+ function loaderOf(ctx) {
109
+ try {
110
+ const direct = ctx.get?.('loader')
111
+ if (typeof direct?.await === 'function') return direct
112
+ } catch {
113
+ // 未提供 loader(测试 fake ctx)→ 退回 root。
114
+ }
115
+ try {
116
+ const root = ctx.root?.loader
117
+ return typeof root?.await === 'function' ? root : undefined
118
+ } catch {
119
+ return undefined
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 把旧文档里的字段一次性导入正式配置(需要 `settings` 服务)。
125
+ *
126
+ * 调用方保证「只调用一次」;本函数自身也幂等(用户层已有该字段就不写)。
127
+ * @param ctx - 已注入 `settings` 的上下文。
128
+ * @param env - 读取 DSH_HOME 的进程环境(测试注入点)。
129
+ * @returns `{ imported, reason, file?, patch? }`(不抛错,失败信息只用于日志)。
130
+ */
131
+ export async function importLegacyConfig(ctx, env = process.env) {
132
+ const legacy = readLegacyDocument(env)
133
+ if (legacy === undefined) return { imported: false, reason: 'absent' }
134
+ const settings = ctx.settings
135
+ if (typeof settings?.describe !== 'function' || typeof settings.update !== 'function') {
136
+ return { imported: false, reason: 'unavailable', file: legacy.file }
137
+ }
138
+ // Loader 装配完成后 describe() 才认得出本行(fiber 必须 active)。
139
+ const loader = loaderOf(ctx)
140
+ if (loader !== undefined) await loader.await()
141
+ const descriptors = await settings.describe()
142
+ const descriptor = Array.isArray(descriptors)
143
+ ? descriptors.find((row) => row?.ns === ENTRY_ID)
144
+ : undefined
145
+ if (descriptor === undefined) return { imported: false, reason: 'not-configurable', file: legacy.file }
146
+ const patch = legacyPatch(legacy.value, descriptor.user, descriptor.value)
147
+ if (Object.keys(patch).length === 0) return { imported: false, reason: 'no-change', file: legacy.file }
148
+ await settings.update(ENTRY_ID, patch, descriptor.revision)
149
+ return { imported: true, file: legacy.file, patch }
150
+ }
151
+
152
+ /**
153
+ * 装配设置侧:延迟注册只读能力路由、登记行配置页面策略、一次性导入旧配置。
154
+ *
155
+ * 由宿主行(`src/index.js` 的 `apply`)调用。所有动作都延迟到服务出现时进行:
156
+ * 没有 webServer / settings 的组合里本函数安静返回,插件其余部分照常工作。
157
+ * @param ctx - 插件上下文。
158
+ * @param config - 行配置(可能含 volatile 引用)。
159
+ * @param options - 注入点(测试用):`env`、`route`(探针依赖,透传
160
+ * {@link registerCapabilitiesRoute})。
161
+ */
162
+ export function mountSettings(ctx, config, options = {}) {
163
+ const env = options.env ?? process.env
164
+
165
+ // 反代部署标志:宿主行只调 mountSettings 时也必须装上(否则设置镜像降级 memory)。
166
+ installOwnsHostTap(ctx, config)
167
+
168
+ ctx.inject(['webServer'], (webCtx) => {
169
+ registerCapabilitiesRoute(webCtx, () => config, options.route)
170
+ })
171
+
172
+ let started = false
173
+ ctx.inject(['settings'], (settingsCtx) => {
174
+ // 行配置页面策略:本行自带配置页,不需要自动生成的一页。
175
+ try {
176
+ const dispose = settingsCtx.settings.configure({ auto: false }, ctx.fiber)
177
+ settingsCtx.effect(() => dispose, 'apollo-settings: row config page policy')
178
+ } catch (error) {
179
+ settingsCtx.logger?.warn?.(`apollo-settings: settings page policy was not registered: ${error instanceof Error ? error.message : String(error)}`)
180
+ }
181
+ if (started) return
182
+ started = true
183
+ void importLegacyConfig(settingsCtx, env).then((result) => {
184
+ if (result.imported) {
185
+ settingsCtx.logger?.info?.(`apollo-settings: imported ${result.file} into the ${ENTRY_ID} config (exitGuard=${String(result.patch.exitGuard)})`)
186
+ }
187
+ }, (error) => {
188
+ settingsCtx.logger?.warn?.(`apollo-settings: legacy config import skipped: ${error instanceof Error ? error.message : String(error)}`)
189
+ })
190
+ })
191
+ }
@@ -0,0 +1,198 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { homedir } from 'node:os'
3
+ import { join, isAbsolute, resolve } from 'node:path'
4
+ import { normalizeConfig } from './config.js'
5
+ import { CLI_ROUTES, createCliAdapter, enabledIn, selectCli } from './cli-adapter.js'
6
+ import { openCliSessions } from './cli-sessions.js'
7
+
8
+ const CLI_NAMES = Object.keys(CLI_ROUTES)
9
+ const cliName = (provider) => CLI_NAMES.includes(provider) ? provider : undefined
10
+ const internalRoute = (provider) => Object.values(CLI_ROUTES).includes(provider)
11
+ const success = (value) => ({ isError: false, value, content: [] })
12
+
13
+ export function cliCatalog(config, request = {}) {
14
+ const providers = CLI_NAMES.filter((cli) => normalizeConfig(config).subagents[cli].enabled)
15
+ if (request.provider === undefined) return providers.map((cli) => `${cli} — ${cli === 'codex' ? 'Codex' : 'Claude'} CLI (background continuable subagents)`).join('\n')
16
+ const cli = cliName(request.provider)
17
+ if (!cli) return ''
18
+ const backend = normalizeConfig(config).subagents[cli]
19
+ if (!backend.enabled) throw new Error(`Apollo ${cli} backend is disabled`)
20
+ if (request.model === undefined) return backend.models.map((model) => `${cli}/${model.id}${model.id === backend.defaultModel ? ' (default)' : ''}`).join('\n') || '(no allowed models)'
21
+ const model = backend.models.find((item) => item.id === request.model)
22
+ if (!model) throw new Error(`Apollo ${cli} model "${request.model}" is not allowed`)
23
+ return `${cli}/${model.id}\nReasoning efforts:\n${model.efforts.map((effort) => `${effort}${effort === model.defaultEffort ? ' (default)' : ''}`).join('\n')}`
24
+ }
25
+
26
+ /**
27
+ * Resolve the child's initial absolute directory from the parent's effective one.
28
+ *
29
+ * The Session header keeps the original project directory, so it can be stale
30
+ * after a working-directory change; the working-directory service is the only
31
+ * authority for the parent's current directory. An absolute explicit `cwd`
32
+ * needs no such lookup, a relative one resolves against that current directory,
33
+ * and omission inherits it. Resolving here keeps the KV record and the
34
+ * manager's pinned initial directory identical.
35
+ * @param {object} ctx - runtime owning the working-directory service.
36
+ * @param {object} parent - calling Agent whose directory is inherited.
37
+ * @param {unknown} requested - explicit tool `cwd`, already schema-validated upstream.
38
+ * @param {AbortSignal} signal - cancellation for the directory inspection.
39
+ * @returns {Promise<string>} absolute child directory.
40
+ */
41
+ async function childCwd(ctx, parent, requested, signal) {
42
+ if (requested !== undefined && typeof requested !== 'string') throw new Error('cwd must be a string')
43
+ signal.throwIfAborted()
44
+ if (requested !== undefined && isAbsolute(requested)) return requested
45
+ const current = await ctx.workingDirectory.ensure(parent, signal)
46
+ signal.throwIfAborted()
47
+ if (!current || !isAbsolute(current)) throw new Error('Apollo CLI subagent requires an absolute session working directory')
48
+ return requested === undefined ? current : resolve(current, requested)
49
+ }
50
+
51
+ export async function startCliSubagent(ctx, config, sessions, args, exec) {
52
+ const cli = cliName(args.provider)
53
+ if (!cli) throw new Error('Unknown Apollo CLI provider')
54
+ const parent = exec.agent
55
+ if (!parent) throw new Error('Apollo subagent requires a calling Agent')
56
+ if (!enabledIn(config, parent)) throw new Error('Apollo CLI subagents are disabled for this preset')
57
+ if (args.run_in_background !== undefined) throw new Error('run_in_background was removed for CLI subagents; they always run in the background and are continued with send_message')
58
+ if (args.agent_preset !== undefined) throw new Error('agent_preset is not supported for CLI subagents yet')
59
+ if (typeof args.prompt !== 'string' || typeof args.description !== 'string') throw new Error('subagent requires a prompt and description')
60
+ const selected = selectCli(config, cli, args.model, args.reasoning_effort)
61
+ const cwd = await childCwd(ctx, parent, args.cwd, exec.signal)
62
+ let executable
63
+ try {
64
+ const command = selected.executable.startsWith('~/') ? join(homedir(), selected.executable.slice(2)) : selected.executable
65
+ executable = await ctx.subprocess.resolveExecutable(command, undefined, exec.signal)
66
+ } catch (error) {
67
+ exec.signal.throwIfAborted()
68
+ throw new Error(`Apollo ${cli} executable is unavailable; set its absolute path in Plugins → Apollo`, { cause: error })
69
+ }
70
+ const id = randomUUID()
71
+ const record = {
72
+ version: 1, id, parentId: parent.id, cli, cwd,
73
+ executable, profile: cli === 'codex' ? selected.profile : '',
74
+ cliHome: cli === 'codex' ? process.env.CODEX_HOME ?? join(homedir(), '.codex')
75
+ : process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'),
76
+ // Explicit CLAUDE_CONFIG_DIR changes config and keychain lookup, even for ~/.claude.
77
+ ...(cli === 'claude' && process.env.CLAUDE_CONFIG_DIR === undefined ? { claudeDefaultHome: true } : {}),
78
+ model: selected.model, effort: selected.effort, delivered: [], started: false,
79
+ }
80
+ await sessions.put(record)
81
+ try {
82
+ exec.signal.throwIfAborted()
83
+ const maxDepth = ctx.subagents.resolveMaxDepth()
84
+ const created = await ctx.subagents.startActivation({
85
+ provider: 'spawn', label: args.description, childId: id, signal: exec.signal,
86
+ delivery: 'parent',
87
+ request: {
88
+ parent, prompt: [{ type: 'text', text: args.prompt }], cwd,
89
+ agentOptions: { provider: CLI_ROUTES[cli], model: selected.model, reasoningEffort: selected.effort },
90
+ ...(maxDepth === undefined ? {} : { maxDepth }),
91
+ },
92
+ })
93
+ exec.signal.throwIfAborted()
94
+ return { kind: 'activation', subagentId: created.childId }
95
+ } catch (error) {
96
+ // The manager rolls back pre-admission failure; drain also covers the final
97
+ // acceptance/cancellation race before this tool could hand the id back.
98
+ await ctx.subagents.drainChildren(parent, [id])
99
+ await sessions.delete(id)
100
+ throw error
101
+ }
102
+ }
103
+
104
+ /** Host-level interception extends existing tools without replacing registrations. */
105
+ export function installCliRouting(ctx, config, sessions) {
106
+ const unpublished = new Map()
107
+ const cleanups = new Set()
108
+ const creating = new Set()
109
+ const lifetime = new AbortController()
110
+ const reclaim = (entry) => {
111
+ if (entry.cleanup) return entry.cleanup
112
+ const task = (async () => {
113
+ await ctx.subagents.drainChildren(entry.parent, [entry.id])
114
+ await sessions.delete(entry.id)
115
+ })()
116
+ entry.cleanup = task
117
+ cleanups.add(task)
118
+ task.catch((error) => ctx.logger?.warn?.(`Apollo could not reclaim unreported CLI child ${entry.id}: ${error.message}`))
119
+ .finally(() => cleanups.delete(task))
120
+ return task
121
+ }
122
+ ctx.on('tools/result', (exec, result) => {
123
+ const key = exec.token ?? exec
124
+ const entry = unpublished.get(key)
125
+ if (!entry) return
126
+ unpublished.delete(key)
127
+ if (!result.isError && result.value?.kind === 'activation' && result.value.subagentId === entry.id) return
128
+ void reclaim(entry)
129
+ })
130
+ ctx.tools.guard((exec) => {
131
+ const args = exec.arguments ?? {}
132
+ if (!['subagent', 'subagent_fork'].includes(exec.name)) return
133
+ if (internalRoute(args.provider)) return 'Use provider="codex" or provider="claude" with subagent; internal CLI routes are reserved'
134
+ const selected = cliName(args.provider)
135
+ if (selected && !enabledIn(config, exec.agent)) return 'Apollo CLI subagents are disabled for this preset'
136
+ if (exec.name === 'subagent_fork' && (selected || (args.provider === undefined && internalRoute(exec.agent?.options.provider)))) {
137
+ return 'Codex/Claude support background subagent creation only; subagent_fork cannot create a CLI child'
138
+ }
139
+ })
140
+ ctx.on('tools/execute', async (exec, next) => {
141
+ const args = exec.arguments ?? {}
142
+ if (!enabledIn(config, exec.agent)) return next()
143
+ if (exec.name === 'subagent' && cliName(args.provider)) {
144
+ const signal = AbortSignal.any([exec.signal, lifetime.signal])
145
+ const creation = (async () => {
146
+ const value = await startCliSubagent(ctx, config, sessions, args, { ...exec, signal })
147
+ unpublished.set(exec.token ?? exec, { parent: exec.agent, id: value.subagentId })
148
+ return success(value)
149
+ })()
150
+ creating.add(creation)
151
+ try { return await creation } finally { creating.delete(creation) }
152
+ }
153
+ if (exec.name !== 'list_subagent_models') return next()
154
+ if (cliName(args.provider)) return success(cliCatalog(config, args))
155
+ if (args.provider !== undefined || args.model !== undefined) return next()
156
+ const base = await next()
157
+ const extra = cliCatalog(config)
158
+ if (base.isError || !extra) return base
159
+ return success([base.value, extra].filter(Boolean).join('\n'))
160
+ })
161
+ ctx.systemPrompt.section({
162
+ name: 'apollo:cli-subagents', order: 80,
163
+ text: (context) => {
164
+ const agent = context.agent ?? ctx.agents.get(context.session?.id ?? context.scope?.id)
165
+ if (!enabledIn(config, agent)) return ''
166
+ const catalog = cliCatalog(config)
167
+ if (!catalog) return ''
168
+ return [
169
+ 'Apollo CLI subagents: subagent also accepts provider="codex" or provider="claude"; model and reasoning_effort default to Apollo settings (list_subagent_models with that provider shows allowed values).',
170
+ 'They always run in the background, start in cwd (default: current directory) with permissions from Apollo settings; subagent_fork and agent_preset are unsupported.',
171
+ 'Control them by child id with send_message, wait_subagent, list_agents and interrupt_agent.',
172
+ catalog,
173
+ ].join('\n')
174
+ },
175
+ })
176
+ return async () => {
177
+ lifetime.abort(new Error('Apollo CLI routing unloaded'))
178
+ await Promise.allSettled([...creating])
179
+ await Promise.allSettled([...unpublished.values()].map(reclaim))
180
+ unpublished.clear()
181
+ await Promise.allSettled([...cleanups])
182
+ }
183
+ }
184
+
185
+ export function mountSubagents(ctx, config) {
186
+ ctx.inject(['tools', 'subagents', 'agents', 'llm', 'storage', 'subprocess', 'systemPrompt', 'workingDirectory'], async (runtime) => {
187
+ const sessions = await openCliSessions(runtime)
188
+ const adapters = CLI_NAMES.map((cli) => createCliAdapter(runtime, config, sessions, cli))
189
+ let stopRouting = async () => {}
190
+ runtime.effect(() => async () => {
191
+ await stopRouting()
192
+ await Promise.all(adapters.map((adapter) => adapter.dispose()))
193
+ await sessions.close()
194
+ })
195
+ adapters.forEach((adapter, index) => runtime.llm.registerAdapter([CLI_ROUTES[CLI_NAMES[index]]], adapter))
196
+ stopRouting = installCliRouting(runtime, config, sessions)
197
+ })
198
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Pocock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.