@xia-sc/dsh-git 0.5.3 → 0.6.1
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.en.md +34 -10
- package/README.md +22 -9
- package/lib/client.js +618 -254
- package/lib/index.js +63 -20
- package/package.json +6 -3
package/lib/index.js
CHANGED
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
* `/api` transport — loopback or a configured trusted authority, same-origin.
|
|
50
50
|
*/
|
|
51
51
|
import { execFile } from "node:child_process";
|
|
52
|
-
import { randomUUID } from "node:crypto";
|
|
53
52
|
import { isAbsolute } from "node:path";
|
|
54
53
|
|
|
55
54
|
/** Stable Cordis plugin name. */
|
|
@@ -95,6 +94,12 @@ const LLM_STAT_MAX_CHARS = 2000;
|
|
|
95
94
|
const LLM_MAX_TOKENS = 8192;
|
|
96
95
|
/** Accepted `generateMessage` modes. */
|
|
97
96
|
const GENERATE_MODES = ["staged", "unstaged", "all"];
|
|
97
|
+
/**
|
|
98
|
+
* Longest session id this plugin will forward to the LLM service. Real ids are
|
|
99
|
+
* short branded strings; the cap only keeps a hostile caller from stuffing
|
|
100
|
+
* arbitrary text into a request header.
|
|
101
|
+
*/
|
|
102
|
+
const SESSION_ID_MAX_CHARS = 200;
|
|
98
103
|
/** Default mode: the only one whose content is what `commit` will actually record. */
|
|
99
104
|
const GENERATE_MODE_DEFAULT = "staged";
|
|
100
105
|
|
|
@@ -682,8 +687,10 @@ function endpointFromPath(pathname) {
|
|
|
682
687
|
|
|
683
688
|
/**
|
|
684
689
|
* Read a request body with a hard byte cap.
|
|
685
|
-
*
|
|
686
|
-
*
|
|
690
|
+
* Deliberately uses the classic data/end/error events rather than async
|
|
691
|
+
* iteration: the host's own bridge does `for await (const chunk of req)`, and
|
|
692
|
+
* keeping this reader on the event API means one dependency fewer on an
|
|
693
|
+
* IncomingMessage behaviour this plugin cannot pin.
|
|
687
694
|
* @param req - node:http request.
|
|
688
695
|
* @param maxBytes - cap; exceeding it resolves to undefined.
|
|
689
696
|
* @returns the utf8 body, or undefined on overrun/error/abort.
|
|
@@ -1038,17 +1045,21 @@ async function resolveLlmRoute(ctx, rawProvider, rawModel) {
|
|
|
1038
1045
|
* Hand-built rather than imported from `@deepseek-ai/dsh-llm`: this package
|
|
1039
1046
|
* deliberately declares no `@deepseek-ai/*` runtime imports, because a plugin
|
|
1040
1047
|
* installed with pnpm `link:` resolves them from its own real source path,
|
|
1041
|
-
* where no host tree exists.
|
|
1042
|
-
*
|
|
1048
|
+
* where no host tree exists.
|
|
1049
|
+
*
|
|
1050
|
+
* The shape is the host's documented hand-built one-shot input
|
|
1051
|
+
* (`RequestUserInput`): the `user` role plus one text block, and deliberately
|
|
1052
|
+
* **no `id` and no `source`**. dsh >= 0.1.7 retired the catch-all `plugin`
|
|
1053
|
+
* source kind — `MessageSourceMap` has no such member ("each producer declares
|
|
1054
|
+
* its own `kind` in its own module; there is no shared catch-all `plugin`
|
|
1055
|
+
* kind", `dsh-llm/lib/types/message.d.ts`), and Session format v4 refuses
|
|
1056
|
+
* `{kind:"plugin"}` outright (`dsh-session-format-v3-to-v4`). A one-shot
|
|
1057
|
+
* request needs no durable identity: only `provider`/`model`/`messages`/… are
|
|
1058
|
+
* read off {@link GenerateOptions}.
|
|
1043
1059
|
* @param text - model-facing prompt body.
|
|
1044
1060
|
*/
|
|
1045
1061
|
function generationMessage(text) {
|
|
1046
|
-
return
|
|
1047
|
-
id: randomUUID(),
|
|
1048
|
-
role: "user",
|
|
1049
|
-
content: Object.freeze([Object.freeze({ type: "text", text })]),
|
|
1050
|
-
source: Object.freeze({ kind: "plugin", plugin: name })
|
|
1051
|
-
});
|
|
1062
|
+
return { role: "user", content: [{ type: "text", text }] };
|
|
1052
1063
|
}
|
|
1053
1064
|
|
|
1054
1065
|
/**
|
|
@@ -1057,18 +1068,27 @@ function generationMessage(text) {
|
|
|
1057
1068
|
* @param prompt - `{ system, body }` from {@link generationPrompt}.
|
|
1058
1069
|
* @param route - `{ provider, model }` from {@link resolveLlmRoute}.
|
|
1059
1070
|
* @param signal - cancellation (browser abort plus the server ceiling).
|
|
1071
|
+
* @param sessionId - the caller's session, when it has one. Forwarded as
|
|
1072
|
+
* `GenerateOptions.sessionId`; adapters that need session affinity (they
|
|
1073
|
+
* receive it only when it is present — see `dsh-llm-pi-ai`'s options mapping)
|
|
1074
|
+
* reject an unidentified request outright, so an AI draft from the panel must
|
|
1075
|
+
* carry one.
|
|
1060
1076
|
* @returns the trimmed message.
|
|
1061
1077
|
* @throws on a terminal stream failure, an output cap hit before any text
|
|
1062
1078
|
* existed (`llm-truncated`), or genuinely empty output (`llm-empty`).
|
|
1063
1079
|
*/
|
|
1064
|
-
async function requestCommitMessage(ctx, prompt, route, signal) {
|
|
1080
|
+
async function requestCommitMessage(ctx, prompt, route, signal, sessionId = undefined) {
|
|
1065
1081
|
const options = {
|
|
1066
1082
|
provider: route.provider,
|
|
1067
1083
|
model: route.model,
|
|
1068
1084
|
messages: [generationMessage(prompt.body)],
|
|
1069
1085
|
system: prompt.system,
|
|
1070
1086
|
maxTokens: LLM_MAX_TOKENS,
|
|
1071
|
-
signal
|
|
1087
|
+
signal,
|
|
1088
|
+
// Session-labelled routes (an "opencode go"-style gateway is the local
|
|
1089
|
+
// example) answer 400 when the request carries no session identity, and
|
|
1090
|
+
// the host's pi-ai adapter forwards this field only when it is present.
|
|
1091
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
1072
1092
|
};
|
|
1073
1093
|
// Final text comes from the assembled blocks; the delta map is a fallback for
|
|
1074
1094
|
// an adapter that emits text without a closing `block-end`.
|
|
@@ -1108,13 +1128,20 @@ async function requestCommitMessage(ctx, prompt, route, signal) {
|
|
|
1108
1128
|
* @param rawMode - `staged`, `unstaged`, or `all`.
|
|
1109
1129
|
* @param rawProvider - caller's provider route (optional).
|
|
1110
1130
|
* @param rawModel - caller's model id (optional).
|
|
1131
|
+
* @param rawSessionId - caller's session id (optional; see
|
|
1132
|
+
* {@link requestCommitMessage} for why a session-scoped caller must send it).
|
|
1111
1133
|
* @param signal - transport cancellation.
|
|
1112
1134
|
*/
|
|
1113
|
-
async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, signal) {
|
|
1135
|
+
async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, rawSessionId, signal) {
|
|
1114
1136
|
if (!validCwd(rawCwd)) return fail("invalid-cwd", "a valid absolute working directory is required");
|
|
1115
1137
|
if (rawMode !== undefined && rawMode !== null && !GENERATE_MODES.includes(rawMode)) {
|
|
1116
1138
|
return fail("invalid-mode", `mode must be one of ${GENERATE_MODES.join(", ")}`);
|
|
1117
1139
|
}
|
|
1140
|
+
if (rawSessionId !== undefined && rawSessionId !== null
|
|
1141
|
+
&& (typeof rawSessionId !== "string" || rawSessionId === "" || rawSessionId.length > SESSION_ID_MAX_CHARS)) {
|
|
1142
|
+
return fail("invalid-session", "sessionId must be a non-empty string when present");
|
|
1143
|
+
}
|
|
1144
|
+
const sessionId = typeof rawSessionId === "string" ? rawSessionId : undefined;
|
|
1118
1145
|
const mode = rawMode ?? GENERATE_MODE_DEFAULT;
|
|
1119
1146
|
const changes = await readChangesForMode(rawCwd, mode, signal);
|
|
1120
1147
|
if (changes.ok !== true) return changes;
|
|
@@ -1138,10 +1165,16 @@ async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, s
|
|
|
1138
1165
|
const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
1139
1166
|
if (combined.aborted) return fail("cancelled", "generation was cancelled", { mode });
|
|
1140
1167
|
try {
|
|
1141
|
-
const message = await requestCommitMessage(ctx, prompt, route, combined);
|
|
1168
|
+
const message = await requestCommitMessage(ctx, prompt, route, combined, sessionId);
|
|
1142
1169
|
return ok({ message, mode, provider: route.provider, model: route.model });
|
|
1143
1170
|
} catch (error) {
|
|
1144
|
-
|
|
1171
|
+
// The route rides along so the panel can name the provider that failed:
|
|
1172
|
+
// a route-level gateway rejection otherwise reads as a plugin bug.
|
|
1173
|
+
return fail(error.pluginCode ?? "llm-failed", error instanceof Error ? error.message : String(error), {
|
|
1174
|
+
mode,
|
|
1175
|
+
provider: route.provider,
|
|
1176
|
+
model: route.model
|
|
1177
|
+
});
|
|
1145
1178
|
}
|
|
1146
1179
|
}
|
|
1147
1180
|
|
|
@@ -1187,7 +1220,7 @@ async function dispatch(ctx, endpoint, payload, signal) {
|
|
|
1187
1220
|
case "log":
|
|
1188
1221
|
return gitLog(args.cwd, args.count, signal);
|
|
1189
1222
|
case "generateMessage":
|
|
1190
|
-
return gitGenerateMessage(ctx, args.cwd, args.mode, args.provider, args.model, signal);
|
|
1223
|
+
return gitGenerateMessage(ctx, args.cwd, args.mode, args.provider, args.model, args.sessionId, signal);
|
|
1191
1224
|
default:
|
|
1192
1225
|
return fail("unknown-endpoint", `unknown git endpoint ${JSON.stringify(endpoint)}`);
|
|
1193
1226
|
}
|
|
@@ -1203,8 +1236,10 @@ function apply(ctx) {
|
|
|
1203
1236
|
// The channel runs git against caller-supplied absolute paths, so it must
|
|
1204
1237
|
// never mount unfenced: the connection service's Host/Origin + browser-cookie
|
|
1205
1238
|
// check is the only gate. `requestRejection` is the >= 0.1.5-rc.1 form of that
|
|
1206
|
-
// check
|
|
1207
|
-
//
|
|
1239
|
+
// check. An older host without it still gets no channel (this throws), but
|
|
1240
|
+
// note the throw is *stderr-only* for the operator: dsh-app-boot treats
|
|
1241
|
+
// non-required rows as optional, so this row is skipped with a warning rather
|
|
1242
|
+
// than failing the boot. Never downgrade the guard to a pass-through.
|
|
1208
1243
|
const connection = ctx.get("connection");
|
|
1209
1244
|
if (connection === undefined || typeof connection.requestRejection !== "function") {
|
|
1210
1245
|
throw new Error(`${name}: this plugin requires dsh >= 0.1.5-rc.1 (connection.requestRejection is unavailable, so /dsh-git-rpc could not be fenced)`);
|
|
@@ -1213,7 +1248,15 @@ function apply(ctx) {
|
|
|
1213
1248
|
kind: "prefix",
|
|
1214
1249
|
path: RPC_CHANNEL,
|
|
1215
1250
|
handler: async (req, res) => {
|
|
1216
|
-
|
|
1251
|
+
// The host's own router matches on `new URL(req.url, "http://x").pathname`,
|
|
1252
|
+
// so an absolute-form request target (a proxy in front of dsh web) must
|
|
1253
|
+
// resolve here too. Origin-form targets are kept verbatim: `endpointFromPath`
|
|
1254
|
+
// is the stricter reader (it rejects `.`/`..`/empty segments and anything
|
|
1255
|
+
// outside the client's own segment pattern), and routing the raw target
|
|
1256
|
+
// through the URL parser first would silently normalize a traversal
|
|
1257
|
+
// attempt into a different endpoint instead of refusing it.
|
|
1258
|
+
const rawTarget = String(req.url ?? "/");
|
|
1259
|
+
const pathname = rawTarget.startsWith("/") ? rawTarget : new URL(rawTarget, "http://x").pathname;
|
|
1217
1260
|
const endpoint = endpointFromPath(pathname);
|
|
1218
1261
|
if (endpoint === undefined) {
|
|
1219
1262
|
sendEnvelope(res, 404, rpcError("invalid-request", "not-found", "not found", {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xia-sc/dsh-git",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Complete Git management for the DeepSeek Harness Web GUI: branch switch, new-branch-from-base, fetch, pull, stage-all, commit (with AI-drafted message), push, status, and a click-to-open diff viewer (staged/unstaged, resizable) — as a collapsible floating panel that follows the current session's workspace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "node test/smoke.mjs && node test/host-mount.mjs && node test/generate.mjs && node test/render.mjs",
|
|
42
|
+
"test": "node test/smoke.mjs && node test/host-mount.mjs && node test/slot-mount.mjs && node test/generate.mjs && node test/render.mjs",
|
|
43
43
|
"test:commit": "node test/commit.mjs",
|
|
44
44
|
"test:diff": "node test/diff.mjs",
|
|
45
45
|
"prepublishOnly": "npm test"
|
|
@@ -51,9 +51,12 @@
|
|
|
51
51
|
"client": {
|
|
52
52
|
"platform": "web",
|
|
53
53
|
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-connection",
|
|
54
55
|
"@deepseek-ai/dsh-client-locale",
|
|
55
56
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-layout"
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-session"
|
|
57
60
|
]
|
|
58
61
|
}
|
|
59
62
|
},
|