shraga 0.1.159 → 0.1.161

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.
@@ -13,7 +13,7 @@
13
13
  <link rel="preconnect" href="https://fonts.googleapis.com" />
14
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15
15
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
16
- <script type="module" crossorigin src="/assets/index-CcLqqS5x.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-C46Azkf_.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-NtN1I_dh.css">
18
18
  </head>
19
19
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.159",
3
+ "version": "0.1.161",
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",
@@ -313,6 +313,11 @@ async function* runTurn(opts: StreamChatOpts, guard?: TurnGuard): AsyncGenerator
313
313
 
314
314
  const sessionMeta = opts.sessionId ? getSession(opts.sessionId) : undefined;
315
315
  const directives: Directives = { ...sessionMeta?.directives, ...parsed };
316
+ // A two-model mode (delegate/advisor) is sticky like the model: `[off]` clears it, and a new mode
317
+ // replaces the other one. `modeOff` is a command, not a setting — never stored.
318
+ if (parsed.modeOff || parsed.advisor) delete directives.delegate;
319
+ if (parsed.modeOff || parsed.delegate) delete directives.advisor;
320
+ delete directives.modeOff;
316
321
  // Persist ONLY what was explicitly chosen (inline directives, or the Agent Config panel's
317
322
  // per-conversation save). Gaps stay gaps: every engine resolves `directives.x ?? config.x` at
318
323
  // send time, so the global agent-config keeps applying live to sessions nobody pinned —
@@ -6,6 +6,12 @@ export interface Directives {
6
6
  engine?: string;
7
7
  /** claude-code SDK session resume for this conversation (overrides agent-config `sdkResume`). */
8
8
  resume?: boolean;
9
+ /** agentx DELEGATE mode `[planner->worker]` (key `delegate`): the worker model; `model` (left side) plans. */
10
+ delegate?: string;
11
+ /** agentx ADVISOR mode `[worker<-advisor]` (key `advisor`): the advisor model; `model` (left side) works. */
12
+ advisor?: string;
13
+ /** `[off]` / `[mode:off]`: clears a sticky delegate/advisor mode. Consumed by streamChat, never stored. */
14
+ modeOff?: boolean;
9
15
  }
10
16
 
11
17
  export interface ParsedPrompt {
@@ -28,7 +34,15 @@ import { MODEL_ALIASES } from './model-aliases.ts';
28
34
 
29
35
  const DIRECTIVE_RE = /^\s*\[([^\]]*)\]\s*([\s\S]*)/;
30
36
 
31
- const DIRECTIVE_KEYS = ['model', 'turns', 'thinking', 'think', 'effort', 'engine', 'resume'];
37
+ const DIRECTIVE_KEYS = ['model', 'turns', 'thinking', 'think', 'effort', 'engine', 'resume', 'delegate', 'advisor', 'mode'];
38
+
39
+ /** `a->b` (delegate) / `a<-b` (advisor) — agentx's two-model mode grammar; the left side may be empty. */
40
+ function splitModePair(token: string): { left: string; op: '->' | '<-'; right: string } | null {
41
+ const i = token.search(/->|<-/);
42
+ if (i === -1) return null;
43
+ const right = token.slice(i + 2).trim();
44
+ return right ? { left: token.slice(0, i).trim(), op: token.slice(i, i + 2) as '->' | '<-', right } : null;
45
+ }
32
46
 
