shraga 0.1.53 → 0.1.55

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.53",
3
+ "version": "0.1.55",
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';
@@ -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(`[directives] Unknown model alias: "${t}"`);
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 model knob rendered to "" must not ship a bogus empty model — omit the field.
304
- if ('model' in rendered.task && !(rendered.task as { model?: string }).model) delete (rendered.task as { model?: string }).model;
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 rides the same [model] prompt-directive channel users type by hand — parseDirectives
144
- // strips it and resolves aliases. Prepending (vs new plumbing) also persists the choice into the
145
- // saved prompt, so the session UI shows which model the schedule actually requested.
146
- if (!resume && task.model) prompt = `[${task.model}] ${prompt}`;
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. */