shraga 0.1.12 → 0.1.14
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/dist/client/assets/index-ChElotX8.js +1936 -0
- package/dist/client/index.html +1 -1
- package/package.json +1 -2
- package/src/cli.ts +15 -0
- package/src/client/lib/desktopAttention.ts +13 -60
- package/src/client/lib/googleAuthNative.ts +13 -91
- package/src/client/lib/native.ts +18 -22
- package/src/client/lib/nativeProvider.ts +88 -0
- package/src/index.ts +6 -0
- package/src/server/boot.ts +39 -30
- package/src/server/claude.ts +4 -5
- package/src/server/engine/claude-code.ts +7 -5
- package/src/server/engine/types.ts +3 -1
- package/src/server/ingress-router.ts +90 -0
- package/src/server/mcp-sidecar.ts +2 -2
- package/src/server/mcp.ts +3 -5
- package/src/server/paths.ts +57 -6
- package/src/server/prompt-suffix.ts +38 -0
- package/src/server/sessions.ts +2 -0
- package/src/server/shraga-config.ts +2 -4
- package/src/server/skills.ts +3 -5
- package/dist/client/assets/index-FM42KQNo.js +0 -1940
- package/src/server/engine/shared.ts +0 -47
- package/src/server/voice-provider.ts +0 -74
|
@@ -12,8 +12,8 @@ import { DEFAULT_MODEL } from '../directives.ts';
|
|
|
12
12
|
import { resolveModelSwitch } from '../model-aliases.ts';
|
|
13
13
|
import type { WsEvent, AskQuestion, QuestionAnswers, QuestionHandler } from '../claude.ts';
|
|
14
14
|
import type { AgentEngine, EngineStreamOpts, EngineModel } from './types.ts';
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { getPromptSuffix } from '../prompt-suffix.ts';
|
|
16
|
+
import { APP_ROOT } from '../paths.ts';
|
|
17
17
|
const IMMUTABLE_SYSTEM_PROMPT = readFileSync(path.resolve(import.meta.dirname, '../../../defaults/system-prompt.md'), 'utf-8');
|
|
18
18
|
const DEFAULT_USER_PROMPT = `You are a helpful assistant with access to MCP tools.`;
|
|
19
19
|
const DEFAULT_ALLOWED_TOOLS = ['Read', 'Edit', 'Bash', 'WebSearch', 'Glob', 'LS', 'ToolSearch'];
|
|
@@ -174,7 +174,7 @@ export class ClaudeCodeEngine implements AgentEngine {
|
|
|
174
174
|
|
|
175
175
|
async *stream(opts: EngineStreamOpts): AsyncGenerator<WsEvent> {
|
|
176
176
|
const { config, directives } = opts;
|
|
177
|
-
const cwd =
|
|
177
|
+
const cwd = APP_ROOT;
|
|
178
178
|
|
|
179
179
|
const fullPrompt = buildHistoryPrompt(opts.conversation, opts.contextBlock, opts.prompt);
|
|
180
180
|
const permMode = opts.onPermissionRequest ? 'default' : (config.permissionMode ?? 'acceptEdits');
|
|
@@ -265,8 +265,10 @@ export class ClaudeCodeEngine implements AgentEngine {
|
|
|
265
265
|
if (effort) options['effort'] = effort;
|
|
266
266
|
|
|
267
267
|
const userPrompt = config.systemPrompt || DEFAULT_USER_PROMPT;
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
// Optional add-ons may append a system-prompt suffix decided off the opaque turn hints (add-on-owned
|
|
269
|
+
// text). With nothing registered (CE's own state) this is '' and the prompt is byte-identical to before.
|
|
270
|
+
const addonSuffix = getPromptSuffix(opts.turnHints);
|
|
271
|
+
options['systemPrompt'] = `${IMMUTABLE_SYSTEM_PROMPT}\n\n${userPrompt}${addonSuffix ? `\n\n${addonSuffix}` : ''}`;
|
|
270
272
|
if (opts.abortController) options['abortController'] = opts.abortController;
|
|
271
273
|
if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) options['mcpServers'] = opts.mcpServers;
|
|
272
274
|
|
|
@@ -23,7 +23,9 @@ export interface EngineStreamOpts {
|
|
|
23
23
|
onPermissionRequest?: PermissionHandler;
|
|
24
24
|
onDestructiveApproval?: PermissionHandler;
|
|
25
25
|
onUserQuestion?: QuestionHandler;
|
|
26
|
-
|
|
26
|
+
/** Opaque per-send hints bag, forwarded verbatim from the client. The core interprets no key of it;
|
|
27
|
+
* an add-on engine reads its own keys (e.g. a duplex engine's `voice` marker). */
|
|
28
|
+
turnHints?: Record<string, unknown>;
|
|
27
29
|
/** True when the conversation was truncated (user replayed/edited a message) — engines with cached state should reset. */
|
|
28
30
|
conversationReset?: boolean;
|
|
29
31
|
context?: Record<string, string>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Ingress router: tiny host-header TCP router fronting shraga instances.
|
|
3
|
+
// The deployment's ingress (CF Tunnel or Caddy) always points here; this process
|
|
4
|
+
// routes by Host header to local ports. Previews and blue-green flips are just edits
|
|
5
|
+
// to the routing file — this process never restarts during a flip (that's why it runs
|
|
6
|
+
// as its own process, NOT inside the server: `flip-restart` restarts the server while
|
|
7
|
+
// the router holds traffic).
|
|
8
|
+
//
|
|
9
|
+
// bun run src/server/ingress-router.ts # INGRESS_PORT (default 3100)
|
|
10
|
+
// shraga ingress # same, via the CLI
|
|
11
|
+
//
|
|
12
|
+
// Routing file (dataPath('ingress-router.json'), hot-reloaded on change):
|
|
13
|
+
// { "default": 3032, "routes": { "pr-13.preview.agent.example.com": 3850 } }
|
|
14
|
+
//
|
|
15
|
+
// Works at the TCP level: reads bytes until the first request's headers end, parses Host,
|
|
16
|
+
// connects upstream, replays the buffered bytes, then splices both directions blindly.
|
|
17
|
+
// WebSocket upgrades and keep-alive flow through untouched (same upstream per connection).
|
|
18
|
+
|
|
19
|
+
import net from 'node:net';
|
|
20
|
+
import { existsSync, readFileSync, watch, writeFileSync } from 'node:fs';
|
|
21
|
+
import { dataPath } from './paths.ts';
|
|
22
|
+
|
|
23
|
+
const PORT = Number(process.env.INGRESS_PORT) || 3100;
|
|
24
|
+
const CONFIG = dataPath('ingress-router.json');
|
|
25
|
+
const HEADER_LIMIT = 16 * 1024;
|
|
26
|
+
|
|
27
|
+
interface Routing { default: number; routes: Record<string, number>; }
|
|
28
|
+
let routing: Routing = { default: 3032, routes: {} };
|
|
29
|
+
|
|
30
|
+
function loadRouting() {
|
|
31
|
+
try {
|
|
32
|
+
routing = { routes: {}, ...JSON.parse(readFileSync(CONFIG, 'utf-8')) };
|
|
33
|
+
console.log(`[ingress] routing: default→:${routing.default}, ${Object.keys(routing.routes).length} route(s)`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`[ingress] bad routing file, keeping previous:`, (err as Error).message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(CONFIG)) writeFileSync(CONFIG, JSON.stringify(routing, null, 2));
|
|
39
|
+
loadRouting();
|
|
40
|
+
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
watch(CONFIG, () => { // debounce — editors fire multiple events per save
|
|
42
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
43
|
+
reloadTimer = setTimeout(loadRouting, 100);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function upstreamFor(host: string): number {
|
|
47
|
+
const bare = host.toLowerCase().split(':')[0];
|
|
48
|
+
return routing.routes[bare] ?? routing.default;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const server = net.createServer((client) => {
|
|
52
|
+
let buf = Buffer.alloc(0);
|
|
53
|
+
client.once('error', () => client.destroy());
|
|
54
|
+
|
|
55
|
+
const onData = (chunk: Buffer) => {
|
|
56
|
+
buf = Buffer.concat([buf, chunk]);
|
|
57
|
+
const headerEnd = buf.indexOf('\r\n\r\n');
|
|
58
|
+
if (headerEnd === -1) {
|
|
59
|
+
if (buf.length > HEADER_LIMIT) client.destroy();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
client.off('data', onData);
|
|
63
|
+
client.pause();
|
|
64
|
+
|
|
65
|
+
const head = buf.subarray(0, headerEnd).toString('latin1');
|
|
66
|
+
const host = /\r\nhost:\s*([^\r\n]+)/i.exec('\r\n' + head)?.[1]?.trim() ?? '';
|
|
67
|
+
const port = upstreamFor(host);
|
|
68
|
+
|
|
69
|
+
const upstream = net.connect(port, '127.0.0.1', () => {
|
|
70
|
+
client.setTimeout(0); // routed — long-lived (WS) connections idle freely
|
|
71
|
+
upstream.write(buf);
|
|
72
|
+
client.pipe(upstream);
|
|
73
|
+
upstream.pipe(client);
|
|
74
|
+
client.resume();
|
|
75
|
+
});
|
|
76
|
+
const drop = () => { client.destroy(); upstream.destroy(); };
|
|
77
|
+
upstream.on('error', (err) => {
|
|
78
|
+
console.warn(`[ingress] upstream :${port} (${host}):`, err.message);
|
|
79
|
+
if (!client.writableEnded) client.end('HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n');
|
|
80
|
+
upstream.destroy();
|
|
81
|
+
});
|
|
82
|
+
client.on('error', drop);
|
|
83
|
+
client.on('close', () => upstream.destroy());
|
|
84
|
+
upstream.on('close', () => client.destroy());
|
|
85
|
+
};
|
|
86
|
+
client.on('data', onData);
|
|
87
|
+
client.setTimeout(15_000, () => { if (!client.bytesWritten) client.destroy(); });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
server.listen(PORT, () => console.log(`[ingress] listening on :${PORT}, routing file: ${CONFIG}`));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getHttpSidecarSpecs, type HttpSidecarSpec } from './shraga-config.ts';
|
|
4
|
+
import { APP_ROOT } from './paths.ts';
|
|
4
5
|
|
|
5
|
-
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
6
6
|
const sidecars = new Map<string, { proc: ChildProcess; spec: HttpSidecarSpec }>();
|
|
7
7
|
|
|
8
8
|
async function isPortAlive(url: string): Promise<boolean> {
|
|
@@ -17,7 +17,7 @@ async function isPortAlive(url: string): Promise<boolean> {
|
|
|
17
17
|
let shuttingDown = false;
|
|
18
18
|
|
|
19
19
|
function startOne(spec: HttpSidecarSpec, restarts = 0) {
|
|
20
|
-
const vendorDir = path.join(
|
|
20
|
+
const vendorDir = path.join(APP_ROOT, 'vendor', spec.dir);
|
|
21
21
|
const entrypoint = path.join(vendorDir, 'src/mcp/cli.ts');
|
|
22
22
|
const args = ['run', entrypoint, '--port', String(spec.port)];
|
|
23
23
|
const startedAt = Date.now();
|
package/src/server/mcp.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { dataPath } from './paths.ts';
|
|
3
|
+
import { dataPath, APP_ROOT } from './paths.ts';
|
|
4
4
|
import { dataSync } from './data-sync.ts';
|
|
5
5
|
import { getGlobalMcpsFromConfig } from './shraga-config.ts';
|
|
6
6
|
|
|
@@ -151,8 +151,6 @@ function withStdioType(config: McpConfig): McpConfig {
|
|
|
151
151
|
return out;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
155
|
-
|
|
156
154
|
/** Canonical path baked into MCP env when prod has the file deployed (cwd = app dir). */
|
|
157
155
|
const GOOGLE_SA_DEPLOY_REL = './secrets/google-service-account.json';
|
|
158
156
|
|
|
@@ -163,7 +161,7 @@ const GOOGLE_SA_DEPLOY_REL = './secrets/google-service-account.json';
|
|
|
163
161
|
*/
|
|
164
162
|
function finalizeGoogleServiceAccountCredentials(config: McpConfig): McpConfig {
|
|
165
163
|
const jsonFromEnv = process.env.GOOGLE_SERVICE_ACCOUNT_JSON?.trim();
|
|
166
|
-
const defaultAbs = path.join(
|
|
164
|
+
const defaultAbs = path.join(APP_ROOT, 'secrets/google-service-account.json');
|
|
167
165
|
const defaultExists = existsSync(defaultAbs);
|
|
168
166
|
|
|
169
167
|
let result = { ...config };
|
|
@@ -184,7 +182,7 @@ function finalizeGoogleServiceAccountCredentials(config: McpConfig): McpConfig {
|
|
|
184
182
|
const pathOk =
|
|
185
183
|
raw &&
|
|
186
184
|
!raw.includes('${') &&
|
|
187
|
-
existsSync(path.isAbsolute(raw) ? raw : path.resolve(
|
|
185
|
+
existsSync(path.isAbsolute(raw) ? raw : path.resolve(APP_ROOT, raw.replace(/^\.\//, '')));
|
|
188
186
|
|
|
189
187
|
if (pathOk) continue;
|
|
190
188
|
if (!defaultExists) continue;
|
package/src/server/paths.ts
CHANGED
|
@@ -2,7 +2,11 @@ import path from 'node:path';
|
|
|
2
2
|
import { readdirSync } from 'node:fs';
|
|
3
3
|
|
|
4
4
|
function resolveDataDir(): string {
|
|
5
|
-
|
|
5
|
+
// Absolutize against cwd: run.sh sets a RELATIVE `DATA_DIR=data-<env>`, and a relative path breaks
|
|
6
|
+
// any consumer that isn't cwd-relative — notably the dynamic `import(configPath)` in
|
|
7
|
+
// shraga-config.ts, which resolves a relative specifier against the IMPORTING MODULE
|
|
8
|
+
// (`node_modules/shraga/src/server/`), not the process cwd.
|
|
9
|
+
if (process.env.DATA_DIR) return path.resolve(process.env.DATA_DIR);
|
|
6
10
|
const root = process.cwd();
|
|
7
11
|
const hasNamed = readdirSync(root).some(f => f.startsWith('data-'));
|
|
8
12
|
if (hasNamed) {
|
|
@@ -17,8 +21,55 @@ function resolveDataDir(): string {
|
|
|
17
21
|
export const DATA_DIR = resolveDataDir();
|
|
18
22
|
export const dataPath = (...segments: string[]) => path.join(DATA_DIR, ...segments);
|
|
19
23
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// `
|
|
24
|
-
|
|
24
|
+
// ── Two distinct roots. Conflating them is what broke npm-consumer deployments. ──────────────────
|
|
25
|
+
//
|
|
26
|
+
// PACKAGE_ROOT — where the SHRAGA PACKAGE's own shipped assets live (`defaults/`, `dist/client/`,
|
|
27
|
+
// `package.json` — everything in package.json `files`). Package-relative is CORRECT here: in an npm
|
|
28
|
+
// consumer these really do live under `node_modules/shraga/`. Do not "fix" this to APP_ROOT.
|
|
29
|
+
export const PACKAGE_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* APP_ROOT — the DEPLOYMENT/consumer root: where `vendor/`, `secrets/` and `data/` live, and where
|
|
33
|
+
* the agent's project filesystem is rooted. NOT shipped in the package.
|
|
34
|
+
*
|
|
35
|
+
* In a source checkout this equals PACKAGE_ROOT. In an npm consumer (`shraga-circles`, `shraga-ee`)
|
|
36
|
+
* it is the CONSUMER root, while PACKAGE_ROOT is `<consumer>/node_modules/shraga` — resolving vendor
|
|
37
|
+
* from PACKAGE_ROOT is what silently killed ~21/25 MCPs in prod.
|
|
38
|
+
*
|
|
39
|
+
* Signal, in precedence order:
|
|
40
|
+
* 1. SHRAGA_APP_ROOT env — explicit escape hatch for any layout the heuristics get wrong.
|
|
41
|
+
* 2. node_modules ancestor — if this file sits under `.../node_modules/shraga/...`, the app root is
|
|
42
|
+
* the directory CONTAINING that `node_modules`. Independent of cwd, so it survives a server
|
|
43
|
+
* started from anywhere (systemd, a cron shell, `bun --cwd`).
|
|
44
|
+
* 3. process.cwd() — the source-checkout case, and already this module's established app-root signal
|
|
45
|
+
* (see resolveDataDir above; run.sh `cd`s to the app root before launching).
|
|
46
|
+
*
|
|
47
|
+
* Failure modes, explicit:
|
|
48
|
+
* - Hoisted/pnpm layouts where `shraga` resolves to a store dir outside the consumer's own
|
|
49
|
+
* node_modules: rule 2 picks the hoisting root, which may not be the dir holding `vendor/`.
|
|
50
|
+
* - A nested `node_modules/x/node_modules/shraga`: rule 2 stops at the INNERMOST node_modules.
|
|
51
|
+
* Both are exactly why rule 1 exists — set SHRAGA_APP_ROOT and the heuristics are bypassed.
|
|
52
|
+
*/
|
|
53
|
+
function resolveAppRoot(): string {
|
|
54
|
+
const explicit = process.env.SHRAGA_APP_ROOT?.trim();
|
|
55
|
+
if (explicit) return path.resolve(explicit);
|
|
56
|
+
|
|
57
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
58
|
+
const idx = PACKAGE_ROOT.lastIndexOf(marker);
|
|
59
|
+
if (idx !== -1) return PACKAGE_ROOT.slice(0, idx);
|
|
60
|
+
|
|
61
|
+
return process.cwd();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const APP_ROOT = resolveAppRoot();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @deprecated Ambiguous name — use APP_ROOT (vendor/secrets/data, agent cwd) or PACKAGE_ROOT
|
|
68
|
+
* (shipped assets) explicitly.
|
|
69
|
+
*
|
|
70
|
+
* Aliased to APP_ROOT, not PACKAGE_ROOT, deliberately: every remaining external consumer of this
|
|
71
|
+
* export (shraga-ee `engine/cursor.ts`, `engine/agentx.ts`) uses it as the agent's project root —
|
|
72
|
+
* i.e. they meant APP_ROOT and were hitting the same npm-layout bug. Pointing the alias here fixes
|
|
73
|
+
* them without an EE change. Nothing in this package reads shipped assets through it.
|
|
74
|
+
*/
|
|
75
|
+
export const PROJECT_ROOT = APP_ROOT;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// System-prompt-suffix seam — a generic drop-in for optional add-ons to append text to the agent's
|
|
2
|
+
// system prompt, decided per-turn off the opaque hints bag.
|
|
3
|
+
//
|
|
4
|
+
// Every turn, each engine appends whatever the registered contributors return. A contributor reads its
|
|
5
|
+
// OWN keys off the opaque `turnHints` bag (the core interprets none) and returns a suffix — or '' to
|
|
6
|
+
// contribute nothing. The core owns only this seam: it names no add-on concept (voice, etc.). With no
|
|
7
|
+
// contributor registered (CE's own state) the suffix is '' and the assembled prompt is byte-identical
|
|
8
|
+
// to before. An add-on (e.g. shraga-ee's voice feature) registers a contributor that returns its own
|
|
9
|
+
// bundled suffix when its marker (e.g. `turnHints.voice`) is present.
|
|
10
|
+
//
|
|
11
|
+
// Sibling of `turn-context.ts` and shaped like it: typed, optional, registered before startup, a no-op
|
|
12
|
+
// when nothing registers.
|
|
13
|
+
|
|
14
|
+
/** Return a system-prompt suffix for this turn (trimmed by the seam), or '' to contribute nothing.
|
|
15
|
+
* Reads its own keys off the opaque per-turn hints bag. */
|
|
16
|
+
export type PromptSuffixContributor = (turnHints?: Record<string, unknown>) => string;
|
|
17
|
+
|
|
18
|
+
const contributors: PromptSuffixContributor[] = [];
|
|
19
|
+
|
|
20
|
+
/** Register a prompt-suffix contributor. Called by an optional add-on before startup. */
|
|
21
|
+
export function registerPromptSuffix(fn: PromptSuffixContributor): void {
|
|
22
|
+
contributors.push(fn);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The combined suffix for this turn, or '' when nothing contributes. A throwing contributor is
|
|
26
|
+
* contained (logged, skipped) so a broken add-on can't take down a turn. */
|
|
27
|
+
export function getPromptSuffix(turnHints?: Record<string, unknown>): string {
|
|
28
|
+
const parts: string[] = [];
|
|
29
|
+
for (const fn of contributors) {
|
|
30
|
+
try {
|
|
31
|
+
const s = fn(turnHints)?.trim();
|
|
32
|
+
if (s) parts.push(s);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('[prompt-suffix] contributor failed:', (err as Error)?.message ?? err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return parts.join('\n\n');
|
|
38
|
+
}
|
package/src/server/sessions.ts
CHANGED
|
@@ -356,6 +356,8 @@ export type ConvBlock =
|
|
|
356
356
|
| { type: 'tool_use'; tool: string; toolUseId: string; input: unknown }
|
|
357
357
|
| { type: 'tool_result'; toolUseId: string; output: string }
|
|
358
358
|
| { type: 'thinking'; text: string }
|
|
359
|
+
// Persisted block written by an add-on engine's background worker (e.g. EE's duplex voice brain).
|
|
360
|
+
// The core stores/renders it but owns none of its semantics; name kept for stored-history back-compat.
|
|
359
361
|
| { type: 'duplex_result'; label?: string; tier?: string; text?: string }
|
|
360
362
|
| { type: 'summary'; text: string; compactedCount: number }
|
|
361
363
|
| { type: 'compact_marker'; summary: string; compactedCount: number };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { DATA_DIR } from './paths.ts';
|
|
3
|
+
import { DATA_DIR, APP_ROOT } from './paths.ts';
|
|
4
4
|
import type { McpServerConfig, McpConfig, McpHttpServerConfig } from './mcp.ts';
|
|
5
5
|
|
|
6
6
|
/** Shorthand for vendor-dir MCPs (auto-resolves command/args from vendor/{name}) */
|
|
@@ -70,8 +70,6 @@ export function defineConfig(config: ShragaConfig): ShragaConfig {
|
|
|
70
70
|
return config;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
74
|
-
|
|
75
73
|
/**
|
|
76
74
|
* Config filenames, in precedence order. `shraga.config.ts` is canonical; `unclaw.config.ts` is
|
|
77
75
|
* the legacy name kept for back-compat — existing deployments have that file in their data dir,
|
|
@@ -131,7 +129,7 @@ export function getGlobalMcpsFromConfig(): McpConfig {
|
|
|
131
129
|
result[name] = { type: 'stdio', ...full } satisfies McpServerConfig;
|
|
132
130
|
} else {
|
|
133
131
|
const shorthand = entry as McpShorthandEntry;
|
|
134
|
-
const vendorDir = path.join(
|
|
132
|
+
const vendorDir = path.join(APP_ROOT, 'vendor', shorthand.dir ?? name);
|
|
135
133
|
const command = shorthand.command ?? 'bun';
|
|
136
134
|
const args = shorthand.args ?? ['run', path.join(vendorDir, 'src/mcp/cli.ts'), '--stdio'];
|
|
137
135
|
const env: Record<string, string> = {};
|
package/src/server/skills.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, renameSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { DATA_DIR, dataPath } from './paths.ts';
|
|
3
|
+
import { DATA_DIR, dataPath, APP_ROOT } from './paths.ts';
|
|
4
4
|
import { getBuiltinSkillNames } from './seed.ts';
|
|
5
5
|
import { dataSync } from './data-sync.ts';
|
|
6
6
|
import { injectFile } from './file-inject.ts';
|
|
7
7
|
import { getGlobalMcpConfig } from './mcp.ts';
|
|
8
8
|
|
|
9
|
-
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
10
|
-
|
|
11
9
|
const SKILLS_DIR = dataPath('skills');
|
|
12
10
|
const DEFAULTS_PATH = dataPath('skills-defaults.json');
|
|
13
11
|
|
|
@@ -108,7 +106,7 @@ function formatMcpCommandBlock(mcpName: string, skillBody: string, args: string)
|
|
|
108
106
|
* Markdown from `vendor/<serverName>/.claude/skills/<serverName>/SKILL.md` — same file the MCP exposes as skill://serverName/workflow.
|
|
109
107
|
*/
|
|
110
108
|
export function resolveMcpBundledSkillContent(serverName: string): string | null {
|
|
111
|
-
const file = path.join(
|
|
109
|
+
const file = path.join(APP_ROOT, 'vendor', serverName, '.claude/skills', serverName, 'SKILL.md');
|
|
112
110
|
if (!existsSync(file)) return null;
|
|
113
111
|
return readFileSync(file, 'utf-8');
|
|
114
112
|
}
|
|
@@ -128,7 +126,7 @@ export function resolvedSkillInjectionBlock(name: string): string | null {
|
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
function mcpSkillFilePath(serverName: string): string {
|
|
131
|
-
return path.join(
|
|
129
|
+
return path.join(APP_ROOT, 'vendor', serverName, '.claude/skills', serverName, 'SKILL.md');
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
/**
|