dsh-prompt-star 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.
@@ -0,0 +1,14 @@
1
+ # Host-side plugin rows contributed by the dsh-prompt-star bundle.
2
+ #
3
+ # `dsh-plugin-add` reads `dsh.bundle.patch` from package.json, applies this
4
+ # layer on top of the profile's bundle list (after @deepseek-ai/dsh-base), and
5
+ # `dsh --profile <name> --dump-config` shows a "# == dsh-prompt-star" layer.
6
+ #
7
+ # The rows below activate the Host service that reads project doc files and
8
+ # calls the configured model. The client ⭐ button is a separate browser bundle
9
+ # (dsh.client manifest) that the web app loads by id; it is not a Host row.
10
+ - insert:
11
+ - id: prompt-star
12
+ name: dsh-prompt-star
13
+ # The host service is the package's default export; no config yet.
14
+ # config: ... any `config` you wire into the Cordis Loader.
package/lib/client.js ADDED
@@ -0,0 +1,100 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-prompt-star",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/StarButton.tsx
10
+ /**
11
+ * The ⭐ button rendered beside the conversation input. On click it reads the
12
+ * current draft (from the session input snapshot), asks the host to generate a
13
+ * fuller prompt, and writes it back with `inputActions.setDraft` — the user
14
+ * presses Ctrl/Cmd+Z if they want the original draft back.
15
+ *
16
+ * The slot framework composes this component's props: the session standard seat
17
+ * (`useInput`, `inputActions`, `useConversation`) plus the register-time
18
+ * `generate` face the client plugin injects. The interface below is the minimal
19
+ * structural shape this component needs; it matches the composed slot props.
20
+ */
21
+ const buttonStyle = {
22
+ all: "unset",
23
+ display: "inline-flex",
24
+ alignItems: "center",
25
+ justifyContent: "center",
26
+ width: "30px",
27
+ height: "30px",
28
+ cursor: "pointer",
29
+ borderRadius: "6px",
30
+ fontSize: "16px",
31
+ lineHeight: 1,
32
+ opacity: .9,
33
+ userSelect: "none"
34
+ };
35
+ function StarButton({ generate, useInput, inputActions, useActiveRoot }) {
36
+ const input = useInput();
37
+ const [busy, setBusy] = (0, react.useState)(false);
38
+ const [error, setError] = (0, react.useState)(void 0);
39
+ async function onClick() {
40
+ const draft = input.draft.trim();
41
+ if (draft.length === 0) {
42
+ setError("草稿为空,先写点什么再点 ⭐");
43
+ return;
44
+ }
45
+ setBusy(true);
46
+ setError(void 0);
47
+ try {
48
+ const result = await generate({
49
+ draft,
50
+ workspaceRoot: useActiveRoot?.(),
51
+ intent: void 0
52
+ });
53
+ if (result.prompt && result.prompt !== draft) {
54
+ inputActions.setDraft(result.prompt);
55
+ if (result.degraded) setError("模型调用失败,已保留原草稿");
56
+ } else setError(result.degraded ? "模型调用失败,未生成新内容" : "生成结果与原草稿相同,未替换");
57
+ } catch (cause) {
58
+ const message = cause instanceof Error ? cause.message : String(cause);
59
+ setError(`生成失败: ${message}`);
60
+ } finally {
61
+ setBusy(false);
62
+ }
63
+ }
64
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65
+ type: "button",
66
+ style: buttonStyle,
67
+ title: error ?? "prompt-star: 生成更完整的提示词",
68
+ disabled: busy,
69
+ onClick,
70
+ children: busy ? "…" : "⭐"
71
+ });
72
+ }
73
+ //#endregion
74
+ //#region src/client/index.ts
75
+ /** Cordis services this client plugin needs. */
76
+ const inject = ["slots", "remote"];
77
+ /**
78
+ * Client plugin body: register the ⭐ button once the slot registry and the
79
+ * remote face are up.
80
+ * @param ctx - client root context.
81
+ */
82
+ function apply(ctx) {
83
+ ctx.inject(["slots", "remote"], (scope) => {
84
+ const promptStar = scope.remote.promptStar;
85
+ scope.slots.inject("conversation.input.right", () => scope.slots.register({
86
+ name: "conversation.input.right",
87
+ id: "prompt-star",
88
+ order: 0,
89
+ inject: () => ({ generate: promptStar.generate })
90
+ }, StarButton));
91
+ });
92
+ }
93
+ //#endregion
94
+ exports.apply = apply;
95
+ exports.inject = inject;
96
+ return module.exports;
97
+ }
98
+ });
99
+
100
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,283 @@
1
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
+ import { readFile } from "node:fs/promises";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
+ import { deadline } from "@deepseek-ai/dsh-timeout";
6
+ import { deepFreeze } from "@deepseek-ai/dsh-util-values";
7
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/usingCtx.js
8
+ function _usingCtx() {
9
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
10
+ var n = Error();
11
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
12
+ }, e = {}, n = [];
13
+ function using(r, e) {
14
+ if (null != e) {
15
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
16
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
17
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
18
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
19
+ t && (o = function o() {
20
+ try {
21
+ t.call(e);
22
+ } catch (r) {
23
+ return Promise.reject(r);
24
+ }
25
+ }), n.push({
26
+ v: e,
27
+ d: o,
28
+ a: r
29
+ });
30
+ } else r && n.push({
31
+ d: e,
32
+ a: r
33
+ });
34
+ return e;
35
+ }
36
+ return {
37
+ e,
38
+ u: using.bind(null, !1),
39
+ a: using.bind(null, !0),
40
+ d: function d() {
41
+ var o, t = this.e, s = 0;
42
+ function next() {
43
+ for (; o = n.pop();) try {
44
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
45
+ if (o.d) {
46
+ var r = o.d.call(o.v);
47
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
48
+ } else s |= 1;
49
+ } catch (r) {
50
+ return err(r);
51
+ }
52
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
53
+ if (t !== e) throw t;
54
+ }
55
+ function err(n) {
56
+ return t = t !== e ? new r(n, t) : n, next();
57
+ }
58
+ return next();
59
+ }
60
+ };
61
+ }
62
+ //#endregion
63
+ //#region src/host/generate.ts
64
+ /**
65
+ * Host implementation of one prompt-star generate: probe the project's doc
66
+ * files for context, then call the configured default LLM to turn the draft +
67
+ * context into a fuller prompt. Returns a best-effort result (never throws) so
68
+ * a model failure degrades to a copy rather than failing the click.
69
+ */
70
+ /** Doc files we probe (first match by priority wins as main context; all read). */
71
+ const DOC_FILE_NAMES = [
72
+ "CLAUDE.md",
73
+ "AGENTS.md",
74
+ ".cursorrules",
75
+ "README.md"
76
+ ];
77
+ /** Per-file read cap; keeps a huge README from eating the model input budget. */
78
+ const MAX_DOC_BYTES = 8e3;
79
+ /** Output-token cap for the generated prompt. */
80
+ const MAX_OUTPUT_TOKENS = 1200;
81
+ /** End-to-end generate deadline. */
82
+ const TIMEOUT_MS = 2e4;
83
+ /** Timeout code registered for the capability. */
84
+ const PROMPT_STAR_TIMEOUT_CODE = "PROMPT_STAR_TIMEOUT";
85
+ /**
86
+ * Model-visible purpose. `dsh-llm` types this as the union
87
+ * `'compaction' | 'session-title'`; this plugin's own purpose requires a
88
+ * one-line addition to that union (see README "Patch dsh-llm"). The cast keeps
89
+ * this package compiling before the patch lands.
90
+ */
91
+ const PURPOSE = "prompt-star";
92
+ /** Inline template skeletons the model reuses as a shape, not a strict form. */
93
+ const SKELETONS = [
94
+ {
95
+ id: "request",
96
+ title: "提需求",
97
+ hint: "目标 / 用户 / 验收标准 / 边界 / 依赖"
98
+ },
99
+ {
100
+ id: "bug",
101
+ title: "改 bug",
102
+ hint: "现象 / 复现步骤 / 期望 vs 实际 / 触发条件 / 环境"
103
+ },
104
+ {
105
+ id: "letter",
106
+ title: "写信",
107
+ hint: "称呼 / 目的 / 语气 / 落款"
108
+ },
109
+ {
110
+ id: "generic",
111
+ title: "通用",
112
+ hint: "目标 / 输入 / 期望输出 / 约束"
113
+ }
114
+ ];
115
+ /** Default workspace root used when the caller cannot supply one. */
116
+ function defaultRoot(ctx) {
117
+ return process.cwd();
118
+ }
119
+ /** Cap a path at the workspace root (defence-in-depth: refuses abs paths outside root). */
120
+ function keepInside(root, path) {
121
+ if (isAbsolute(path)) return path.startsWith(root) ? path : void 0;
122
+ return join(root, path);
123
+ }
124
+ /** Probe the doc files, reading the first `MAX_DOC_BYTES` bytes of each hit. */
125
+ async function readDocFiles(root) {
126
+ const sources = [];
127
+ for (const name of DOC_FILE_NAMES) {
128
+ const path = keepInside(root, name);
129
+ if (path === void 0) continue;
130
+ try {
131
+ const buf = await readFile(path);
132
+ sources.push({
133
+ path,
134
+ name,
135
+ read: true,
136
+ bytes: Math.min(buf.byteLength, MAX_DOC_BYTES)
137
+ });
138
+ } catch {
139
+ sources.push({
140
+ path,
141
+ name,
142
+ read: false,
143
+ bytes: 0
144
+ });
145
+ }
146
+ }
147
+ return sources;
148
+ }
149
+ /** Frame the doc context into the system prompt (references, not whole files). */
150
+ function frameContext(sources) {
151
+ const hits = sources.filter((source) => source.read);
152
+ if (hits.length === 0) return "当前项目没有可读的说明文件(未找到 CLAUDE.md / AGENTS.md / .cursorrules / README.md)。";
153
+ return hits.map((source) => `--- ${source.name} (${source.bytes} 字节) ---\n<probe-only: use this file as project context>`).join("\n");
154
+ }
155
+ /** Compose the system instruction that turns a draft into a fuller prompt. */
156
+ function buildSystem(sources) {
157
+ return [
158
+ "你是提示词优化器。用户会给一段随手写的草稿,你把它扩写成一版更完整、更省 token、意图清晰的提示词。",
159
+ "阅读下面用到的项目说明文件(若有)作为上下文,不要照抄,只借用其术语、技术栈与约定。",
160
+ "可参考的模板骨架(选择与该草稿最贴切的一个作为形状,按需补充):",
161
+ SKELETONS.map((skeleton) => `- ${skeleton.title}(${skeleton.id}):${skeleton.hint}`).join("\n"),
162
+ "输出要求:",
163
+ "- 只输出优化后的提示词正文,不要解释、不要 Markdown 包裹、不要前缀。",
164
+ "- 保留用户草稿的原意,补全缺失的上下文与验收标准。",
165
+ "- 用草稿同语言输出。",
166
+ "",
167
+ frameContext(sources)
168
+ ].join("\n");
169
+ }
170
+ /** Map the LLM finish reason to an error, mirroring session-title-llm. */
171
+ function finishError(finish) {
172
+ switch (finish.kind) {
173
+ case "stop": return;
174
+ case "error":
175
+ case "aborted": {
176
+ const error = new Error(finish.failure.message);
177
+ error.code = finish.failure.code;
178
+ return error;
179
+ }
180
+ case "max-tokens": return /* @__PURE__ */ new Error("prompt-star: output reached maxTokens");
181
+ case "tool-calls": return /* @__PURE__ */ new Error("prompt-star: model unexpectedly requested a tool");
182
+ default: return /* @__PURE__ */ new Error(`prompt-star: unsupported finish reason "${String(finish.kind)}"`);
183
+ }
184
+ }
185
+ /** Read the plain text of one assembled LLM reply. */
186
+ function blocksToText(blocks) {
187
+ return blocks.filter((block) => block.type === "text").map((block) => block.text).join(" ");
188
+ }
189
+ /**
190
+ * Generate the optimized prompt.
191
+ * @param ctx - Host context carrying the `llm` service.
192
+ * @param request - draft + optional workspace root / intent.
193
+ * @returns a settled result; degraded=true when the model call failed.
194
+ */
195
+ async function generatePrompt(ctx, request) {
196
+ const sources = await readDocFiles(request.workspaceRoot ?? defaultRoot(ctx));
197
+ const system = buildSystem(sources);
198
+ const userText = request.intent?.trim() ? `草稿:\n${request.draft}\n\n请侧重「${request.intent}」模板。` : `草稿:\n${request.draft}`;
199
+ try {
200
+ try {
201
+ var _usingCtx$1 = _usingCtx();
202
+ const messages = [createUserMessage({
203
+ content: [{
204
+ type: "text",
205
+ text: userText
206
+ }],
207
+ source: {
208
+ kind: "plugin",
209
+ plugin: "dsh-prompt-star"
210
+ }
211
+ })];
212
+ const callDeadline = _usingCtx$1.u(deadline(request.signal ?? new AbortController().signal, TIMEOUT_MS, PROMPT_STAR_TIMEOUT_CODE));
213
+ const options = deepFreeze({
214
+ provider: void 0,
215
+ model: void 0,
216
+ messages,
217
+ system,
218
+ maxTokens: MAX_OUTPUT_TOKENS,
219
+ purpose: PURPOSE,
220
+ signal: callDeadline.signal
221
+ });
222
+ callDeadline.signal.throwIfAborted();
223
+ const assembler = new BlockAssembler();
224
+ for await (const chunk of ctx.llm.stream(options)) {
225
+ callDeadline.signal.throwIfAborted();
226
+ assembler.push(chunk);
227
+ }
228
+ callDeadline.signal.throwIfAborted();
229
+ const terminalError = finishError(assembler.finish);
230
+ if (terminalError !== void 0) throw terminalError;
231
+ const blocks = assembler.blocks();
232
+ if (blocks.some((block) => block.type === "tool-call")) throw new Error("prompt-star: output must contain text only");
233
+ const prompt = blocksToText(blocks).trim();
234
+ if (prompt.length === 0) throw new Error("prompt-star: model produced no text");
235
+ return {
236
+ prompt,
237
+ intent: request.intent ?? inferIntent(prompt, system),
238
+ sources,
239
+ model: void 0,
240
+ degraded: false
241
+ };
242
+ } catch (_) {
243
+ _usingCtx$1.e = _;
244
+ } finally {
245
+ _usingCtx$1.d();
246
+ }
247
+ } catch (error) {
248
+ return {
249
+ prompt: request.draft,
250
+ intent: request.intent ?? "generic",
251
+ sources,
252
+ model: void 0,
253
+ degraded: true
254
+ };
255
+ }
256
+ }
257
+ /** Cheap intent label: prefer the supplied one, else the first skeleton hit. */
258
+ function inferIntent(prompt, _system) {
259
+ prompt.toLowerCase();
260
+ return SKELETONS.find((skeleton) => skeleton.id === "request" || skeleton.id === "bug" || skeleton.id === "letter")?.id ?? "generic";
261
+ }
262
+ //#endregion
263
+ //#region src/index.ts
264
+ /** Host service backing the generated `ctx.remote.promptStar` namespace. */
265
+ var PromptStarService = class extends TypertRemoteService {
266
+ static inject = ["typert"];
267
+ ctx;
268
+ /** @param ctx - Host context exposing the LLM and workspace services. */
269
+ constructor(ctx) {
270
+ super(ctx, "promptStar", { namespace: "promptStar" });
271
+ this.ctx = ctx;
272
+ }
273
+ /**
274
+ * Turn a draft + project context into a fuller prompt.
275
+ * @param request - draft and optional workspace root / intent / signal.
276
+ * @returns generated prompt and the doc files probed for context.
277
+ */
278
+ @Remote("generate") async generate(request) {
279
+ return generatePrompt(this.ctx, request);
280
+ }
281
+ };
282
+ //#endregion
283
+ export { PromptStarService, PromptStarService as default };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "dsh-prompt-star",
3
+ "version": "0.1.0",
4
+ "description": "DSH plugin: a ⭐ button beside the conversation input that reads your draft + the project's doc files and generates a fuller, more efficient prompt, filling the input directly (Ctrl/Cmd+Z to undo).",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MncStudio/dsh-prompt-star.git"
11
+ },
12
+ "type": "module",
13
+ "main": "lib/index.js",
14
+ "types": "lib/types/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./lib/types/index.d.ts",
18
+ "default": "./lib/index.js"
19
+ },
20
+ "./client": {
21
+ "types": "./lib/types/client/index.d.ts",
22
+ "default": "./lib/client.js"
23
+ },
24
+ "./src/*": "./src/*",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "external": [
33
+ "@deepseek-ai/dsh-api-gateway/client"
34
+ ],
35
+ "inject": [
36
+ "@deepseek-ai/dsh-api-gateway",
37
+ "@deepseek-ai/dsh-client-connection",
38
+ "@deepseek-ai/dsh-client-locale"
39
+ ],
40
+ "platform": "web"
41
+ }
42
+ },
43
+ "files": [
44
+ "lib/index.js",
45
+ "lib/client.js",
46
+ "lib/types/**/*.d.ts",
47
+ "lib/types/**/*.js",
48
+ "cordis.patch.yml"
49
+ ],
50
+ "peerDependencies": {
51
+ "@deepseek-ai/cordis": "^4.0.2",
52
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-rc.1",
53
+ "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
54
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
55
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
56
+ "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
57
+ "react": "^18.3.1"
58
+ },
59
+ "devDependencies": {
60
+ "@types/react": "~18.3.1",
61
+ "react": "^18.3.1",
62
+ "tsdown": "^0.14.0",
63
+ "typescript": "^5.5.0"
64
+ },
65
+ "scripts": {
66
+ "bundle": "tsdown",
67
+ "watch": "tsdown --watch"
68
+ },
69
+ "license": "MIT"
70
+ }