33
47
  /** MODEL_ALIASES only covers the bare Anthropic shorthands. A `provider/model` id
34
48
  * (`cursor/composer-2.5`, `openai/gpt-5.6`) is already concrete — gating it on the alias table
@@ -72,6 +86,9 @@ export function isDirectiveGroup(raw: string): boolean {
72
86
  const colonIdx = t.indexOf(':');
73
87
  if (colonIdx !== -1) return DIRECTIVE_KEYS.includes(t.slice(0, colonIdx).trim().toLowerCase());
74
88
  const v = t.toLowerCase();
89
+ const pair = splitModePair(v);
90
+ if (pair) return (!pair.left || !!resolveModelAlias(pair.left)) && !!resolveModelAlias(pair.right);
91
+ if (v === 'off') return true;
75
92
  return !!MODEL_ALIASES[v] || !!modelResolver?.(v) || /^\d+$/.test(v) || ['think', 'adaptive', 'nothink', 'nothinking'].includes(v);
76
93
  });
77
94
  }
@@ -132,6 +149,17 @@ export function parseDirectives(text: string): ParsedPrompt {
132
149
  unresolvedModel = applyDirective(directives, key, val) ?? unresolvedModel;
133
150
  } else {
134
151
  const val = t.toLowerCase();
152
+ const pair = splitModePair(val);
153
+ if (pair) {
154
+ // Both sides must resolve: a half-resolved pair would silently run a mode on the wrong model.
155
+ const worker = resolveModelAlias(pair.right);
156
+ if (worker && (!pair.left || resolveModelToken(directives, pair.left))) {
157
+ if (pair.op === '->') directives.delegate = worker.model; else directives.advisor = worker.model;
158
+ } else unresolvedModel ??= t;
159
+ positionalIndex++;
160
+ continue;
161
+ }
162
+ if (val === 'off') { directives.modeOff = true; positionalIndex++; continue; }
135
163
  if (positionalIndex === 0 && resolveModelToken(directives, val)) {
136
164
  // handled
137
165
  } else if (positionalIndex <= 1 && /^\d+$/.test(val)) {
@@ -179,6 +207,18 @@ function applyDirective(d: Directives, key: string, val: string): string | undef
179
207
  case 'engine':
180
208
  d.engine = val;
181
209
  break;
210
+ case 'delegate':
211
+ case 'advisor': {
212
+ const hit = resolveModelAlias(val);
213
+ if (hit) d[key] = hit.model;
214
+ else if (isQualifiedModel(val)) d[key] = val;
215
+ else return val;
216
+ break;
217
+ }
218
+ case 'mode':
219
+ if (val === 'off') d.modeOff = true;
220
+ else console.warn(`[directives] Invalid mode value: "${val}"`);
221
+ break;
182
222
  case 'resume':
183
223
  if (['on', 'true', '1'].includes(val)) d.resume = true;
184
224
  else if (['off', 'false', '0'].includes(val)) d.resume = false;
@@ -538,11 +538,13 @@ export class ClaudeCodeEngine implements AgentEngine {
538
538
  // where the profile grants Bash — job_start is arbitrary shell — and only for a real session id
539
539
  // (wake reports need a session to land in).
540
540
  if (durableJobs) {
541
- // The job store scrubs `*_API_KEY` from children, so a nested agent (the social legs' agentx worker)
542
- // died "No provider key found" (2026-09-24). Hand back the provider keys THIS turn's own env already
543
- // carries (profile-filtered by buildAgentEnv) — the same set the agentx engine gives its jobs.
544
- const providerEnv = Object.fromEntries(Object.entries(sdkEnv).filter(([k, v]) => /_API_KEY$/.test(k) && v));
545
- const server = jobsMcpServer({ sessionId: opts.sessionId!, uid: opts.uid, userEmail: opts.userEmail, cwd, env: providerEnv });
541
+ // The job store scrubs credentials from children, but here the turn's own Bash already sees this
542
+ // env (profile-filtered by buildAgentEnv). Scrubbing it made job_start strictly weaker than Bash, and
543
+ // the hook steers long scripts INTO job_start: a nested agent died "No provider key found" (2026-09-24),
544
+ // AGF Post-Daily lost MIXPANEL_API_SECRET_AGF/GOOGLE_SERVICE_ACCOUNT (2026-10-20). Hand the job the
545
+ // turn's env, minus PORT (the server's own; a job honouring it would shadow us).
546
+ const jobEnv = Object.fromEntries(Object.entries(sdkEnv).filter(([k, v]) => v && k !== 'PORT'));
547
+ const server = jobsMcpServer({ sessionId: opts.sessionId!, uid: opts.uid, userEmail: opts.userEmail, cwd, env: jobEnv });
546
548
  options['mcpServers'] = { ...(options['mcpServers'] as object | undefined), [server.name]: server };
547
549
  options['allowedTools'] = [...((options['allowedTools'] as string[] | undefined) ?? allowedTools), ...JOBS_TOOL_IDS];
548
550
  }