dsh-local-ai 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +211 -0
  4. package/README.hi.md +211 -0
  5. package/README.md +211 -0
  6. package/README.pt.md +211 -0
  7. package/README.zh.md +211 -0
  8. package/THIRD_PARTY_NOTICES.md +20 -0
  9. package/cordis.patch.yml +44 -0
  10. package/lib/index.js +1334 -0
  11. package/lib/types/adapter.d.ts +39 -0
  12. package/lib/types/adapter.d.ts.map +1 -0
  13. package/lib/types/adapter.js +190 -0
  14. package/lib/types/adapter.js.map +1 -0
  15. package/lib/types/config.d.ts +109 -0
  16. package/lib/types/config.d.ts.map +1 -0
  17. package/lib/types/config.js +161 -0
  18. package/lib/types/config.js.map +1 -0
  19. package/lib/types/health.d.ts +64 -0
  20. package/lib/types/health.d.ts.map +1 -0
  21. package/lib/types/health.js +92 -0
  22. package/lib/types/health.js.map +1 -0
  23. package/lib/types/index.d.ts +43 -0
  24. package/lib/types/index.d.ts.map +1 -0
  25. package/lib/types/index.js +232 -0
  26. package/lib/types/index.js.map +1 -0
  27. package/lib/types/ollama.d.ts +91 -0
  28. package/lib/types/ollama.d.ts.map +1 -0
  29. package/lib/types/ollama.js +184 -0
  30. package/lib/types/ollama.js.map +1 -0
  31. package/lib/types/route.d.ts +52 -0
  32. package/lib/types/route.d.ts.map +1 -0
  33. package/lib/types/route.js +119 -0
  34. package/lib/types/route.js.map +1 -0
  35. package/lib/types/sanitize.d.ts +58 -0
  36. package/lib/types/sanitize.d.ts.map +1 -0
  37. package/lib/types/sanitize.js +110 -0
  38. package/lib/types/sanitize.js.map +1 -0
  39. package/lib/types/serialize.d.ts +65 -0
  40. package/lib/types/serialize.d.ts.map +1 -0
  41. package/lib/types/serialize.js +149 -0
  42. package/lib/types/serialize.js.map +1 -0
  43. package/lib/types/translate.d.ts +57 -0
  44. package/lib/types/translate.d.ts.map +1 -0
  45. package/lib/types/translate.js +169 -0
  46. package/lib/types/translate.js.map +1 -0
  47. package/lib/types/version.d.ts +6 -0
  48. package/lib/types/version.d.ts.map +1 -0
  49. package/lib/types/version.js +6 -0
  50. package/lib/types/version.js.map +1 -0
  51. package/package.json +141 -0
  52. package/src/adapter.ts +158 -0
  53. package/src/config.ts +243 -0
  54. package/src/health.ts +133 -0
  55. package/src/index.ts +279 -0
  56. package/src/ollama.ts +274 -0
  57. package/src/route.ts +127 -0
  58. package/src/sanitize.ts +114 -0
  59. package/src/serialize.ts +178 -0
  60. package/src/translate.ts +205 -0
  61. package/src/version.ts +5 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Ollama HTTP API client (zero runtime dependencies — plain `fetch`). The
