shraga 0.1.13 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -13,8 +13,7 @@ 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
15
  import { getPromptSuffix } from '../prompt-suffix.ts';
16
-
17
- const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..', '..');
16
+ import { APP_ROOT } from '../paths.ts';
18
17
  const IMMUTABLE_SYSTEM_PROMPT = readFileSync(path.resolve(import.meta.dirname, '../../../defaults/system-prompt.md'), 'utf-8');
19
18
  const DEFAULT_USER_PROMPT = `You are a helpful assistant with access to MCP tools.`;
20
19
  const DEFAULT_ALLOWED_TOOLS = ['Read', 'Edit', 'Bash', 'WebSearch', 'Glob', 'LS', 'ToolSearch'];
@@ -175,7 +174,7 @@ export class ClaudeCodeEngine implements AgentEngine {
175
174
 
176
175
  async *stream(opts: EngineStreamOpts): AsyncGenerator<WsEvent> {
177
176
  const { config, directives } = opts;
178
- const cwd = PROJECT_ROOT;
177
+ const cwd = APP_ROOT;
179
178
 
180
179
  const fullPrompt = buildHistoryPrompt(opts.conversation, opts.contextBlock, opts.prompt);
181
180
  const permMode = opts.onPermissionRequest ? 'default' : (config.permissionMode ?? 'acceptEdits');
@@ -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(PROJECT_ROOT, 'vendor', spec.dir);
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(PROJECT_ROOT, 'secrets/google-service-account.json');
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(PROJECT_ROOT, raw.replace(/^\.\//, '')));
185
+ existsSync(path.isAbsolute(raw) ? raw : path.resolve(APP_ROOT, raw.replace(/^\.\//, '')));
188
186
 
189
187
  if (pathOk) continue;
190
188
  if (!defaultExists) continue;
@@ -2,7 +2,11 @@ import path from 'node:path';
2
2
  import { readdirSync } from 'node:fs';
3
3
 
4
4
  function resolveDataDir(): string {
5
- if (process.env.DATA_DIR) return process.env.DATA_DIR;
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
- // The Shraga app root (where `defaults/` lives and the agent's project filesystem is rooted).
21
- // Derived from THIS module's stable location — `src/server/paths.ts` → repo root is two dirs up — so
22
- // it's correct even for code loaded from an overlay checkout in a different directory (where
23
- // `import.meta.dirname`-relative math would resolve to the overlay, not the app).
24
- export const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..');
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;
@@ -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(PROJECT_ROOT, 'vendor', shorthand.dir ?? name);
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> = {};
@@ -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(PROJECT_ROOT, 'vendor', serverName, '.claude/skills', serverName, 'SKILL.md');
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(PROJECT_ROOT, 'vendor', serverName, '.claude/skills', serverName, 'SKILL.md');
129
+ return path.join(APP_ROOT, 'vendor', serverName, '.claude/skills', serverName, 'SKILL.md');
132
130
  }
133
131
 
134
132
  /**