shraga 0.1.52 → 0.1.54
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.
|
@@ -130,14 +130,21 @@ Trigger:
|
|
|
130
130
|
{ kind: "cron", expr: "<cron>", tz: "<IANA_tz>" }
|
|
131
131
|
{ kind: "event", source: "<name>", match?: { "<dot.path>": "<value>" } }
|
|
132
132
|
|
|
133
|
-
Task:
|
|
134
|
-
{ kind: "prompt", prompt: "<text>" }
|
|
135
|
-
{ kind: "bash", command: "<cmd>" }
|
|
133
|
+
Task: // model/engine pin the runtime (see below)
|
|
134
|
+
{ kind: "prompt", prompt: "<text>", model?: "<model>", engine?: "<engine>" }
|
|
135
|
+
{ kind: "bash", command: "<cmd>", model?: "<model>", engine?: "<engine>" }
|
|
136
136
|
|
|
137
137
|
Schedule: { id, name, enabled, trigger, task, scope, createdBy, nextRun?, lastRun?, runCount,
|
|
138
138
|
onMissed?, missedRun? }
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
`task.model` / `task.engine` pin what the run executes on, instead of the instance default:
|
|
142
|
+
`model` takes an alias (`opus`, `haiku`) or a provider-qualified id (`cursor/composer-2.5`);
|
|
143
|
+
`engine` names a registered runtime (`claude-code`, `agentx`, `cursor` — availability is
|
|
144
|
+
deployment-specific). Both are prepended to the prompt as a `[engine:…,model:…]` directive, so a
|
|
145
|
+
prompt that opens with its OWN model group (`[opus] …`) would out-rank the pin — drop it from the
|
|
146
|
+
prompt text when pinning. `job` tasks run a command, not an agent, so neither applies.
|
|
147
|
+
|
|
141
148
|
## Missed windows (`onMissed` / `missedRun`)
|
|
142
149
|
|
|
143
150
|
If a window elapses with nothing running (deploy, crash, power cut — or the host **suspending**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
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",
|
|
@@ -5,8 +5,8 @@ export type Trigger =
|
|
|
5
5
|
| { kind: 'event'; source: string; match?: Record<string, string> };
|
|
6
6
|
|
|
7
7
|
export type Task =
|
|
8
|
-
| { kind: 'prompt'; prompt: string; model?: string }
|
|
9
|
-
| { kind: 'bash'; command: string; model?: string }
|
|
8
|
+
| { kind: 'prompt'; prompt: string; model?: string; engine?: string }
|
|
9
|
+
| { kind: 'bash'; command: string; model?: string; engine?: string }
|
|
10
10
|
| { kind: 'job'; command: string };
|
|
11
11
|
|
|
12
12
|
export type Scope = 'system' | 'user';
|
package/src/server/directives.ts
CHANGED
|
@@ -25,6 +25,13 @@ const DIRECTIVE_RE = /^\s*\[([^\]]*)\]\s*([\s\S]*)/;
|
|
|
25
25
|
|
|
26
26
|
const DIRECTIVE_KEYS = ['model', 'turns', 'thinking', 'think', 'effort', 'engine'];
|
|
27
27
|
|
|
28
|
+
/** MODEL_ALIASES only covers the bare Anthropic shorthands. A `provider/model` id
|
|
29
|
+
* (`cursor/composer-2.5`, `openai/gpt-5.6`) is already concrete — gating it on the alias table
|
|
30
|
+
* silently dropped it and ran the instance default instead. Honoured ONLY in `model:` key form:
|
|
31
|
+
* as a bare positional it is indistinguishable from prompt text like `[src/foo.ts]`, which a
|
|
32
|
+
* second bracket group would then swallow. */
|
|
33
|
+
const isQualifiedModel = (v: string) => /^[a-z0-9._-]+\/[a-z0-9./_-]+$/.test(v);
|
|
34
|
+
|
|
28
35
|
/** Does a bracket group look like directives (vs. prompt text that happens to start with `[`)?
|
|
29
36
|
* Every token must be a known key:value or a known positional, else we leave the group alone. */
|
|
30
37
|
function isDirectiveGroup(raw: string): boolean {
|
|
@@ -84,7 +91,9 @@ export function parseDirectives(text: string): ParsedPrompt {
|
|
|
84
91
|
} else if (['nothink', 'nothinking'].includes(val)) {
|
|
85
92
|
directives.thinking = 'disabled';
|
|
86
93
|
} else if (positionalIndex === 0) {
|
|
87
|
-
console.warn(
|
|
94
|
+
console.warn(isQualifiedModel(val)
|
|
95
|
+
? `[directives] Provider-qualified model needs key form: "[model:${t}]"`
|
|
96
|
+
: `[directives] Unknown model alias: "${t}"`);
|
|
88
97
|
}
|
|
89
98
|
positionalIndex++;
|
|
90
99
|
}
|
|
@@ -97,6 +106,7 @@ function applyDirective(d: Directives, key: string, val: string) {
|
|
|
97
106
|
switch (key) {
|
|
98
107
|
case 'model':
|
|
99
108
|
if (MODEL_ALIASES[val]) d.model = MODEL_ALIASES[val];
|
|
109
|
+
else if (isQualifiedModel(val)) d.model = val;
|
|
100
110
|
else console.warn(`[directives] Unknown model alias: "${val}"`);
|
|
101
111
|
break;
|
|
102
112
|
case 'turns':
|
|
@@ -300,8 +300,11 @@ function applyModule(rec: InstalledModule): void {
|
|
|
300
300
|
const id = scheduleIdFor(rec, def);
|
|
301
301
|
expectedIds.add(id);
|
|
302
302
|
const rendered = renderDeep({ name: def.name, trigger: def.trigger, task: def.task }, config, `${rec.name}/schedules/${def.def}`);
|
|
303
|
-
// A templated
|
|
304
|
-
|
|
303
|
+
// A templated runtime knob rendered to "" must not ship a bogus empty pin — omit the field.
|
|
304
|
+
for (const k of ['model', 'engine'] as const) {
|
|
305
|
+
const t = rendered.task as Record<string, unknown>;
|
|
306
|
+
if (k in t && !t[k]) delete t[k];
|
|
307
|
+
}
|
|
305
308
|
const existing = scheduler.getSchedule(id);
|
|
306
309
|
const now = Date.now();
|
|
307
310
|
const schedule: Schedule = {
|
|
@@ -140,10 +140,13 @@ export async function runSchedule(
|
|
|
140
140
|
if (eventCtx) base = `${base}\n\n---\n${formatEventBlock(eventCtx)}`;
|
|
141
141
|
prompt = base;
|
|
142
142
|
}
|
|
143
|
-
// task.model
|
|
144
|
-
// strips
|
|
145
|
-
// saved prompt, so the session UI shows
|
|
146
|
-
if (!resume
|
|
143
|
+
// task.engine/task.model ride the same prompt-directive channel users type by hand —
|
|
144
|
+
// parseDirectives strips them and resolves aliases. Prepending (vs new plumbing) also persists the
|
|
145
|
+
// choice into the saved prompt, so the session UI shows what the schedule actually requested.
|
|
146
|
+
if (!resume) {
|
|
147
|
+
const pins = [task.engine && `engine:${task.engine}`, task.model && `model:${task.model}`].filter(Boolean);
|
|
148
|
+
if (pins.length) prompt = `[${pins.join(',')}] ${prompt}`;
|
|
149
|
+
}
|
|
147
150
|
|
|
148
151
|
// Save the synthesized user prompt to the conversation (skip on resume — task prompt already persisted).
|
|
149
152
|
if (!resume) {
|
|
@@ -19,9 +19,11 @@ export interface EventThrottle {
|
|
|
19
19
|
windowSec: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** `engine`/`model` pin the runtime a run uses (e.g. engine 'agentx' + model
|
|
23
|
+
* 'cursor/composer-2.5'); both ride the prompt-directive channel — see runner.ts. */
|
|
22
24
|
export type Task =
|
|
23
|
-
| { kind: 'prompt'; prompt?: string; promptFile?: string; model?: string }
|
|
24
|
-
| { kind: 'bash'; command: string; model?: string }
|
|
25
|
+
| { kind: 'prompt'; prompt?: string; promptFile?: string; model?: string; engine?: string }
|
|
26
|
+
| { kind: 'bash'; command: string; model?: string; engine?: string }
|
|
25
27
|
| { kind: 'job'; command: string };
|
|
26
28
|
|
|
27
29
|
/** Visibility: 'system' schedules + their sessions are shared with all whitelisted users; 'user' is private to createdBy. */
|