3
+ * harness `LlmAdapter` streams through `/api/chat`; discovery and management
4
+ * tools call `/api/tags`, `/api/show`, `/api/pull`, `/api/delete`, and
5
+ * `/api/version`. Every request carries the harness attribution headers and
6
+ * honors the caller's abort signal; non-2xx responses fail with a normalized
7
+ * `LlmError`. The fetch implementation is injectable for tests.
8
+ * @module dsh-local-ai/ollama
9
+ */
10
+ import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
11
+ import { sanitizeEndpoint } from "./sanitize.js";
12
+ /** Build an absolute API URL from a normalized base URL. */
13
+ export function endpointUrl(baseURL, path) {
14
+ return `${baseURL}${path}`;
15
+ }
16
+ /** Map an HTTP status to a stable LlmError code. */
17
+ export function httpErrorCode(status) {
18
+ if (status === 404)
19
+ return 'NOT_FOUND';
20
+ if (status === 400)
21
+ return 'INVALID_REQUEST';
22
+ if (status >= 500)
23
+ return 'SERVER';
24
+ return `HTTP_${status}`;
25
+ }
26
+ /** Throw a normalized LlmError from a non-2xx response, using the body's `error`. */
27
+ async function throwHttpError(response, context) {
28
+ let message = `Ollama API error (HTTP ${response.status}) from ${sanitizeEndpoint(context)}`;
29
+ try {
30
+ const body = await response.json();
31
+ if (typeof body.error === 'string' && body.error.length > 0)
32
+ message = body.error;
33
+ }
34
+ catch {
35
+ // Only swallow error-body parsing: the HTTP status still identifies the failure.
36
+ }
37
+ throw new LlmError(message, httpErrorCode(response.status), { status: response.status });
38
+ }
39
+ /** Send a GET request and parse the JSON response. */
40
+ export async function requestJson(baseURL, path, fetchImpl, signal) {
41
+ const response = await fetchImpl(endpointUrl(baseURL, path), {
42
+ method: 'GET',
43
+ headers: { accept: 'application/json', ...attributionHeaders() },
44
+ ...signal === undefined ? {} : { signal },
45
+ });
46
+ if (!response.ok)
47
+ await throwHttpError(response, endpointUrl(baseURL, path));
48
+ return response.json();
49
+ }
50
+ /** Send a POST request and parse the JSON response. */
51
+ export async function postJson(baseURL, path, body, fetchImpl, signal) {
52
+ const response = await fetchImpl(endpointUrl(baseURL, path), {
53
+ method: 'POST',
54
+ headers: { 'content-type': 'application/json', accept: 'application/json', ...attributionHeaders() },
55
+ body: JSON.stringify(body),
56
+ ...signal === undefined ? {} : { signal },
57
+ });
58
+ if (!response.ok)
59
+ await throwHttpError(response, endpointUrl(baseURL, path));
60
+ return response.json();
61
+ }
62
+ /** Send a DELETE request and parse the JSON response. */
63
+ export async function deleteJson(baseURL, path, body, fetchImpl, signal) {
64
+ const response = await fetchImpl(endpointUrl(baseURL, path), {
65
+ method: 'DELETE',
66
+ headers: { 'content-type': 'application/json', accept: 'application/json', ...attributionHeaders() },
67
+ body: JSON.stringify(body),
68
+ ...signal === undefined ? {} : { signal },
69
+ });
70
+ if (!response.ok)
71
+ await throwHttpError(response, endpointUrl(baseURL, path));
72
+ return response.json();
73
+ }
74
+ /**
75
+ * Send a POST and return the raw `Response` after validating 2xx. Used by the
76
+ * streaming adapter, which owns body decoding and the idle watchdog.
77
+ */
78
+ export async function postStream(baseURL, path, body, fetchImpl, signal) {
79
+ const response = await fetchImpl(endpointUrl(baseURL, path), {
80
+ method: 'POST',
81
+ headers: { 'content-type': 'application/json', accept: 'application/x-ndjson', ...attributionHeaders() },
82
+ body: JSON.stringify(body),
83
+ ...signal === undefined ? {} : { signal },
84
+ });
85
+ if (!response.ok)
86
+ await throwHttpError(response, endpointUrl(baseURL, path));
87
+ return response;
88
+ }
89
+ /** List installed models from `/api/tags`. */
90
+ export async function listModels(baseURL, fetchImpl, signal) {
91
+ const result = await requestJson(baseURL, '/api/tags', fetchImpl, signal);
92
+ return result.models ?? [];
93
+ }
94
+ /** List currently-loaded (running) models from `/api/ps`. */
95
+ export async function listRunning(baseURL, fetchImpl, signal) {
96
+ const result = await requestJson(baseURL, '/api/ps', fetchImpl, signal);
97
+ return result.models ?? [];
98
+ }
99
+ /** Inspect one model via `/api/show`. */
100
+ export async function showModel(baseURL, name, fetchImpl, signal) {
101
+ return postJson(baseURL, '/api/show', { name }, fetchImpl, signal);
102
+ }
103
+ /** Remove one model via `/api/delete`. */
104
+ export async function removeModel(baseURL, name, fetchImpl, signal) {
105
+ await deleteJson(baseURL, '/api/delete', { name }, fetchImpl, signal);
106
+ }
107
+ /** Query the Ollama server version via `/api/version`. */
108
+ export async function apiVersion(baseURL, fetchImpl, signal) {
109
+ const result = await requestJson(baseURL, '/api/version', fetchImpl, signal);
110
+ return result.version;
111
+ }
112
+ /**
113
+ * Pull a model via `/api/pull`, consuming the progress stream and returning the
114
+ * final status. An intermediate error status or a non-2xx response fails loud.
115
+ */
116
+ export async function pullModel(baseURL, name, fetchImpl, signal) {
117
+ const response = await postStream(baseURL, '/api/pull', { name, stream: true }, fetchImpl, signal);
118
+ if (!response.body)
119
+ throw new LlmError('Ollama pull returned no response body', 'EMPTY_RESPONSE');
120
+ let last = { status: 'success' };
121
+ for await (const line of readNdjsonLines(response.body)) {
122
+ if (line.length === 0)
123
+ continue;
124
+ const chunk = JSON.parse(line);
125
+ if (typeof chunk.error === 'string' && chunk.error.length > 0) {
126
+ throw new LlmError(chunk.error, 'PROVIDER');
127
+ }
128
+ if (typeof chunk.status === 'string')
129
+ last = { status: chunk.status };
130
+ }
131
+ return last;
132
+ }
133
+ /**
134
+ * Decode a `ReadableStream<Uint8Array>` into newline-delimited text lines.
135
+ * The final line is yielded even without a trailing newline; a missing body
136
+ * yields nothing.
137
+ * @param body - the response body stream.
138
+ * @returns text lines in delivery order.
139
+ */
140
+ export async function* readNdjsonLines(body) {
141
+ const reader = body.getReader();
142
+ const decoder = new TextDecoder();
143
+ let buffer = '';
144
+ try {
145
+ while (true) {
146
+ const { done, value } = await reader.read();
147
+ if (done)
148
+ break;
149
+ buffer += decoder.decode(value, { stream: true });
150
+ let newline = buffer.indexOf('\n');
151
+ while (newline >= 0) {
152
+ const line = buffer.slice(0, newline).replace(/\r$/u, '');
153
+ buffer = buffer.slice(newline + 1);
154
+ newline = buffer.indexOf('\n');
155
+ yield line;
156
+ }
157
+ }
158
+ }
159
+ finally {
160
+ reader.releaseLock();
161
+ }
162
+ buffer += decoder.decode();
163
+ if (buffer.length > 0)
164
+ yield buffer;
165
+ }
166
+ /**
167
+ * Extract the context length from an `/api/show` result by scanning
168
+ * `model_info` for a `*.context_length` or bare `context_length` entry.
169
+ * @param show - the `/api/show` result.
170
+ * @returns the context length, or `undefined` when not reported.
171
+ */
172
+ export function contextLengthOf(show) {
173
+ const info = show.model_info;
174
+ if (info === undefined)
175
+ return undefined;
176
+ for (const [key, value] of Object.entries(info)) {
177
+ if (key === 'context_length' || key.endsWith('.context_length')) {
178
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0)
179
+ return value;
180
+ }
181
+ }
182
+ return undefined;
183
+ }
184
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../src/ollama.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Local-model routing decision and the streaming fallback. The pure decision
3
+ * matches configured rules (task type via `purpose`, case-insensitive
4
+ * keywords, or a blanket `always`) against a request, in list order — first
5
+ * match wins. The streaming helper routes a matched request to the local
6
+ * Ollama adapter and, when the local route fails BEFORE producing any visible
7
+ * content, falls back to the cloud (`next()`) so a down Ollama never bricks a
8
+ * conversation. Once local content has started, it is streamed through — a
9
+ * mid-stream failure cannot be retracted.
10
+ * @module dsh-local-ai/route
11
+ */
12
+ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
13
+ import type { ResolvedConfig, ResolvedRouteRule } from './config.ts';
14
+ /** The chosen local model for one matched request. */
15
+ export interface RouteDecision {
16
+ /** Harness-visible local model name. */
17
+ readonly model: string;
18
+ }
19
+ /**
20
+ * Concatenate the request's model-visible text (system prompt + every text
21
+ * block) for keyword matching.
22
+ * @param options - the request.
23
+ * @returns the joined text.
24
+ */
25
+ export declare function requestText(options: GenerateOptions): string;
26
+ /** Whether a keyword appears case-insensitively in the text. */
27
+ export declare function matchesKeyword(text: string, keyword: string): boolean;
28
+ /** Whether one resolved rule matches a request. */
29
+ export declare function ruleMatches(rule: ResolvedRouteRule, options: GenerateOptions): boolean;
30
+ /**
31
+ * Decide whether a request should route to a local model. A request already
32
+ * addressed to the `ollama` provider (explicit selection or a prior re-route)
33
+ * never re-routes.
34
+ * @param options - the request.
35
+ * @param resolved - the resolved config.
36
+ * @returns the local model to use, or `undefined` to stay on the cloud route.
37
+ */
38
+ export declare function decideRoute(options: GenerateOptions, resolved: ResolvedConfig): RouteDecision | undefined;
39
+ /**
40
+ * Stream a locally-routed request with automatic cloud fallback. The local
41
+ * stream is produced through `streamLocal` (the full harness stream, so the
42
+ * local route keeps retry and failure normalization). If the local route
43
+ * finishes with an error or aborts before any token delta, `next()` (the
44
+ * cloud) is streamed instead; otherwise the local stream is forwarded.
45
+ * @param streamLocal - produces the local stream for a re-routed request.
46
+ * @param options - the original request.
47
+ * @param decision - the local model to route to.
48
+ * @param next - the cloud stream (the waterfall's `next()`).
49
+ * @returns the effective chunk stream.
50
+ */
51
+ export declare function routeLocal(streamLocal: (options: GenerateOptions) => AsyncIterable<StreamChunk>, options: GenerateOptions, decision: RouteDecision, next: () => AsyncIterable<StreamChunk>): AsyncGenerator<StreamChunk>;
52
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpE,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAS5D;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAQtF;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,GAAG,aAAa,GAAG,SAAS,CAMzG;AAED;;;;;;;;;;;GAWG;AACH,wBAAuB,UAAU,CAC/B,WAAW,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,aAAa,CAAC,WAAW,CAAC,EACrE,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,MAAM,aAAa,CAAC,WAAW,CAAC,GACrC,cAAc,CAAC,WAAW,CAAC,CAqC7B"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Local-model routing decision and the streaming fallback. The pure decision
3
+ * matches configured rules (task type via `purpose`, case-insensitive
4
+ * keywords, or a blanket `always`) against a request, in list order — first
5
+ * match wins. The streaming helper routes a matched request to the local
6
+ * Ollama adapter and, when the local route fails BEFORE producing any visible
7
+ * content, falls back to the cloud (`next()`) so a down Ollama never bricks a
8
+ * conversation. Once local content has started, it is streamed through — a
9
+ * mid-stream failure cannot be retracted.
10
+ * @module dsh-local-ai/route
11
+ */
12
+ import { isTokenDelta } from '@deepseek-ai/dsh-llm';
13
+ import { OLLAMA_PROVIDER } from "./adapter.js";
14
+ /**
15
+ * Concatenate the request's model-visible text (system prompt + every text
16
+ * block) for keyword matching.
17
+ * @param options - the request.
18
+ * @returns the joined text.
19
+ */
20
+ export function requestText(options) {
21
+ const parts = [];
22
+ if (options.system !== undefined)
23
+ parts.push(options.system);
24
+ for (const message of options.messages) {
25
+ for (const block of message.content) {
26
+ if (block.type === 'text')
27
+ parts.push(block.text);
28
+ }
29
+ }
30
+ return parts.join('\n');
31
+ }
32
+ /** Whether a keyword appears case-insensitively in the text. */
33
+ export function matchesKeyword(text, keyword) {
34
+ return text.toLowerCase().includes(keyword.toLowerCase());
35
+ }
36
+ /** Whether one resolved rule matches a request. */
37
+ export function ruleMatches(rule, options) {
38
+ if (rule.always)
39
+ return true;
40
+ if (rule.purpose !== undefined && options.purpose === rule.purpose)
41
+ return true;
42
+ if (rule.keywords.length > 0) {
43
+ const text = requestText(options);
44
+ return rule.keywords.some(keyword => matchesKeyword(text, keyword));
45
+ }
46
+ return false;
47
+ }
48
+ /**
49
+ * Decide whether a request should route to a local model. A request already
50
+ * addressed to the `ollama` provider (explicit selection or a prior re-route)
51
+ * never re-routes.
52
+ * @param options - the request.
53
+ * @param resolved - the resolved config.
54
+ * @returns the local model to use, or `undefined` to stay on the cloud route.
55
+ */
56
+ export function decideRoute(options, resolved) {
57
+ if (options.provider === OLLAMA_PROVIDER)
58
+ return undefined;
59
+ for (const rule of resolved.route) {
60
+ if (ruleMatches(rule, options))
61
+ return { model: rule.model };
62
+ }
63
+ return undefined;
64
+ }
65
+ /**
66
+ * Stream a locally-routed request with automatic cloud fallback. The local
67
+ * stream is produced through `streamLocal` (the full harness stream, so the
68
+ * local route keeps retry and failure normalization). If the local route
69
+ * finishes with an error or aborts before any token delta, `next()` (the
70
+ * cloud) is streamed instead; otherwise the local stream is forwarded.
71
+ * @param streamLocal - produces the local stream for a re-routed request.
72
+ * @param options - the original request.
73
+ * @param decision - the local model to route to.
74
+ * @param next - the cloud stream (the waterfall's `next()`).
75
+ * @returns the effective chunk stream.
76
+ */
77
+ export async function* routeLocal(streamLocal, options, decision, next) {
78
+ const localOptions = { ...options, provider: OLLAMA_PROVIDER, model: decision.model };
79
+ const upstream = streamLocal(localOptions);
80
+ let producedContent = false;
81
+ const pending = [];
82
+ try {
83
+ for await (const chunk of upstream) {
84
+ if (producedContent) {
85
+ yield chunk;
86
+ continue;
87
+ }
88
+ if (chunk.type === 'finish') {
89
+ if (chunk.reason.kind === 'error' || chunk.reason.kind === 'aborted') {
90
+ // Local failed before producing content — fall back to the cloud.
91
+ yield* next();
92
+ return;
93
+ }
94
+ yield chunk;
95
+ return;
96
+ }
97
+ pending.push(chunk);
98
+ if (isTokenDelta(chunk)) {
99
+ producedContent = true;
100
+ for (const buffered of pending)
101
+ yield buffered;
102
+ pending.length = 0;
103
+ }
104
+ }
105
+ // Stream ended without a finish chunk — flush whatever was buffered.
106
+ for (const buffered of pending)
107
+ yield buffered;
108
+ }
109
+ catch (error) {
110
+ if (!producedContent) {
111
+ yield* next();
112
+ return;
113
+ }
114
+ for (const buffered of pending)
115
+ yield buffered;
116
+ throw error;
117
+ }
118
+ }
119
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/route.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Display/log sanitization for `dsh-local-ai`. Every value shown to the model
3
+ * or to the user (tool results, the `/ollama` command, error messages) passes
4
+ * through one of these pure functions first, so an endpoint address or a local
5
+ * path can never leak credentials, secret query parameters, or unbounded text.
6
+ *
7
+ * All functions are pure: they depend only on their arguments, never on
8
+ * process state (the caller supplies a home directory for path redaction).
9
+ * @module dsh-local-ai/sanitize
10
+ */
11
+ /** Placeholder substituted for a redacted secret or credential. */
12
+ export declare const REDACTED = "[REDACTED]";
13
+ /** Remove C0/DEL control characters from a string. */
14
+ export declare function stripControl(value: string): string;
15
+ /**
16
+ * Truncate a string to `maxChars`, appending an ellipsis when it was cut.
17
+ * A non-positive `maxChars` yields the empty string.
18
+ * @param value - the string to bound.
19
+ * @param maxChars - maximum returned length, including the ellipsis.
20
+ * @returns the bounded string.
21
+ */
22
+ export declare function truncate(value: string, maxChars: number): string;
23
+ /**
24
+ * Sanitize an endpoint address for display: strip the URL userinfo
25
+ * (`user:pass@`), drop query parameters whose key looks like a secret, strip
26
+ * control characters, and bound the length. Values that are not parseable as
27
+ * a URL are still stripped and truncated.
28
+ * @param value - the raw endpoint (URL string or anything stringifiable).
29
+ * @param maxChars - maximum returned length.
30
+ * @returns the sanitized endpoint text.
31
+ */
32
+ export declare function sanitizeEndpoint(value: unknown, maxChars?: number): string;
33
+ /**
34
+ * Sanitize a local path for display: strip control characters, redact a
35
+ * leading home directory to `~`, and bound the length.
36
+ * @param value - the raw path (string or anything stringifiable).
37
+ * @param home - the user's home directory to redact; omit to skip redaction.
38
+ * @param maxChars - maximum returned length.
39
+ * @returns the sanitized path text.
40
+ */
41
+ export declare function sanitizePath(value: unknown, home?: string, maxChars?: number): string;
42
+ /**
43
+ * Redact built-in secret literals (API keys, GitHub tokens, AWS keys, bearer
44
+ * credentials, PEM private keys) from arbitrary text. Control characters are
45
+ * stripped first.
46
+ * @param value - the raw text (string or anything stringifiable).
47
+ * @returns the text with secret literals replaced by {@link REDACTED}.
48
+ */
49
+ export declare function redactSecrets(value: unknown): string;
50
+ /**
51
+ * Sanitize arbitrary display text: redact secrets, strip control characters,
52
+ * and bound the length.
53
+ * @param value - the raw text (string or anything stringifiable).
54
+ * @param maxChars - maximum returned length.
55
+ * @returns the sanitized text.
56
+ */
57
+ export declare function sanitizeText(value: unknown, maxChars?: number): string;
58
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,mEAAmE;AACnE,eAAO,MAAM,QAAQ,eAAe,CAAA;AAkBpC,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAO,GAAG,MAAM,CAexE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,SAAK,EAAE,QAAQ,SAAO,GAAG,MAAM,CAM/E;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKpD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAO,GAAG,MAAM,CAEpE"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Display/log sanitization for `dsh-local-ai`. Every value shown to the model
3
+ * or to the user (tool results, the `/ollama` command, error messages) passes
4
+ * through one of these pure functions first, so an endpoint address or a local
5
+ * path can never leak credentials, secret query parameters, or unbounded text.
6
+ *
7
+ * All functions are pure: they depend only on their arguments, never on
8
+ * process state (the caller supplies a home directory for path redaction).
9
+ * @module dsh-local-ai/sanitize
10
+ */
11
+ /** Placeholder substituted for a redacted secret or credential. */
12
+ export const REDACTED = '[REDACTED]';
13
+ /** Control characters (C0 + DEL) stripped from every sanitized value. */
14
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/gu;
15
+ /** Secret-shaped query-parameter keys removed from endpoint URLs. */
16
+ const SECRET_KEY_PATTERN = /key|token|secret|password|credential|auth/iu;
17
+ /** Built-in secret literal patterns redacted from arbitrary text. */
18
+ const BUILTIN_SECRET_PATTERNS = [
19
+ /\bsk-[A-Za-z0-9]{16,}\b/u,
20
+ /\bghp_[A-Za-z0-9]{20,}\b/u,
21
+ /\bgho_[A-Za-z0-9]{20,}\b/u,
22
+ /\bAKIA[0-9A-Z]{16}\b/u,
23
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/u,
24
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[A-Za-z0-9+/=\s]*-----END [A-Z ]*PRIVATE KEY-----/u,
25
+ ];
26
+ /** Remove C0/DEL control characters from a string. */
27
+ export function stripControl(value) {
28
+ return value.replace(CONTROL_CHARS, '');
29
+ }
30
+ /**
31
+ * Truncate a string to `maxChars`, appending an ellipsis when it was cut.
32
+ * A non-positive `maxChars` yields the empty string.
33
+ * @param value - the string to bound.
34
+ * @param maxChars - maximum returned length, including the ellipsis.
35
+ * @returns the bounded string.
36
+ */
37
+ export function truncate(value, maxChars) {
38
+ if (value.length <= maxChars)
39
+ return value;
40
+ if (maxChars <= 1)
41
+ return maxChars <= 0 ? '' : '…';
42
+ return `${value.slice(0, maxChars - 1)}…`;
43
+ }
44
+ /**
45
+ * Sanitize an endpoint address for display: strip the URL userinfo
46
+ * (`user:pass@`), drop query parameters whose key looks like a secret, strip
47
+ * control characters, and bound the length. Values that are not parseable as
48
+ * a URL are still stripped and truncated.
49
+ * @param value - the raw endpoint (URL string or anything stringifiable).
50
+ * @param maxChars - maximum returned length.
51
+ * @returns the sanitized endpoint text.
52
+ */
53
+ export function sanitizeEndpoint(value, maxChars = 2048) {
54
+ const text = stripControl(typeof value === 'string' ? value : String(value));
55
+ let out;
56
+ try {
57
+ const url = new URL(text);
58
+ url.username = '';
59
+ url.password = '';
60
+ for (const key of [...url.searchParams.keys()]) {
61
+ if (SECRET_KEY_PATTERN.test(key))
62
+ url.searchParams.delete(key);
63
+ }
64
+ out = url.href;
65
+ }
66
+ catch {
67
+ out = text;
68
+ }
69
+ return truncate(out, maxChars);
70
+ }
71
+ /**
72
+ * Sanitize a local path for display: strip control characters, redact a
73
+ * leading home directory to `~`, and bound the length.
74
+ * @param value - the raw path (string or anything stringifiable).
75
+ * @param home - the user's home directory to redact; omit to skip redaction.
76
+ * @param maxChars - maximum returned length.
77
+ * @returns the sanitized path text.
78
+ */
79
+ export function sanitizePath(value, home = '', maxChars = 1024) {
80
+ const text = stripControl(typeof value === 'string' ? value : String(value));
81
+ const redacted = home.length > 0 && text.startsWith(home)
82
+ ? `~${text.slice(home.length)}`
83
+ : text;
84
+ return truncate(redacted, maxChars);
85
+ }
86
+ /**
87
+ * Redact built-in secret literals (API keys, GitHub tokens, AWS keys, bearer
88
+ * credentials, PEM private keys) from arbitrary text. Control characters are
89
+ * stripped first.
90
+ * @param value - the raw text (string or anything stringifiable).
91
+ * @returns the text with secret literals replaced by {@link REDACTED}.
92
+ */
93
+ export function redactSecrets(value) {
94
+ const text = stripControl(typeof value === 'string' ? value : String(value));
95
+ let out = text;
96
+ for (const pattern of BUILTIN_SECRET_PATTERNS)
97
+ out = out.replace(pattern, REDACTED);
98
+ return out;
99
+ }
100
+ /**
101
+ * Sanitize arbitrary display text: redact secrets, strip control characters,
102
+ * and bound the length.
103
+ * @param value - the raw text (string or anything stringifiable).
104
+ * @param maxChars - maximum returned length.
105
+ * @returns the sanitized text.
106
+ */
107
+ export function sanitizeText(value, maxChars = 4000) {
108
+ return truncate(redactSecrets(value), maxChars);
109
+ }
110
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/sanitize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Serialize harness messages and requests into the Ollama `/api/chat` wire
3
+ * vocabulary. User text is joined; assistant text becomes `content` and tool
4
+ * calls become `tool_calls` (with arguments parsed from the raw JSON string to
5
+ * the object Ollama expects); tool results become separate `tool` messages.
6
+ * Core image blocks are rejected explicitly because this route is text-only;
7
+ * unknown declaration-merged block types retain the documented extension
8
+ * fallback (ignored for content, retained as text where text is expected).
9
+ * @module dsh-local-ai/serialize
10
+ */
11
+ import type { GenerateOptions, Message, ToolSchema } from '@deepseek-ai/dsh-llm';
12
+ import type { ResolvedConfig } from './config.ts';
13
+ /** One Ollama chat message on the wire. */
14
+ export interface OllamaWireMessage {
15
+ role: 'system' | 'user' | 'assistant' | 'tool';
16
+ content: string;
17
+ tool_calls?: Array<{
18
+ function: {
19
+ name: string;
20
+ arguments: Record<string, unknown>;
21
+ };
22
+ }>;
23
+ tool_name?: string;
24
+ }
25
+ /** The Ollama `/api/chat` request body. */
26
+ export interface OllamaWireRequest {
27
+ model: string;
28
+ messages: OllamaWireMessage[];
29
+ stream: boolean;
30
+ options?: Record<string, unknown>;
31
+ tools?: Array<{
32
+ type: 'function';
33
+ function: ToolSchema;
34
+ }>;
35
+ }
36
+ /**
37
+ * Parse a tool-call argument string into the object Ollama expects. The raw
38
+ * string is guaranteed by the harness contract to be JSON; a malformed value
39
+ * from a hand-built call degrades to a single `value` field rather than
40
+ * bricking the whole session.
41
+ * @param raw - the raw JSON string produced by the model.
42
+ * @returns the parsed object, or a `{ value }` fallback.
43
+ */
44
+ export declare function parseToolArguments(raw: string): Record<string, unknown>;
45
+ /**
46
+ * Serialize the conversation. `tool-result` blocks become standalone
47
+ * `{role: 'tool'}` messages; the harness puts each tool result in its own
48
+ * user-role message, so a mixed user message contributes its text first and
49
+ * its tool results as separate wire messages after. Assistant tool calls are
50
+ * indexed first so their results can carry the tool name Ollama needs.
51
+ * @param messages - the harness conversation, in order.
52
+ * @returns the wire messages; order preserved, each tool result expanded into its own entry.
53
+ */
54
+ export declare function serializeMessages(messages: Message[]): OllamaWireMessage[];
55
+ /**
56
+ * Build the full wire request. Always streaming (`stream: true`); optional
57
+ * fields are omitted rather than sent as null, so Ollama defaults apply.
58
+ * `temperature` resolves request → model mapping → plugin default; `num_predict`
59
+ * is the harness-materialized `maxTokens`.
60
+ * @param options - the harness request (model, history, system, tools, sampling).
61
+ * @param resolved - the resolved plugin config.
62
+ * @returns the `/api/chat` request body.
63
+ */
64
+ export declare function serializeRequest(options: GenerateOptions, resolved: ResolvedConfig): OllamaWireRequest;
65
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAgB,eAAe,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAC9F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAA;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAA;KAAE,CAAC,CAAA;IACtF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,iBAAiB,EAAE,CAAA;IAC7B,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,UAAU,CAAA;KAAE,CAAC,CAAA;CAC1D;AAiBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAuBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,iBAAiB,EAAE,CAsC1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,GACvB,iBAAiB,CA4BnB"}