shraga 0.1.158 → 0.1.160
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/defaults/skills/platform.md +1 -0
- package/defaults/skills/scheduler.md +2 -0
- package/dist/client/assets/{index-BLAQpfJ9.css → index-NtN1I_dh.css} +1 -1
- package/dist/client/assets/{index-z_8q_3F7.js → index-yBsLZ3yV.js} +173 -173
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/client/components/ConfigPanel.tsx +29 -13
- package/src/client/components/schedules/ScheduleEditor.tsx +12 -2
- package/src/server/boot.ts +2 -1
- package/src/server/claude.ts +11 -1
- package/src/server/engine/claude-code.ts +7 -5
- package/src/server/scheduler/runner.ts +6 -1
- package/src/server/shraga-config.ts +2 -0
package/dist/client/index.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
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-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-yBsLZ3yV.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-NtN1I_dh.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.160",
|
|
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",
|
|
@@ -11,6 +11,8 @@ import { ClaudeAccountSection } from './ClaudeAccountSection';
|
|
|
11
11
|
|
|
12
12
|
interface AgentConfig {
|
|
13
13
|
model?: string;
|
|
14
|
+
/** Default model for scheduled runs; unset = same as `model`. */
|
|
15
|
+
scheduleModel?: string;
|
|
14
16
|
engine?: string;
|
|
15
17
|
systemPrompt?: string;
|
|
16
18
|
allowedTools?: string[];
|
|
@@ -176,6 +178,18 @@ export function ConfigPanel({ getToken, onSaved, trigger, sessionId, sessionDire
|
|
|
176
178
|
)
|
|
177
179
|
: null;
|
|
178
180
|
|
|
181
|
+
/** The model <option>s (grouped by provider when the engine has several). `skipDefault` drops the "" entry. */
|
|
182
|
+
const modelOptions = (skipDefault = false) => {
|
|
183
|
+
const opts = (list: EngineModel[]) => list.filter((m) => !skipDefault || m.value).map((m) => (
|
|
184
|
+
<option key={m.value} value={m.value}>{m.label}</option>
|
|
185
|
+
));
|
|
186
|
+
return groupedModels
|
|
187
|
+
? groupedModels.map(([provider, providerModels]) => (
|
|
188
|
+
<optgroup key={provider} label={provider.charAt(0).toUpperCase() + provider.slice(1)}>{opts(providerModels)}</optgroup>
|
|
189
|
+
))
|
|
190
|
+
: opts(models);
|
|
191
|
+
};
|
|
192
|
+
|
|
179
193
|
const selectClass = 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
|
|
180
194
|
|
|
181
195
|
return (
|
|
@@ -237,20 +251,22 @@ export function ConfigPanel({ getToken, onSaved, trigger, sessionId, sessionDire
|
|
|
237
251
|
onChange={(e) => setConfig((c) => ({ ...c, model: e.target.value || undefined }))}
|
|
238
252
|
className={selectClass}
|
|
239
253
|
>
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)}
|
|
254
|
+
{modelOptions()}
|
|
255
|
+
</select>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div>
|
|
259
|
+
<label className="text-sm font-medium mb-1.5 block">Scheduled tasks model</label>
|
|
260
|
+
<select
|
|
261
|
+
disabled={!canGlobal}
|
|
262
|
+
value={config.scheduleModel ?? ''}
|
|
263
|
+
onChange={(e) => setConfig((c) => ({ ...c, scheduleModel: e.target.value || undefined }))}
|
|
264
|
+
className={selectClass}
|
|
265
|
+
>
|
|
266
|
+
<option value="">Same as default model ({(sessionId && !applyGlobally ? globalRef.current.model : config.model) || DEFAULT_MODEL})</option>
|
|
267
|
+
{modelOptions(true)}
|
|
253
268
|
</select>
|
|
269
|
+
<p className="text-xs text-muted-foreground mt-1">Used by scheduled runs whose prompt has no <code>[model:…]</code> directive.</p>
|
|
254
270
|
</div>
|
|
255
271
|
|
|
256
272
|
{slots.settingsSections?.({ getToken, sessionId })}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import cronstrue from 'cronstrue';
|
|
3
3
|
import type { Schedule, Trigger, Task } from '@/lib/schedule-types';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
@@ -7,6 +7,7 @@ import { Textarea } from '../ui/textarea';
|
|
|
7
7
|
import { AutocompleteTextarea } from '../AutocompleteTextarea';
|
|
8
8
|
import { useEngines } from '@/hooks/useEngines';
|
|
9
9
|
import { readRuntimeDirective, writeRuntimeDirective } from '@/lib/prompt-directives';
|
|
10
|
+
import { DEFAULT_MODEL } from '../../../shared/models';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
getToken: () => Promise<string | null>;
|
|
@@ -72,6 +73,15 @@ function MatchEditor({ match, onChange }: { match: Record<string, string>; onCha
|
|
|
72
73
|
* drift out of sync with the prompt the way a shadow `task.model` did. */
|
|
73
74
|
function RuntimePicker({ prompt, onChange, getToken }: { prompt: string; onChange: (p: string) => void; getToken: () => Promise<string | null> }) {
|
|
74
75
|
const { engines } = useEngines(getToken);
|
|
76
|
+
// The model an unpinned scheduled run actually uses: scheduleModel > model > built-in default.
|
|
77
|
+
const [defaultModel, setDefaultModel] = useState<string>();
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
let alive = true;
|
|
80
|
+
getToken().then((token) => (token ? fetch('/api/config', { headers: { Authorization: `Bearer ${token}` } }).then((r) => r.json()) : null))
|
|
81
|
+
.then((c) => { if (alive && c) setDefaultModel(c.scheduleModel || c.model || DEFAULT_MODEL); })
|
|
82
|
+
.catch((e) => console.warn('[schedules] agent config load failed', e));
|
|
83
|
+
return () => { alive = false; };
|
|
84
|
+
}, [getToken]);
|
|
75
85
|
// Until the registry has loaded we cannot tell a bare model token from prose, so a rewrite could
|
|
76
86
|
// leave a stale duplicate pin behind. Read-only until then.
|
|
77
87
|
const ready = engines.length > 0;
|
|
@@ -111,7 +121,7 @@ function RuntimePicker({ prompt, onChange, getToken }: { prompt: string; onChang
|
|
|
111
121
|
{engines.map((e) => <option key={e.name} value={e.name}>{e.name}</option>)}
|
|
112
122
|
</select>
|
|
113
123
|
<select className={selectClass} disabled={!ready} value={sel.model ?? ''} onChange={(e) => set({ model: e.target.value || undefined })}>
|
|
114
|
-
<option value="">Default model (agent config)</option>
|
|
124
|
+
<option value="">Default model ({(!sel.engineExplicit && defaultModel) || 'agent config'})</option>
|
|
115
125
|
{!modelListed && <option value={sel.model}>{sel.model} — not offered by this engine</option>}
|
|
116
126
|
{options.map(({ engine, models }) => (
|
|
117
127
|
<optgroup key={engine} label={engine}>
|
package/src/server/boot.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { setBroadcaster, emitToSession } from './session-bus.ts';
|
|
|
31
31
|
import { TURN_CONTROL_EVENTS } from './turn-stream.ts';
|
|
32
32
|
import * as scheduler from './scheduler/index.ts';
|
|
33
33
|
import { initPolls } from './polls.ts';
|
|
34
|
+
import { isScheduledSession } from './scheduler/runner.ts';
|
|
34
35
|
import { initBackgroundJobs } from './background-jobs.ts';
|
|
35
36
|
import { pushEnabled } from './push/push.ts';
|
|
36
37
|
import { upsertToken, removeToken } from './push/store.ts';
|
|
@@ -1365,7 +1366,7 @@ initPolls({
|
|
|
1365
1366
|
// NOTHING until it ended — and an add-on engine's subagent events were dropped outright — so a
|
|
1366
1367
|
// turn that dispatched a worker was indistinguishable from one that only claimed to.
|
|
1367
1368
|
return await consumeStream(
|
|
1368
|
-
streamChat({ principal: fromInternal({ uid, email: userEmail, lane: 'wake' }), prompt, sessionId, uid, userEmail, mcpServers: getMcpConfig(uid), abortController, onPermissionRequest: async () => ({ allow: true }) }),
|
|
1369
|
+
streamChat({ principal: fromInternal({ uid, email: userEmail, lane: 'wake' }), prompt, sessionId, uid, userEmail, mcpServers: getMcpConfig(uid), abortController, onPermissionRequest: async () => ({ allow: true }), scheduled: isScheduledSession(sessionId) }),
|
|
1369
1370
|
undefined,
|
|
1370
1371
|
{
|
|
1371
1372
|
maxResultChars: STREAMED_RESULT_MAX,
|
package/src/server/claude.ts
CHANGED
|
@@ -207,6 +207,8 @@ export interface StreamChatOpts {
|
|
|
207
207
|
* where an add-on's contributor reads its own keys. */
|
|
208
208
|
turnHints?: Record<string, unknown>;
|
|
209
209
|
context?: Record<string, string>;
|
|
210
|
+
/** Unattended scheduled run: `config.scheduleModel` (when set) replaces `config.model` as the default. */
|
|
211
|
+
scheduled?: boolean;
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
/** Run one agent turn. Security wrapper: resolves the principal's role/profile and audits turn.start / turn.end.
|
|
@@ -285,8 +287,16 @@ export async function taintSession(sessionId: string, principal: Principal | (()
|
|
|
285
287
|
return lowerSessionFloor(sessionId, resolvePrincipal(sec.policy, p).rank);
|
|
286
288
|
}
|
|
287
289
|
|
|
288
|
-
|
|
290
|
+
/** The agent config a turn runs under. A scheduled run swaps in `scheduleModel` as its default model —
|
|
291
|
+
* at the config layer, so every engine's `directives.model || config.model` picks it up and an
|
|
292
|
+
* explicit `[model:…]` directive still wins. */
|
|
293
|
+
export function effectiveAgentConfig(opts: Pick<StreamChatOpts, 'scheduled'>): AgentConfig {
|
|
289
294
|
const config = getAgentConfig();
|
|
295
|
+
return opts.scheduled && config.scheduleModel ? { ...config, model: config.scheduleModel } : config;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function* runTurn(opts: StreamChatOpts, guard?: TurnGuard): AsyncGenerator<WsEvent> {
|
|
299
|
+
const config = effectiveAgentConfig(opts);
|
|
290
300
|
const { prompt: cleanPrompt, directives: parsed, unresolvedModel } = parseDirectives(opts.prompt);
|
|
291
301
|
|
|
292
302
|
// An explicit model selection that resolves to nothing is a HARD stop, for the same reason an
|
|
@@ -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
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
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
|
}
|
|
@@ -182,6 +182,10 @@ function formatEventBlock(e: EventContext): string {
|
|
|
182
182
|
return `An external event triggered this automation.\nSource: ${e.source}\nEvent data:\n\`\`\`json\n${body}\n\`\`\``;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/** Prefix of every session id a scheduled run mints — the marker that a session belongs to the scheduler. */
|
|
186
|
+
export const SCHEDULED_SESSION_PREFIX = 'sched-';
|
|
187
|
+
export const isScheduledSession = (sessionId: string): boolean => sessionId.startsWith(SCHEDULED_SESSION_PREFIX);
|
|
188
|
+
|
|
185
189
|
export async function runSchedule(
|
|
186
190
|
schedule: Schedule,
|
|
187
191
|
onEvent: (ev: object) => void,
|
|
@@ -192,7 +196,7 @@ export async function runSchedule(
|
|
|
192
196
|
): Promise<ScheduleRunSummary> {
|
|
193
197
|
const now = Date.now();
|
|
194
198
|
// Resume reuses the interrupted run's session (same convo) instead of spawning a new one.
|
|
195
|
-
const sessionId = resume ? resume.sessionId :
|
|
199
|
+
const sessionId = resume ? resume.sessionId : `${SCHEDULED_SESSION_PREFIX}${schedule.id}-${now}`;
|
|
196
200
|
const abortController = new AbortController();
|
|
197
201
|
registerRun(sessionId, abortController);
|
|
198
202
|
|
|
@@ -326,6 +330,7 @@ export async function runSchedule(
|
|
|
326
330
|
abortController,
|
|
327
331
|
onPermissionRequest,
|
|
328
332
|
foregroundBashCommand: allowedCmd ?? undefined,
|
|
333
|
+
scheduled: true,
|
|
329
334
|
})) {
|
|
330
335
|
onEvent({ type: 'session_stream', sessionId, event: ev });
|
|
331
336
|
if (ev.type === 'text_delta') {
|
|
@@ -44,6 +44,8 @@ function isFullEntry(entry: McpEntry): entry is McpFullEntry {
|
|
|
44
44
|
|
|
45
45
|
export interface AgentSettings {
|
|
46
46
|
model?: string;
|
|
47
|
+
/** Model for scheduled (unattended) runs whose prompt names no `[model:…]`. Unset = `model`. */
|
|
48
|
+
scheduleModel?: string;
|
|
47
49
|
engine?: string;
|
|
48
50
|
systemPrompt?: string;
|
|
49
51
|
allowedTools?: string[];
|