@zleap-ai/dsh-sag 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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/README.zh.md +58 -0
- package/THIRD_PARTY_NOTICES +671 -0
- package/cordis.patch.yml +5 -0
- package/docs/embedded.md +61 -0
- package/lib/brand.d.ts +13 -0
- package/lib/brand.js +15 -0
- package/lib/cli/runtime.d.ts +40 -0
- package/lib/cli/runtime.js +121 -0
- package/lib/cli.d.ts +21 -0
- package/lib/cli.js +265 -0
- package/lib/config.d.ts +60 -0
- package/lib/config.js +120 -0
- package/lib/connection/descriptor.d.ts +4 -0
- package/lib/connection/descriptor.js +66 -0
- package/lib/connection/discovery.d.ts +40 -0
- package/lib/connection/discovery.js +139 -0
- package/lib/connection/guidance.d.ts +7 -0
- package/lib/connection/guidance.js +7 -0
- package/lib/connection/manager.d.ts +67 -0
- package/lib/connection/manager.js +273 -0
- package/lib/connection/store.d.ts +42 -0
- package/lib/connection/store.js +164 -0
- package/lib/connection/types.d.ts +35 -0
- package/lib/connection/types.js +2 -0
- package/lib/dsh-sag-cli.js +32202 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +68 -0
- package/lib/local/api-client.d.ts +157 -0
- package/lib/local/api-client.js +338 -0
- package/lib/local/gateway.d.ts +43 -0
- package/lib/local/gateway.js +34 -0
- package/lib/local/mcp-probe.d.ts +27 -0
- package/lib/local/mcp-probe.js +92 -0
- package/lib/presentation.d.ts +8 -0
- package/lib/presentation.js +7 -0
- package/lib/runtime/client.d.ts +22 -0
- package/lib/runtime/client.js +117 -0
- package/lib/runtime/protocol.d.ts +113 -0
- package/lib/runtime/protocol.js +118 -0
- package/lib/runtime/supervisor.d.ts +30 -0
- package/lib/runtime/supervisor.js +107 -0
- package/lib/tools/documents.d.ts +8 -0
- package/lib/tools/documents.js +134 -0
- package/lib/tools/ingest.d.ts +5 -0
- package/lib/tools/ingest.js +35 -0
- package/lib/tools/local.d.ts +40 -0
- package/lib/tools/local.js +111 -0
- package/lib/tools/output.d.ts +96 -0
- package/lib/tools/output.js +67 -0
- package/lib/tools/read.d.ts +6 -0
- package/lib/tools/read.js +84 -0
- package/lib/tools/search.d.ts +6 -0
- package/lib/tools/search.js +107 -0
- package/lib/tools/sources.d.ts +5 -0
- package/lib/tools/sources.js +61 -0
- package/lib/tools/status.d.ts +5 -0
- package/lib/tools/status.js +28 -0
- package/lib/tools/upload.d.ts +6 -0
- package/lib/tools/upload.js +68 -0
- package/package.json +96 -0
- package/runtime/pyproject.toml +29 -0
- package/runtime/src/dsh_sag_runtime/__init__.py +3 -0
- package/runtime/src/dsh_sag_runtime/__main__.py +80 -0
- package/runtime/src/dsh_sag_runtime/engines.py +139 -0
- package/runtime/src/dsh_sag_runtime/errors.py +47 -0
- package/runtime/src/dsh_sag_runtime/evidence.py +53 -0
- package/runtime/src/dsh_sag_runtime/protocol.py +161 -0
- package/runtime/src/dsh_sag_runtime/read.py +61 -0
- package/runtime/src/dsh_sag_runtime/search.py +76 -0
- package/runtime/src/dsh_sag_runtime/server.py +136 -0
- package/runtime/uv.lock +2310 -0
- package/scripts/setup-runtime.mjs +47 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { sagNamespaceId } from './brand.js';
|
|
3
|
+
const DEFAULT_DISCOVERY_URLS = ['http://127.0.0.1:8000', 'http://localhost:8000'];
|
|
4
|
+
const DEFAULT_MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
5
|
+
const localConfigSchema = z.object({
|
|
6
|
+
mode: z.const('local').default('local'),
|
|
7
|
+
discoveryUrls: z.array(z.string()).default(null),
|
|
8
|
+
requestTimeoutMs: z.number().default(30_000),
|
|
9
|
+
maxUploadBytes: z.number().default(DEFAULT_MAX_UPLOAD_BYTES),
|
|
10
|
+
maxReadChars: z.number().default(40_000),
|
|
11
|
+
connectionCacheTtlMs: z.number().default(5_000),
|
|
12
|
+
});
|
|
13
|
+
const embeddedConfigSchema = z.object({
|
|
14
|
+
mode: z.const('embedded').required(),
|
|
15
|
+
pythonCommand: z.string().required(),
|
|
16
|
+
envFile: z.string().required(),
|
|
17
|
+
cwd: z.string(),
|
|
18
|
+
namespaces: z.array(z.object({ id: z.string().required(), label: z.string().required() })).required(),
|
|
19
|
+
defaultMode: z.union(['fast', 'precise']).default('fast'),
|
|
20
|
+
maxResults: z.number().default(20),
|
|
21
|
+
maxSnippetChars: z.number().default(1200),
|
|
22
|
+
maxReadChars: z.number().default(40_000),
|
|
23
|
+
maxReadEngines: z.number().default(4),
|
|
24
|
+
requestTimeoutMs: z.number().default(30_000),
|
|
25
|
+
shutdownGraceMs: z.number().default(5_000),
|
|
26
|
+
allowDegraded: z.boolean().default(false),
|
|
27
|
+
});
|
|
28
|
+
export const Config = z.union([localConfigSchema, embeddedConfigSchema]);
|
|
29
|
+
function integer(name, value, maximum) {
|
|
30
|
+
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
|
31
|
+
throw new Error(`dsh-sag: ${name} must be an integer from 1 to ${maximum}`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function nonEmpty(name, value) {
|
|
36
|
+
if (typeof value !== 'string')
|
|
37
|
+
throw new Error(`dsh-sag: ${name} must not be empty`);
|
|
38
|
+
const normalized = value.trim();
|
|
39
|
+
if (!normalized)
|
|
40
|
+
throw new Error(`dsh-sag: ${name} must not be empty`);
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
function normalizeHttpUrl(name, value) {
|
|
44
|
+
const normalized = nonEmpty(name, value);
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = new URL(normalized);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error(`dsh-sag: ${name} must be an http or https URL`);
|
|
51
|
+
}
|
|
52
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
53
|
+
throw new Error(`dsh-sag: ${name} must be an http or https URL`);
|
|
54
|
+
}
|
|
55
|
+
if (parsed.username || parsed.password) {
|
|
56
|
+
throw new Error(`dsh-sag: ${name} must not contain a username or password`);
|
|
57
|
+
}
|
|
58
|
+
return parsed.toString().replace(/\/$/, '');
|
|
59
|
+
}
|
|
60
|
+
function resolveLocalConfig(config) {
|
|
61
|
+
const discoveryUrls = config.discoveryUrls ?? DEFAULT_DISCOVERY_URLS;
|
|
62
|
+
if (!Array.isArray(discoveryUrls) || discoveryUrls.length === 0) {
|
|
63
|
+
throw new Error('dsh-sag: discoveryUrls must contain at least one URL');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
mode: 'local',
|
|
67
|
+
discoveryUrls: discoveryUrls.map((url, index) => normalizeHttpUrl(`discoveryUrls[${index}]`, url)),
|
|
68
|
+
requestTimeoutMs: integer('requestTimeoutMs', config.requestTimeoutMs ?? 30_000, 2_147_483_647),
|
|
69
|
+
maxUploadBytes: integer('maxUploadBytes', config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES, 2_147_483_647),
|
|
70
|
+
maxReadChars: integer('maxReadChars', config.maxReadChars ?? 40_000, 200_000),
|
|
71
|
+
connectionCacheTtlMs: integer('connectionCacheTtlMs', config.connectionCacheTtlMs ?? 5_000, 2_147_483_647),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function resolveEmbeddedConfig(config) {
|
|
75
|
+
const pythonCommand = nonEmpty('pythonCommand', config.pythonCommand);
|
|
76
|
+
const envFile = nonEmpty('envFile', config.envFile);
|
|
77
|
+
if (!Array.isArray(config.namespaces) || config.namespaces.length === 0) {
|
|
78
|
+
throw new Error('dsh-sag: at least one namespace is required');
|
|
79
|
+
}
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const namespaces = config.namespaces.map((namespace) => {
|
|
82
|
+
let id;
|
|
83
|
+
try {
|
|
84
|
+
id = sagNamespaceId(namespace.id.trim());
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new Error('dsh-sag: namespace id must be 1-36 URL-safe characters');
|
|
88
|
+
}
|
|
89
|
+
if (seen.has(id))
|
|
90
|
+
throw new Error(`dsh-sag: duplicate namespace ${JSON.stringify(id)}`);
|
|
91
|
+
seen.add(id);
|
|
92
|
+
return { id, label: nonEmpty('namespace label', namespace.label) };
|
|
93
|
+
});
|
|
94
|
+
const defaultMode = config.defaultMode ?? 'fast';
|
|
95
|
+
if (defaultMode !== 'fast' && defaultMode !== 'precise') {
|
|
96
|
+
throw new Error('dsh-sag: defaultMode must be fast or precise');
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
mode: 'embedded',
|
|
100
|
+
pythonCommand,
|
|
101
|
+
envFile,
|
|
102
|
+
cwd: config.cwd === undefined ? process.cwd() : nonEmpty('cwd', config.cwd),
|
|
103
|
+
namespaces,
|
|
104
|
+
defaultMode,
|
|
105
|
+
maxResults: integer('maxResults', config.maxResults ?? 20, 50),
|
|
106
|
+
maxSnippetChars: integer('maxSnippetChars', config.maxSnippetChars ?? 1200, 20_000),
|
|
107
|
+
maxReadChars: integer('maxReadChars', config.maxReadChars ?? 40_000, 200_000),
|
|
108
|
+
maxReadEngines: integer('maxReadEngines', config.maxReadEngines ?? 4, 32),
|
|
109
|
+
requestTimeoutMs: integer('requestTimeoutMs', config.requestTimeoutMs ?? 30_000, 2_147_483_647),
|
|
110
|
+
shutdownGraceMs: integer('shutdownGraceMs', config.shutdownGraceMs ?? 5_000, 2_147_483_647),
|
|
111
|
+
allowDegraded: config.allowDegraded ?? false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Apply and validate every default before a local client or sidecar observes config. */
|
|
115
|
+
export function resolveConfig(config) {
|
|
116
|
+
if (config.mode === 'embedded')
|
|
117
|
+
return resolveEmbeddedConfig(config);
|
|
118
|
+
return resolveLocalConfig(config);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SagConnectionDescriptor } from './types.js';
|
|
2
|
+
/** Parse a complete, versioned connection descriptor from a JSON or file boundary. */
|
|
3
|
+
export declare function parseConnectionDescriptor(value: unknown): SagConnectionDescriptor;
|
|
4
|
+
//# sourceMappingURL=descriptor.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const descriptorFields = new Set([
|
|
2
|
+
'schemaVersion',
|
|
3
|
+
'name',
|
|
4
|
+
'apiUrl',
|
|
5
|
+
'mcpUrl',
|
|
6
|
+
'accessToken',
|
|
7
|
+
'defaultSourceId',
|
|
8
|
+
]);
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
if (typeof value !== 'object' || value === null)
|
|
11
|
+
return false;
|
|
12
|
+
const prototype = Object.getPrototypeOf(value);
|
|
13
|
+
return prototype === Object.prototype || prototype === null;
|
|
14
|
+
}
|
|
15
|
+
function requiredString(object, field) {
|
|
16
|
+
const value = object[field];
|
|
17
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
18
|
+
throw new Error(`dsh-sag: connection descriptor ${field} must be a non-empty string`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function httpUrl(object, field) {
|
|
23
|
+
const value = requiredString(object, field);
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(value);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`dsh-sag: connection descriptor ${field} must be an http or https URL`);
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
32
|
+
throw new Error(`dsh-sag: connection descriptor ${field} must be an http or https URL`);
|
|
33
|
+
}
|
|
34
|
+
if (parsed.username || parsed.password) {
|
|
35
|
+
throw new Error(`dsh-sag: connection descriptor ${field} must not contain a username or password`);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
/** Parse a complete, versioned connection descriptor from a JSON or file boundary. */
|
|
40
|
+
export function parseConnectionDescriptor(value) {
|
|
41
|
+
if (!isPlainObject(value))
|
|
42
|
+
throw new Error('dsh-sag: connection descriptor must be a plain object');
|
|
43
|
+
for (const field of Object.keys(value)) {
|
|
44
|
+
if (!descriptorFields.has(field)) {
|
|
45
|
+
throw new Error(`dsh-sag: connection descriptor has unknown field ${field}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (value.schemaVersion !== 1) {
|
|
49
|
+
throw new Error('dsh-sag: connection descriptor requires schemaVersion 1');
|
|
50
|
+
}
|
|
51
|
+
const defaultSourceId = value.defaultSourceId;
|
|
52
|
+
if (defaultSourceId !== undefined && defaultSourceId !== null && (typeof defaultSourceId !== 'string' || !defaultSourceId.trim())) {
|
|
53
|
+
throw new Error('dsh-sag: connection descriptor defaultSourceId must be a non-empty string or null');
|
|
54
|
+
}
|
|
55
|
+
const apiUrl = httpUrl(value, 'apiUrl');
|
|
56
|
+
const mcpUrl = httpUrl(value, 'mcpUrl');
|
|
57
|
+
return {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
name: requiredString(value, 'name'),
|
|
60
|
+
apiUrl: apiUrl.toString().replace(/\/$/, ''),
|
|
61
|
+
mcpUrl: mcpUrl.toString(),
|
|
62
|
+
accessToken: requiredString(value, 'accessToken'),
|
|
63
|
+
...(defaultSourceId === undefined ? {} : { defaultSourceId }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=descriptor.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
2
|
+
import type { SagConnectionDescriptor } from './types.js';
|
|
3
|
+
/** An HTTP response sufficient to parse a SAG connection descriptor. */
|
|
4
|
+
export interface DiscoveryHttpResponse {
|
|
5
|
+
readonly ok: boolean;
|
|
6
|
+
readonly status: number;
|
|
7
|
+
json(): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
/** The injected HTTP boundary used for loopback discovery. */
|
|
10
|
+
export type DiscoveryFetch = (url: string, init: {
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
}) => Promise<DiscoveryHttpResponse>;
|
|
13
|
+
/** One failed discovery candidate with a remediation a caller can present. */
|
|
14
|
+
export interface DiscoveryDiagnostic {
|
|
15
|
+
readonly source: 'file' | 'loopback';
|
|
16
|
+
readonly candidate: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly action: string;
|
|
19
|
+
}
|
|
20
|
+
/** A connection found without changing the saved dsh-sag settings. */
|
|
21
|
+
export interface DiscoveryResult {
|
|
22
|
+
readonly source: 'file' | 'loopback' | undefined;
|
|
23
|
+
readonly descriptor: SagConnectionDescriptor | undefined;
|
|
24
|
+
readonly diagnostics: readonly DiscoveryDiagnostic[];
|
|
25
|
+
}
|
|
26
|
+
/** The filesystem and network candidates to inspect in their supplied order. */
|
|
27
|
+
export interface DiscoverConnectionOptions {
|
|
28
|
+
readonly fs: Pick<FileSystem, 'resolve' | 'readText'>;
|
|
29
|
+
readonly paths: readonly string[];
|
|
30
|
+
readonly urls: readonly string[];
|
|
31
|
+
readonly fetch: DiscoveryFetch;
|
|
32
|
+
}
|
|
33
|
+
/** Return the one explicit export path or the platform-standard SAG connection path. */
|
|
34
|
+
export declare function platformConnectionPaths(env: Readonly<NodeJS.ProcessEnv>, platform: NodeJS.Platform, home: string): readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Inspect connection files before configured loopback URLs, returning the first strict v1 descriptor.
|
|
37
|
+
* Discovery never saves a descriptor; its caller decides whether a valid result should be persisted.
|
|
38
|
+
*/
|
|
39
|
+
export declare function discoverConnection(options: DiscoverConnectionOptions, signal: AbortSignal): Promise<DiscoveryResult>;
|
|
40
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { posix, win32 } from 'node:path';
|
|
2
|
+
import { parseConnectionDescriptor } from './descriptor.js';
|
|
3
|
+
import { SAG_SETUP_COMMAND } from './guidance.js';
|
|
4
|
+
const LOOPBACK_CONNECTION_PATH = '/api/v1/system/dsh-connection';
|
|
5
|
+
const LOOPBACK_TIMEOUT_MS = 1_500;
|
|
6
|
+
function joinForPlatform(platform, ...parts) {
|
|
7
|
+
return (platform === 'win32' ? win32 : posix).join(...parts);
|
|
8
|
+
}
|
|
9
|
+
/** Return the one explicit export path or the platform-standard SAG connection path. */
|
|
10
|
+
export function platformConnectionPaths(env, platform, home) {
|
|
11
|
+
const explicit = env.SAG_DSH_CONNECTION_FILE;
|
|
12
|
+
if (explicit)
|
|
13
|
+
return [explicit];
|
|
14
|
+
if (platform === 'darwin') {
|
|
15
|
+
return [joinForPlatform(platform, home, 'Library', 'Application Support', 'SAG', 'dsh-connection.json')];
|
|
16
|
+
}
|
|
17
|
+
if (platform === 'win32') {
|
|
18
|
+
return [joinForPlatform(platform, env.APPDATA || joinForPlatform(platform, home, 'AppData', 'Roaming'), 'SAG', 'dsh-connection.json')];
|
|
19
|
+
}
|
|
20
|
+
return [joinForPlatform(platform, env.XDG_CONFIG_HOME || joinForPlatform(platform, home, '.config'), 'sag', 'dsh-connection.json')];
|
|
21
|
+
}
|
|
22
|
+
function message(error) {
|
|
23
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
24
|
+
}
|
|
25
|
+
function abortIfNeeded(signal) {
|
|
26
|
+
if (signal.aborted)
|
|
27
|
+
throw signal.reason;
|
|
28
|
+
}
|
|
29
|
+
function fileDiagnostic(candidate, error) {
|
|
30
|
+
return {
|
|
31
|
+
source: 'file',
|
|
32
|
+
candidate,
|
|
33
|
+
message: message(error),
|
|
34
|
+
action: `Check the SAG connection export at ${candidate} or run ${SAG_SETUP_COMMAND}.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function loopbackDiagnostic(candidate, error) {
|
|
38
|
+
return {
|
|
39
|
+
source: 'loopback',
|
|
40
|
+
candidate,
|
|
41
|
+
message: message(error),
|
|
42
|
+
action: `Start SAG for ${candidate} or run ${SAG_SETUP_COMMAND} --url <SAG loopback URL>.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function safeLoopbackCandidate(url) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = new URL(url);
|
|
48
|
+
parsed.username = '';
|
|
49
|
+
parsed.password = '';
|
|
50
|
+
return parsed.toString();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return url.replace(/(\/\/)[^/?#]*@/, '$1<redacted>@');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function connectionEndpoint(url) {
|
|
57
|
+
const parsed = new URL(url);
|
|
58
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
59
|
+
throw new Error('discovery URL must be an http or https URL');
|
|
60
|
+
}
|
|
61
|
+
if (parsed.username || parsed.password) {
|
|
62
|
+
throw new Error('discovery URL must not include a username or password');
|
|
63
|
+
}
|
|
64
|
+
return new URL(LOOPBACK_CONNECTION_PATH, parsed).toString();
|
|
65
|
+
}
|
|
66
|
+
async function fetchWithTimeout(fetch, url, signal) {
|
|
67
|
+
abortIfNeeded(signal);
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
let rejectCallerAbort;
|
|
70
|
+
const callerAbort = new Promise((_resolve, reject) => { rejectCallerAbort = reject; });
|
|
71
|
+
const abortFromCaller = () => {
|
|
72
|
+
controller.abort(signal.reason);
|
|
73
|
+
rejectCallerAbort(signal.reason);
|
|
74
|
+
};
|
|
75
|
+
signal.addEventListener('abort', abortFromCaller, { once: true });
|
|
76
|
+
let timer;
|
|
77
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
78
|
+
timer = setTimeout(() => {
|
|
79
|
+
const error = new Error(`timed out after ${LOOPBACK_TIMEOUT_MS}ms`);
|
|
80
|
+
controller.abort(error);
|
|
81
|
+
reject(error);
|
|
82
|
+
}, LOOPBACK_TIMEOUT_MS);
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const response = await Promise.race([fetch(url, { signal: controller.signal }), timeout, callerAbort]);
|
|
86
|
+
abortIfNeeded(signal);
|
|
87
|
+
return response;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (timer !== undefined)
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
signal.removeEventListener('abort', abortFromCaller);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Inspect connection files before configured loopback URLs, returning the first strict v1 descriptor.
|
|
97
|
+
* Discovery never saves a descriptor; its caller decides whether a valid result should be persisted.
|
|
98
|
+
*/
|
|
99
|
+
export async function discoverConnection(options, signal) {
|
|
100
|
+
const diagnostics = [];
|
|
101
|
+
for (const candidate of options.paths) {
|
|
102
|
+
abortIfNeeded(signal);
|
|
103
|
+
try {
|
|
104
|
+
const target = await options.fs.resolve(candidate, { signal });
|
|
105
|
+
const text = await options.fs.readText(target, signal);
|
|
106
|
+
let value;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(text);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new Error(`invalid JSON: ${message(error)}`);
|
|
112
|
+
}
|
|
113
|
+
return { source: 'file', descriptor: parseConnectionDescriptor(value), diagnostics };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (signal.aborted)
|
|
117
|
+
throw error;
|
|
118
|
+
diagnostics.push(fileDiagnostic(candidate, error));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const url of options.urls) {
|
|
122
|
+
abortIfNeeded(signal);
|
|
123
|
+
let candidate = safeLoopbackCandidate(url);
|
|
124
|
+
try {
|
|
125
|
+
candidate = connectionEndpoint(url);
|
|
126
|
+
const response = await fetchWithTimeout(options.fetch, candidate, signal);
|
|
127
|
+
if (!response.ok)
|
|
128
|
+
throw new Error(`HTTP ${response.status}`);
|
|
129
|
+
return { source: 'loopback', descriptor: parseConnectionDescriptor(await response.json()), diagnostics };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (signal.aborted)
|
|
133
|
+
throw error;
|
|
134
|
+
diagnostics.push(loopbackDiagnostic(candidate, error));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { source: undefined, descriptor: undefined, diagnostics };
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Profile-scoped command prefix that reaches the installed dsh-sag bin. */
|
|
2
|
+
export declare const SAG_PROFILE_CLI = "dsh plugin --profile web exec dsh-sag";
|
|
3
|
+
/** Default local SAG discovery/setup command shown in recovery guidance. */
|
|
4
|
+
export declare const SAG_SETUP_COMMAND = "dsh plugin --profile web exec dsh-sag setup";
|
|
5
|
+
/** Non-mutating connection diagnostic command shown in recovery guidance. */
|
|
6
|
+
export declare const SAG_DOCTOR_COMMAND = "dsh plugin --profile web exec dsh-sag doctor";
|
|
7
|
+
//# sourceMappingURL=guidance.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Profile-scoped command prefix that reaches the installed dsh-sag bin. */
|
|
2
|
+
export const SAG_PROFILE_CLI = 'dsh plugin --profile web exec dsh-sag';
|
|
3
|
+
/** Default local SAG discovery/setup command shown in recovery guidance. */
|
|
4
|
+
export const SAG_SETUP_COMMAND = `${SAG_PROFILE_CLI} setup`;
|
|
5
|
+
/** Non-mutating connection diagnostic command shown in recovery guidance. */
|
|
6
|
+
export const SAG_DOCTOR_COMMAND = `${SAG_PROFILE_CLI} doctor`;
|
|
7
|
+
//# sourceMappingURL=guidance.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { DiscoveryResult } from './discovery.js';
|
|
2
|
+
import type { SagCapabilityDescriptor, SagConnectionDescriptor } from './types.js';
|
|
3
|
+
import { type SagGateway } from '../local/gateway.js';
|
|
4
|
+
import { type McpProbeResult } from '../local/mcp-probe.js';
|
|
5
|
+
/** The only connection states exposed to setup, doctor, and tools. */
|
|
6
|
+
export type SagConnectionStatus = 'ready' | 'not-found' | 'unreachable' | 'incompatible';
|
|
7
|
+
/** Non-mutating checks made against one candidate connection. */
|
|
8
|
+
export interface ConnectionInspection {
|
|
9
|
+
readonly status: SagConnectionStatus;
|
|
10
|
+
readonly health: boolean;
|
|
11
|
+
readonly ready: boolean;
|
|
12
|
+
readonly capabilities?: SagCapabilityDescriptor;
|
|
13
|
+
readonly sourceCount: number;
|
|
14
|
+
readonly mcp?: McpProbeResult;
|
|
15
|
+
readonly errors?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/** A manager result suitable for tools and human-readable doctor output. */
|
|
18
|
+
export interface SagConnectionReport extends ConnectionInspection {
|
|
19
|
+
readonly descriptor?: SagConnectionDescriptor;
|
|
20
|
+
readonly gateway?: SagGateway;
|
|
21
|
+
readonly discovery?: DiscoveryResult;
|
|
22
|
+
}
|
|
23
|
+
/** Read access to the explicitly saved connection; runtime discovery never mutates it. */
|
|
24
|
+
export interface SagManagerStore {
|
|
25
|
+
load(): Promise<SagConnectionDescriptor | undefined>;
|
|
26
|
+
}
|
|
27
|
+
/** Injectable connection lifecycle operations. */
|
|
28
|
+
export interface SagConnectionManagerDeps {
|
|
29
|
+
readonly store: SagManagerStore;
|
|
30
|
+
readonly discover: (signal: AbortSignal) => Promise<DiscoveryResult>;
|
|
31
|
+
readonly inspect?: (descriptor: SagConnectionDescriptor, signal: AbortSignal) => Promise<ConnectionInspection>;
|
|
32
|
+
readonly gateway?: (descriptor: SagConnectionDescriptor) => SagGateway;
|
|
33
|
+
/** Hard deadline for one complete discovery and inspection flight. */
|
|
34
|
+
readonly requestTimeoutMs?: number;
|
|
35
|
+
/** Maximum reuse window for a matching validated descriptor and gateway. */
|
|
36
|
+
readonly readyCacheTtlMs?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Run health, readiness, capabilities, source count, and MCP compatibility checks without mutations. */
|
|
39
|
+
export declare function inspectSagConnection(descriptor: SagConnectionDescriptor, signal: AbortSignal, gateway?: SagGateway, mcpProbe?: (descriptor: SagConnectionDescriptor, signal: AbortSignal, capabilities: readonly string[]) => Promise<McpProbeResult>): Promise<ConnectionInspection>;
|
|
40
|
+
/** Lazy, single-flight manager for saved and automatically discovered local SAG connections. */
|
|
41
|
+
export declare class SagConnectionManager {
|
|
42
|
+
private readonly deps;
|
|
43
|
+
private flight;
|
|
44
|
+
private connected;
|
|
45
|
+
private readonly gateway;
|
|
46
|
+
private readonly requestTimeoutMs;
|
|
47
|
+
private readonly readyCacheTtlMs;
|
|
48
|
+
private connectedAt;
|
|
49
|
+
private generation;
|
|
50
|
+
private requestSequence;
|
|
51
|
+
private latestAdmittedRequest;
|
|
52
|
+
/** @param deps - saved-connection reader, discovery, and optional testable network seams. */
|
|
53
|
+
constructor(deps: SagConnectionManagerDeps);
|
|
54
|
+
private report;
|
|
55
|
+
private inspectCandidate;
|
|
56
|
+
private connect;
|
|
57
|
+
private deadline;
|
|
58
|
+
private static fingerprint;
|
|
59
|
+
private closeDetached;
|
|
60
|
+
private replaceConnected;
|
|
61
|
+
private reusableConnected;
|
|
62
|
+
/** Load the caller's latest saved descriptor, then share only a matching connection flight. */
|
|
63
|
+
ensureConnected(signal: AbortSignal): Promise<SagConnectionReport>;
|
|
64
|
+
/** Re-run only non-mutating connection checks for setup and diagnostics. */
|
|
65
|
+
doctor(signal: AbortSignal): Promise<SagConnectionReport>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=manager.d.ts.map
|