@lynn123411/dsh-ask-user-grilling 0.2.6 → 0.5.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/README.md CHANGED
@@ -1,17 +1,24 @@
1
1
  # @lynn123411/dsh-ask-user-grilling
2
2
 
3
- DSH 原生 `ask_user_question`([`@deepseek-ai/dsh-tool-ask-user`](https://www.npmjs.com/package/@deepseek-ai/dsh-tool-ask-user))的**表单呈现变体**:与原生共用同一条 `ctx.userQuestions` seam,工具描述与全部参数描述**与原生逐字相同**,只改表单本身的呈现——强制多选、并自动追加一道轮末补充题。因此它可以直接顶替原生工具而不带来任何提示词漂移,本身不施加任何额外纪律或约束。
3
+ DSH 原生 `ask_user_question`([`@deepseek-ai/dsh-tool-ask-user`](https://www.npmjs.com/package/@deepseek-ai/dsh-tool-ask-user))的**表单呈现变体**:与原生共用同一条 `ctx.userQuestions` seam,工具描述与共有参数描述**与原生逐字相同**,差异在工具名、参数集(本插件没有 `multi_select`,一律强制多选;另多出可选参数 `number`(题号)、`detail`(正文)与 `recommended`(推荐))与输出 schema(多出 `rejected`、`violations`、`error` 三个字段)。原生模式下发给模型的工具定义只含名称、描述与参数,共有参数的提示词与原生一致;PTC 模式下 SDK 声明会带上输出 schema,模型看到的类型与原生不同。它可以直接顶替原生工具,本身不施加任何额外纪律或约束。
4
4
 
5
5
  > ⚠️ **安装约束:本插件是 preset 工具行消费的 Cordis 插件,严禁加入 profile `package.json` 的 `dsh.profile.bundles`**(bundle 层必须在包内声明 `dsh.bundle`,本插件没有,加入会导致启动报错)。只需通过 `pnpm add` / `dsh plugin --profile web add` 进入 dependencies 即可,preset 的工具行会直接从 node_modules 解析本包。
6
6
 
7
7
  ## 特性
8
8
 
9
- - **ask_user_grilling**:与原生 `ask_user_question` 同形同名同描述,只改呈现。
9
+ - **ask_user_grilling**:与原生 `ask_user_question` 共用同一条 seam,工具描述与共有参数描述逐字相同,工具名与参数集不同。
10
10
  - **强制多选**:所有问题一律多选——schema 不提供 `multi_select` 参数,也就没有关闭开关。此行为刻意**不写入工具描述**(描述与原生逐字一致):模型不知道只能多选,才不会为避免互斥选项而影响出题质量;
11
+ - **题号并入标题**:可选参数 `number`(如 `"Q2"`)由代码并进 `header`,格式 `<number> · <header>`,只给一个时就是那一个值——散文预告里的 Q 编号与表单页标题因此能对上。`header` 已经以该题号开头时原样送出,不出现「`Q2 · Q2 · …`」这种重复前缀;
12
+ - **正文交给 `detail`**:可选参数 `detail` 透传给 seam。界面侧把 `question` 渲染成无 markdown、不换行的标题,`detail` 才走 markdown 正文块——多段正文塞在 `question` 里会挤成一坨大标题并把选项区往下顶,所以题干留一句、正文进 `detail`;
13
+ - **推荐标记归一化**:模型把推荐写成 `description` 末尾的「;推荐」、`label` 里的无括号写法,或直接给 `recommended: true`,都由本插件在投递前统一搬到 `label` 末尾的「(推荐)」。界面只按 `label` 末尾的括号写法判断,其他写法既不渲染徽标、标记还会原样显示成文字——搬到这里之后徽标才出现;回传答案时再把 `label` 还原成模型原本写的那一个,模型收到的仍是自己写的内容;
11
14
  - **轮末补充题**:每次调用由代码自动追加一道「这轮还有什么要补充或调整的吗?(无需补充)」,每题末尾的补充输入框则由 UI 自动渲染——两者都不依赖模型、也不写入描述,模型自加补充项只会与它们重复;
12
- - **id 前缀保护**:`__grill_` 开头的 id 会被拒绝(该前缀归自动追加的轮末补充题所有),保证两者不会撞 id;模型侧的 id 描述与原生一致,正常出题不会触发;
15
+ - **送不进表单的入参就地拒绝**:`__grill_` 前缀的 id(该前缀归轮末补充题)、同一轮里重复的 id、空白题干、空白选项 label、同一题内归一化后同名的两个选项——这几种都在投递前返回 `rejected` + `violations`,让模型改完重调,而不是把空选项、歧义选项画到界面上。界面按 label 认选项,重名的两个选项用户根本分不开,还原答案时也只会留下最后一个;题干与选项的分工仍是引导,不做校验;
13
16
  - **同一条 seam**:直接调 `ctx.userQuestions.ask`,与原生工具走同一通道、行为一致。
14
17
 
18
+ ## 自检
19
+
20
+ `pnpm test`(= `node scripts/test-recommendation.mjs && node scripts/smoke-host.mjs`):前者测推荐标记的归一化,后者用真 `defineTool` + 假 seam 跑 `execute()`,逐条断言上面的转换与拒绝。内核包(`@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-user-questions`)与界面侧副本从 DSH 运行副本 `~/.dsh/profiles/node_modules/` 解析(可用 `DSH_HOME` 改主目录);`scripts/recommended-label-rule.mjs` 抄着界面侧「哪个 label 算推荐」那条正则,`smoke-host.mjs` 断言它仍与安装副本逐字相同,上游一改就当场失败。
21
+
15
22
  ## 安装
16
23
 
17
24
  ```bash
@@ -20,4 +27,4 @@ dsh plugin --profile web add @lynn123411/dsh-ask-user-grilling
20
27
 
21
28
  已发布至 npm,供三个 matt 预设(`matt-standard` / `matt-ptc` / `matt-cordis`)使用(preset 的工具行直接消费本包)。
22
29
 
23
- **本地开发**:用 `link:`,不要拷贝——运行副本软链接到本仓库目录后,仓库里的文件就是“已安装内容”,改完即生效、不必发版:`cd ~/.dsh/profiles/web && pnpm add "link:<my-dsh 绝对路径>/plugins/dsh-ask-user-grilling" --offline`。本插件宿主半边 `import` 了 `@deepseek-ai/dsh-tools` 与 `@deepseek-ai/dsh-user-questions`;`link:` 的插件沿**仓库路径**解析这些裸包名,故仓库根 `node_modules/@deepseek-ai/` 必须备好这两个包(指向当前 DSH 版本里的同一份,升级后需重指)——做法见仓库 `docs/rules/plugins.md` 第 3 条。**不要**手工拷贝进 `node_modules/`:裸拷贝是未注册状态,`pnpm install` / `dsh plugin add|remove` 等任何 profile 同步都会把它当 extraneous 清掉,而 roster 对每份 preset 做行可解析性健康检查——插件一旦被剪,引用它的三份 matt preset 会整体从模式选择里消失。也**不要**把它加进 profile `package.json` 的 `dsh.profile.bundles`(bundle 层必须在包内声明 `dsh.bundle`,否则启动报错)。
30
+ **本地开发**:用 `link:`,不要拷贝——运行副本软链接到本仓库目录后,仓库里的文件就是“已安装内容”,改完即生效、不必发版:`cd ~/.dsh/profiles/web && pnpm add "link:<my-dsh 绝对路径>/plugins/dsh-ask-user-grilling" --offline`。本插件宿主半边 `import` 了 `@deepseek-ai/dsh-tools` 与 `@deepseek-ai/dsh-user-questions`;`link:` 插件的这些内核包由 DSH 自身路由供给,**无需在仓库里备 `node_modules`**(前提是 DSH 带 `--preserve-symlinks` 启动,启动器已默认带上——见仓库 `docs/rules/dev-copy.md`「内核包路由与 `--preserve-symlinks`」)。**不要**手工拷贝进 `node_modules/`:裸拷贝是未注册状态,`pnpm install` / `dsh plugin add|remove` 等任何 profile 同步都会把它当 extraneous 清掉,而 roster 对每份 preset 做行可解析性健康检查——插件一旦被剪,引用它的三份 matt preset 会整体从模式选择里消失。也**不要**把它加进 profile `package.json` 的 `dsh.profile.bundles`(bundle 层必须在包内声明 `dsh.bundle`,否则启动报错)。
package/lib/index.js CHANGED
@@ -1,19 +1,34 @@
1
1
  import { defineTool } from "@deepseek-ai/dsh-tools";
2
2
  import "@deepseek-ai/dsh-user-questions";
3
+ import { normalizeOption } from "./recommendation.js";
3
4
 
4
5
  /**
5
6
  * @lynn123411/dsh-ask-user-grilling — a presentation variant of the native
6
7
  * `ask_user_question` (`@deepseek-ai/dsh-tool-ask-user`). Same capability seam
7
- * (ctx.userQuestions) and the same tool/parameter descriptions verbatim; only
8
- * the rendered form differs.
8
+ * (ctx.userQuestions), the same tool description and the same descriptions for
9
+ * the shared parameters verbatim; only the rendered form differs.
9
10
  *
10
11
  * ask_user_grilling:
11
12
  * - forces multi-select on every question — the schema offers no opt-out
13
+ * - merges the optional `number` into the header as "<number> · <header>",
14
+ * so a form page carries the same Q-number the round announced in prose;
15
+ * a header that already opens with that number is sent as it is
16
+ * - passes the optional `detail` through to the seam: the client renders it as
17
+ * markdown under the question, while the question itself is a plain heading
18
+ * with no markdown and no line breaks, so a multi-paragraph body belongs in
19
+ * `detail`
20
+ * - normalizes the recommendation marker into the one form the client renders,
21
+ * a trailing "(推荐)" on the label: an explicit `recommended` flag, or a
22
+ * loose marker at the end of the label or the description, is moved there —
23
+ * the client reads only the label and accepts only the bracketed suffix
12
24
  * - appends a round-end supplement question; per-question supplement goes
13
25
  * through the built-in custom input ("Type your answer" / "输入你的答案"),
14
26
  * so no extra per-question option is added (it would duplicate that field)
15
- * - rejects question ids using the reserved `__grill_` prefix, so the
16
- * auto-appended question can never be shadowed
27
+ * - rejects what the form cannot carry: a question id under the reserved
28
+ * `__grill_` prefix, a duplicate question id, a blank question text, a blank
29
+ * option label, or two options of one question that end up with the same
30
+ * label (the client keys selection by label, so a collision cannot be
31
+ * restored or even clicked apart)
17
32
  */
18
33
  const name = "tool-ask-user-grilling";
19
34
  const inject = ["tools", "userQuestions"];
@@ -28,6 +43,26 @@ const ROUND_END_QUESTION = {
28
43
  multiSelect: true,
29
44
  };
30
45
 
46
+ /** 去掉首尾空白后是否为空。空串与纯空白都不值得送进表单。 */
47
+ function isBlank(value) {
48
+ return typeof value !== "string" || value.trim() === "";
49
+ }
50
+
51
+ /**
52
+ * 把题号并进标题:两者都给写作 `<number> · <header>`,只给一个就是那一个;
53
+ * 标题已经以该题号开头时原样送出去(模型常自己写成 "Q2 · Deadline")。
54
+ * @param {string} [number] - 题号。
55
+ * @param {string} [header] - 标题。
56
+ * @returns {string} 交给界面的标题;两者都缺时为空串(调用方据此省略该字段)。
57
+ */
58
+ function mergeNumberIntoHeader(number, header) {
59
+ const num = isBlank(number) ? undefined : number;
60
+ const head = isBlank(header) ? undefined : header;
61
+ if (num === undefined) return head ?? "";
62
+ if (head === undefined) return num;
63
+ return head.startsWith(num) ? head : `${num} · ${head}`;
64
+ }
65
+
31
66
  function apply(ctx) {
32
67
  ctx.tools.register(defineTool({
33
68
  name: "ask_user_grilling",
@@ -55,6 +90,14 @@ function apply(ctx) {
55
90
  type: "string",
56
91
  description: "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\".",
57
92
  },
93
+ number: {
94
+ type: "string",
95
+ description: "Optional question number, e.g. \"Q2\". Rendered before the header as \"Q2 · <header>\"; use the same Q-number you announced in the message text.",
96
+ },
97
+ detail: {
98
+ type: "string",
99
+ description: "Optional supporting body for this question. Rendered as markdown under the question, so put multi-paragraph context here and keep question to the one line that asks.",
100
+ },
58
101
  options: {
59
102
  type: "array",
60
103
  description: "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.",
@@ -71,6 +114,10 @@ function apply(ctx) {
71
114
  type: "string",
72
115
  description: "One sentence explaining the tradeoff or impact.",
73
116
  },
117
+ recommended: {
118
+ type: "boolean",
119
+ description: "Set true on the option you recommend (alternative to the \"(Recommended)\" label suffix).",
120
+ },
74
121
  },
75
122
  },
76
123
  },
@@ -90,7 +137,7 @@ function apply(ctx) {
90
137
  violations: {
91
138
  type: "array",
92
139
  items: { type: "string" },
93
- description: "Validation violations when rejected (reserved id prefix only).",
140
+ description: "Validation violations when rejected.",
94
141
  },
95
142
  error: {
96
143
  type: "string",
@@ -127,38 +174,78 @@ function apply(ctx) {
127
174
  render: (_args, value) => [{ type: "text", text: JSON.stringify(value) }],
128
175
  },
129
176
  async execute(args, exec) {
130
- // 1. input validation: reserved id prefix guard only (the round-end
131
- // question owns __grill_). Stem/option separation is guidance, NOT
132
- // enforced: substring matching would reject legitimate stems (e.g. a
133
- // stem that naturally mentions an option name), so no stem check may
134
- // refuse a round — a bad stem is preferable to a false rejection.
177
+ // 1. input validation: only what the form cannot represent at all. Stem /
178
+ // option separation stays guidance, NOT enforced: substring matching
179
+ // would reject legitimate stems (e.g. a stem that naturally mentions an
180
+ // option name), so no stem check may refuse a round — a bad stem is
181
+ // preferable to a false rejection.
135
182
  const violations = [];
136
- for (const question of args.questions) {
183
+ const seenIds = new Set();
184
+ args.questions.forEach((question, questionIndex) => {
185
+ const where = `Question ${questionIndex + 1} (id ${JSON.stringify(question.id)})`;
137
186
  if (typeof question.id === "string" && question.id.startsWith("__grill_")) {
138
- violations.push(`Question id "${question.id}" uses the reserved prefix __grill_`);
187
+ violations.push(`${where} uses the reserved prefix __grill_ (owned by the round-end supplement question)`);
139
188
  }
140
- }
189
+ if (typeof question.id === "string") {
190
+ if (seenIds.has(question.id)) violations.push(`${where} repeats an id already used in this round; ids must be unique because the answer is keyed by id`);
191
+ seenIds.add(question.id);
192
+ }
193
+ if (isBlank(question.question)) {
194
+ violations.push(`${where} has an empty question text; write the question itself in question and its body in detail`);
195
+ }
196
+ const seenLabels = new Set();
197
+ (question.options ?? []).forEach((option, optionIndex) => {
198
+ const label = normalizeOption(option).label;
199
+ if (isBlank(label)) {
200
+ violations.push(`${where}, option ${optionIndex + 1} has an empty label; every option needs a label the user can read and pick`);
201
+ return;
202
+ }
203
+ if (seenLabels.has(label)) {
204
+ violations.push(`${where} has two options that render with the same label ${JSON.stringify(label)}; the form identifies a choice by its label, so make them differ`);
205
+ return;
206
+ }
207
+ seenLabels.add(label);
208
+ });
209
+ });
141
210
  if (violations.length > 0) {
142
211
  return {
143
212
  rejected: true,
144
213
  violations,
145
- error: "Question ids must not use the reserved prefix __grill_ (reserved for the round-end supplement question). Fix the ids and call this tool again.",
214
+ error: `${violations.length} violation(s) in this round: ids must be unique and must not use the reserved __grill_ prefix, question text must not be blank, and the options of one question must not share a label. Fix them and call this tool again.`,
215
+ answers: [],
146
216
  };
147
217
  }
148
218
 
149
- // 2. transform: force multi-select; per-question supplement is via the built-in custom input ("Type your answer"/"输入你的答案") — no extra option is added to avoid duplication with that field
150
- const questions = args.questions.map((question) => ({
151
- id: question.id,
152
- question: question.question,
153
- ...(question.header !== undefined ? { header: question.header } : {}),
154
- options: [
155
- ...(question.options ?? []).map((option) => ({
156
- label: option.label,
157
- ...(option.description !== undefined ? { description: option.description } : {}),
158
- })),
159
- ],
160
- multiSelect: true,
161
- }));
219
+ // 2. transform: force multi-select; merge the optional number into the
220
+ // header; hand detail to the field the client renders as markdown; move
221
+ // the recommendation marker to the label suffix the client renders;
222
+ // per-question supplement is via the built-in custom input ("Type your
223
+ // answer"/"输入你的答案") — no extra option is added to avoid
224
+ // duplication with that field
225
+ const labelRestore = new Map();
226
+ const questions = args.questions.map((question) => {
227
+ const header = mergeNumberIntoHeader(question.number, question.header);
228
+ const restore = new Map();
229
+ const options = (question.options ?? []).map((option) => {
230
+ const normalized = normalizeOption(option);
231
+ if (normalized.label !== normalized.originalLabel) {
232
+ restore.set(normalized.label, normalized.originalLabel);
233
+ }
234
+ return {
235
+ label: normalized.label,
236
+ ...(normalized.description !== undefined ? { description: normalized.description } : {}),
237
+ };
238
+ });
239
+ labelRestore.set(question.id, restore);
240
+ return {
241
+ id: question.id,
242
+ question: question.question,
243
+ ...(header !== "" ? { header } : {}),
244
+ ...(isBlank(question.detail) ? {} : { detail: question.detail }),
245
+ options,
246
+ multiSelect: true,
247
+ };
248
+ });
162
249
 
163
250
  // 3. round-end supplement question — single "无需补充" option; supplement is via custom input, so no "I have something to add" option (duplicates that field)
164
251
  questions.push({
@@ -173,11 +260,14 @@ function apply(ctx) {
173
260
  signal: exec.signal,
174
261
  });
175
262
  return {
176
- answers: answer.answers.map((entry) => ({
177
- id: entry.id,
178
- selected: [...entry.selected],
179
- ...(entry.custom !== undefined ? { custom: entry.custom } : {}),
180
- })),
263
+ answers: answer.answers.map((entry) => {
264
+ const restore = labelRestore.get(entry.id);
265
+ return {
266
+ id: entry.id,
267
+ selected: entry.selected.map((label) => restore?.get(label) ?? label),
268
+ ...(entry.custom !== undefined ? { custom: entry.custom } : {}),
269
+ };
270
+ }),
181
271
  };
182
272
  },
183
273
  }));
@@ -0,0 +1,53 @@
1
+ // 推荐标记的归一化。
2
+ //
3
+ // 客户端(@deepseek-ai/dsh-client-ui-user-questions 的 parseRecommendedLabel)
4
+ // 只有一个判定依据:option.label 必须以半角或全角的括号推荐字样收尾;option.description
5
+ // 完全不参与。模型经常把推荐写成 description 末尾的「;推荐」,或者写成 label 里不带
6
+ // 括号的形式,于是徽标不渲染、标记原样显示成文字。
7
+ //
8
+ // 这里在题目交给 ctx.userQuestions.ask 之前把标记统一搬到 label 末尾的「(推荐)」,
9
+ // 客户端就能认出并渲染徽标。显式字段 recommended 是另一条更明确的信号,优先于文字。
10
+
11
+ const MARKER = 'recommended|推荐';
12
+ // 半角/全角圆括号与方括号/方头括号
13
+ const BRACKETED_MARKER = `[((【\\[]\\s*(?:${MARKER})\\s*[))】\\]]`;
14
+ // 无括号写法必须由分隔符引出,否则「查看推荐」这种正常文字会被误判
15
+ const SEPARATOR = ';;,,、::\\-—';
16
+ const TRAILING_PUNCTUATION = '。.!!??;;,,、~~\\s';
17
+
18
+ const BRACKETED_SUFFIX = new RegExp(`[\\s]*[${SEPARATOR}]?\\s*${BRACKETED_MARKER}[${TRAILING_PUNCTUATION}]*$`, 'i');
19
+ const BARE_SUFFIX = new RegExp(`[\\s]*[${SEPARATOR}]\\s*(?:${MARKER})[${TRAILING_PUNCTUATION}]*$`, 'i');
20
+
21
+ /** 供测试核对客户端规则的推荐标记后缀;客户端只认 label 末尾的这两种括号写法。 */
22
+ export const RECOMMENDED_LABEL_SUFFIX = '(推荐)';
23
+
24
+ /**
25
+ * 去掉文本末尾的推荐标记。
26
+ * @param {string} text - label 或 description 原文。
27
+ * @returns {{ text: string, found: boolean }} 去掉标记后的文本,以及是否找到标记。
28
+ */
29
+ export function stripRecommendationMarker(text) {
30
+ if (typeof text !== 'string') return { text, found: false };
31
+ const match = BRACKETED_SUFFIX.exec(text) ?? BARE_SUFFIX.exec(text);
32
+ if (match === null) return { text, found: false };
33
+ return { text: text.slice(0, match.index).replace(/\s+$/, ''), found: true };
34
+ }
35
+
36
+ /**
37
+ * 把一个 option 归一化成客户端能渲染的形式。
38
+ * @param {{ label: string, description?: string, recommended?: boolean }} option - 模型给的选项。
39
+ * @returns {{ originalLabel: string, label: string, description?: string, recommended: boolean }}
40
+ * 交给界面时用 label/description;回传答案时用 originalLabel 还原模型原本写的 label。
41
+ */
42
+ export function normalizeOption(option) {
43
+ const label = stripRecommendationMarker(option.label);
44
+ const description = option.description === undefined ? undefined : stripRecommendationMarker(option.description);
45
+ const recommended = option.recommended === true || label.found || (description !== undefined && description.found);
46
+ const cleanLabel = label.text;
47
+ return {
48
+ originalLabel: option.label,
49
+ label: recommended ? `${cleanLabel}${RECOMMENDED_LABEL_SUFFIX}` : cleanLabel,
50
+ description: description === undefined ? undefined : description.text,
51
+ recommended,
52
+ };
53
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-ask-user-grilling",
3
- "version": "0.2.6",
4
- "description": "Form-presentation variant of the native ask_user_question: same tool and parameter descriptions verbatim, forced multi-select, plus an auto-appended round-end supplement question.",
3
+ "version": "0.5.0",
4
+ "description": "Form-presentation variant of the native ask_user_question: same tool description and shared-parameter descriptions verbatim, forced multi-select, an optional question number merged into the header, an optional markdown detail body, a recommendation marker normalized into the one label suffix the client renders, plus an auto-appended round-end supplement question.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -13,12 +13,17 @@
13
13
  "README.md"
14
14
  ],
15
15
  "license": "MIT",
16
+ "scripts": {
17
+ "test": "node scripts/test-recommendation.mjs && node scripts/smoke-host.mjs",
18
+ "check": "node --check lib/index.js && node --check lib/recommendation.js && pnpm test"
19
+ },
16
20
  "peerDependencies": {
17
21
  "@deepseek-ai/cordis": "^4.0.2",
18
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
19
- "@deepseek-ai/dsh-user-questions": "^0.1.5-rc.1"
22
+ "@deepseek-ai/dsh-tools": "0.1.6-alpha.2",
23
+ "@deepseek-ai/dsh-user-questions": "0.1.6-alpha.2"
20
24
  },
21
25
  "engines": {
26
+ "dsh": "0.1.6-alpha.2",
22
27
  "node": "^22.19 || >=24"
23
28
  },
24
29
  "publishConfig": {
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 liceses
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.