@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/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
- * Uses the classic data/end/error events rather than async iteration: the
686
- * IncomingMessage async iterator is not a stable path in this runtime.
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. The shape must stay exactly this: a fresh id, the
1042
- * `user` role, one text block, and a plugin source.
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 Object.freeze({
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
- return fail(error.pluginCode ?? "llm-failed", error instanceof Error ? error.message : String(error), { mode });
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; an older host without it fails loudly instead of serving an open
1207
- // channel.
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
- const pathname = String(req.url ?? "/").split("?")[0];
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.5.3",
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
  },